import { createFeedbackController, escapeHtml, fetchJson, linkToSource, renderSourceSummaryCard } from './modules/common.js'; const CONTROL_VIEWS = new Set(['overview', 'sources', 'upload', 'server-db', 'defaults']); const state = { sources: [], serverDb: null, globalDefaults: null, currentView: 'overview' }; const uploadForm = document.querySelector('#uploadForm'); const uploadButton = document.querySelector('#uploadButton'); const databaseInput = document.querySelector('#databaseInput'); const serverDbForm = document.querySelector('#serverDbForm'); const serverDbUploadButton = document.querySelector('#serverDbUploadButton'); const serverDbInput = document.querySelector('#serverDbInput'); const globalDefaultsForm = document.querySelector('#globalDefaultsForm'); const globalDefaultsSaveButton = document.querySelector('#globalDefaultsSaveButton'); const refreshButton = document.querySelector('#refreshButton'); const serverDbSummaryElement = document.querySelector('#serverDbSummary'); const globalDefaultsSummaryElement = document.querySelector('#globalDefaultsSummary'); const overviewSummaryElement = document.querySelector('#overviewSummary'); const sourceListElement = document.querySelector('#sourceList'); const viewPanels = [...document.querySelectorAll('[data-view-panel]')]; const viewLinks = [...document.querySelectorAll('[data-view-link]')]; const feedback = createFeedbackController(document.querySelector('#feedback')); function normalizeView(value) { return CONTROL_VIEWS.has(value) ? value : 'overview'; } function setCurrentView(nextView, { syncHash = true, replace = false } = {}) { state.currentView = normalizeView(nextView); for (const panel of viewPanels) { panel.hidden = panel.dataset.viewPanel !== state.currentView; } for (const link of viewLinks) { const active = link.dataset.viewLink === state.currentView; link.classList.toggle('is-active', active); link.setAttribute('aria-current', active ? 'page' : 'false'); } if (!syncHash) { return; } const url = new URL(window.location.href); url.hash = `#${state.currentView}`; if (replace) { window.history.replaceState(null, '', url); } else { window.history.pushState(null, '', url); } } function readGlobalDefaultsForm() { return { webdavBaseUrl: document.querySelector('#globalWebdavBaseUrl')?.value?.trim() ?? '', authMode: document.querySelector('#globalAuthMode')?.value ?? '', username: document.querySelector('#globalUsername')?.value?.trim() ?? '', password: document.querySelector('#globalPassword')?.value ?? '', passphrase: document.querySelector('#globalPassphrase')?.value ?? '' }; } function writeGlobalDefaultsForm(defaults) { document.querySelector('#globalWebdavBaseUrl').value = defaults?.webdavBaseUrl ?? ''; document.querySelector('#globalAuthMode').value = defaults?.authMode ?? ''; document.querySelector('#globalUsername').value = ''; document.querySelector('#globalPassword').value = ''; document.querySelector('#globalPassphrase').value = ''; } function renderOverviewSummary() { const sourceCount = state.sources.length; const readyCount = state.sources.filter((source) => source.enhancement?.status === 'ready').length; const failedCount = state.sources.filter((source) => source.enhancement?.status === 'failed').length; const mappedCount = state.sources.filter((source) => source.displayNameSource === 'server-db').length; const serverDbAvailable = Boolean(state.serverDb?.available); const defaultsConfigured = Boolean(state.globalDefaults?.configured); overviewSummaryElement.innerHTML = `
当前 source 数量 ${escapeHtml(String(sourceCount))}
已增强完成 ${escapeHtml(String(readyCount))}
增强失败 ${escapeHtml(String(failedCount))}
已映射任务名 ${escapeHtml(String(mappedCount))}
Server DB ${serverDbAvailable ? escapeHtml(state.serverDb.originalFilename) : '未上传'}
全局 WebDAV 默认值 ${defaultsConfigured ? '已配置' : '未配置'}
`; } function renderServerDbSummary() { if (!state.serverDb || !state.serverDb.available) { serverDbSummaryElement.innerHTML = `
当前状态 尚未上传 Duplicati-server.sqlite
说明 没有它也能浏览和增强,只是任务名和默认目标 URL 无法自动映射。
`; return; } serverDbSummaryElement.innerHTML = `
当前文件 ${escapeHtml(state.serverDb.originalFilename)}
上传时间 ${escapeHtml(state.serverDb.uploadedAt)}
任务数量 ${escapeHtml(String(state.serverDb.backupCount))}
`; } function renderGlobalDefaultsSummary() { const defaults = state.globalDefaults; if (!defaults?.configured) { globalDefaultsSummaryElement.innerHTML = `
当前状态 尚未保存全局默认值
说明 保存一次后,后续 source 可以继承这里的认证信息和备份口令。
`; writeGlobalDefaultsForm(null); return; } writeGlobalDefaultsForm(defaults); globalDefaultsSummaryElement.innerHTML = `
Fallback URL ${escapeHtml(defaults.webdavBaseUrl ?? '未设置')}
认证方式 ${escapeHtml(defaults.authMode ?? '自动')}
已保存凭据 username=${defaults.hasUsername ? 'yes' : 'no'}, password=${defaults.hasPassword ? 'yes' : 'no'}, passphrase=${defaults.hasPassphrase ? 'yes' : 'no'}
更新时间 ${escapeHtml(defaults.updatedAt ?? '未知')}
`; } function renderSources() { if (state.sources.length === 0) { sourceListElement.innerHTML = `

还没有任何任务数据源。

先上传一个随机名任务库,然后再进入工作台浏览目录、做增强或进入文件页预览下载。

`; return; } sourceListElement.innerHTML = state.sources .map((source) => { const targetHint = source.webdav?.effectiveWebdavBaseUrl ? `
默认目标 URL 来源: ${escapeHtml(source.webdav.effectiveWebdavBaseUrlSource ?? 'unknown')}
` : '
当前还没有可用的默认目标 URL
'; return renderSourceSummaryCard( source, ` ${targetHint}
进入工作台
` ); }) .join(''); } async function loadServerDbSummary() { const payload = await fetchJson('/api/server-db'); state.serverDb = payload.serverDb; renderServerDbSummary(); renderOverviewSummary(); } async function loadGlobalDefaults() { const payload = await fetchJson('/api/webdav-defaults'); state.globalDefaults = payload.defaults; renderGlobalDefaultsSummary(); renderOverviewSummary(); } async function loadSources() { const payload = await fetchJson('/api/sources'); state.sources = payload.sources; renderSources(); renderOverviewSummary(); } async function handleTaskDbUpload(event) { event.preventDefault(); feedback.clear(); const file = databaseInput.files?.[0]; if (!file) { feedback.set('error', '请先选择一个任务 SQLite 文件。'); return; } uploadButton.disabled = true; refreshButton.disabled = true; feedback.set('info', '正在上传任务数据库...'); try { const formData = new FormData(); formData.set('database', file); const response = await fetch('/api/sources/upload', { method: 'POST', body: formData }); const payload = await response.json(); if (!response.ok) { throw new Error(`${payload.error?.code ?? 'UPLOAD_FAILED'}: ${payload.error?.message ?? 'Upload failed.'}`); } await Promise.all([loadSources(), loadServerDbSummary()]); uploadForm.reset(); setCurrentView('sources'); feedback.set( payload.reused ? 'info' : 'success', payload.reused ? '这个任务库之前已经上传过,系统复用了现有 source。' : '任务数据库上传成功,可以进入对应工作台继续操作。' ); } catch (error) { feedback.set('error', error.message); } finally { uploadButton.disabled = false; refreshButton.disabled = false; } } async function handleServerDbUpload(event) { event.preventDefault(); feedback.clear(); const file = serverDbInput.files?.[0]; if (!file) { feedback.set('error', '请先选择 Duplicati-server.sqlite。'); return; } serverDbUploadButton.disabled = true; feedback.set('info', '正在上传 Server DB 并刷新任务名与目标 URL 映射...'); try { const formData = new FormData(); formData.set('database', file); const response = await fetch('/api/server-db/upload', { method: 'POST', body: formData }); const payload = await response.json(); if (!response.ok) { throw new Error(`${payload.error?.code ?? 'UPLOAD_FAILED'}: ${payload.error?.message ?? 'Upload failed.'}`); } await Promise.all([loadSources(), loadServerDbSummary()]); serverDbForm.reset(); feedback.set('success', 'Server DB 上传成功,任务名和默认目标 URL 已刷新。'); } catch (error) { feedback.set('error', error.message); } finally { serverDbUploadButton.disabled = false; } } async function handleGlobalDefaultsSave(event) { event.preventDefault(); feedback.clear(); globalDefaultsSaveButton.disabled = true; feedback.set('info', '正在保存全局 WebDAV 默认值...'); try { const payload = await fetchJson('/api/webdav-defaults', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(readGlobalDefaultsForm()) }); state.globalDefaults = payload.defaults; renderGlobalDefaultsSummary(); renderOverviewSummary(); await loadSources(); feedback.set( payload.defaults.configured ? 'success' : 'info', payload.defaults.configured ? '全局默认值已保存。' : '全局默认值已清空。' ); } catch (error) { feedback.set('error', error.message); } finally { globalDefaultsSaveButton.disabled = false; } } async function deleteSource(sourceId) { feedback.clear(); const source = state.sources.find((item) => item.id === sourceId); const label = source?.displayName || source?.originalFilename || sourceId; const confirmed = window.confirm(`确定要删除这个 source 吗?\n\n${label}\n\n这只会删除当前任务库和本地增强副本,不会删除 Duplicati-server.sqlite。`); if (!confirmed) { return; } feedback.set('info', `正在删除 ${label}...`); try { const payload = await fetchJson(`/api/sources/${encodeURIComponent(sourceId)}`, { method: 'DELETE' }); await Promise.all([loadSources(), loadServerDbSummary()]); feedback.set('success', `已删除 ${payload.deleted.originalFilename}。Server DB 保持不变。`); } catch (error) { feedback.set('error', error.message); } } uploadForm.addEventListener('submit', (event) => { void handleTaskDbUpload(event); }); serverDbForm.addEventListener('submit', (event) => { void handleServerDbUpload(event); }); globalDefaultsForm.addEventListener('submit', (event) => { void handleGlobalDefaultsSave(event); }); refreshButton.addEventListener('click', () => { feedback.clear(); sourceListElement.textContent = '正在刷新数据源列表...'; void Promise.all([loadSources(), loadServerDbSummary(), loadGlobalDefaults()]).catch((error) => { feedback.set('error', error.message); }); }); sourceListElement.addEventListener('click', (event) => { const target = event.target; if (!(target instanceof HTMLElement)) { return; } const actionElement = target.closest('[data-delete-source-id]'); if (!(actionElement instanceof HTMLElement)) { return; } const sourceId = actionElement.dataset.deleteSourceId; if (!sourceId) { return; } void deleteSource(sourceId); }); for (const link of viewLinks) { link.addEventListener('click', (event) => { event.preventDefault(); setCurrentView(normalizeView(link.dataset.viewLink ?? 'overview')); }); } window.addEventListener('hashchange', () => { setCurrentView(normalizeView(window.location.hash.slice(1)), { syncHash: false }); }); async function bootstrap() { const initialView = normalizeView(window.location.hash.slice(1) || 'overview'); setCurrentView(initialView, { replace: true }); await Promise.all([loadServerDbSummary(), loadGlobalDefaults(), loadSources()]); } void bootstrap().catch((error) => { feedback.set('error', error.message); sourceListElement.innerHTML = `

无法读取数据源列表。

`; });