fix: localize home and split auth setup flow

This commit is contained in:
2026-05-07 12:25:16 +08:00
parent fdec8960c2
commit 31d63607ca
11 changed files with 389 additions and 255 deletions
+57 -57
View File
@@ -136,13 +136,13 @@ function renderOverviewSummary() {
const mappedCount = state.sources.filter((source) => source.displayNameSource === 'server-db').length;
overviewSummaryElement.innerHTML = renderSummaryRows([
{ label: 'Total sources', value: String(sourceCount) },
{ label: 'Enhanced sources', value: String(readyCount) },
{ label: 'Failed enhancements', value: String(failedCount) },
{ label: 'Mapped task names', value: String(mappedCount) },
{ label: 'Server DB', value: state.serverDb?.available ? state.serverDb.originalFilename : 'Not uploaded' },
{ label: 'Global WebDAV', value: state.globalDefaults?.configured ? 'Configured' : 'Not configured' },
{ label: 'Active certificate', value: state.tls?.activeSource ?? 'Unavailable' }
{ label: '数据源总数', value: String(sourceCount) },
{ label: '已增强数据源', value: String(readyCount) },
{ label: '增强失败', value: String(failedCount) },
{ label: '已映射任务名', value: String(mappedCount) },
{ label: 'Server DB', value: state.serverDb?.available ? state.serverDb.originalFilename : '未上传' },
{ label: '全局 WebDAV', value: state.globalDefaults?.configured ? '已配置' : '未配置' },
{ label: '当前证书来源', value: state.tls?.activeSource ?? '不可用' }
]);
}
@@ -150,15 +150,15 @@ function renderServerDbSummary() {
if (!state.serverDb?.available) {
serverDbSummaryElement.innerHTML = renderSummaryRows([
{ label: 'Status', value: 'No server database uploaded yet' },
{ label: 'Note', value: 'Task names and inferred target URLs stay unavailable until you upload Duplicati-server.sqlite.' }
{ label: '说明', value: '未上传 Duplicati-server.sqlite 之前,任务名和推导目标 URL 不可用。' }
]);
return;
}
serverDbSummaryElement.innerHTML = renderSummaryRows([
{ label: 'Current file', value: state.serverDb.originalFilename },
{ label: 'Uploaded at', value: state.serverDb.uploadedAt },
{ label: 'Backup entries', value: String(state.serverDb.backupCount) }
{ label: '当前文件', value: state.serverDb.originalFilename },
{ label: '上传时间', value: state.serverDb.uploadedAt },
{ label: '任务条目数', value: String(state.serverDb.backupCount) }
]);
}
@@ -167,46 +167,46 @@ function renderGlobalDefaultsSummary() {
if (!defaults?.configured) {
writeGlobalDefaultsForm(null);
globalDefaultsSummaryElement.innerHTML = renderSummaryRows([
{ label: 'Status', value: 'No global defaults saved' },
{ label: 'Note', value: 'Save shared credentials here if most tasks use the same WebDAV account and passphrase.' }
{ label: '状态', value: '尚未保存全局默认值' },
{ label: '说明', value: '如果大多数任务共用同一套 WebDAV 账号和口令,可以在这里统一保存。' }
]);
return;
}
writeGlobalDefaultsForm(defaults);
globalDefaultsSummaryElement.innerHTML = renderSummaryRows([
{ label: 'Fallback URL', value: defaults.webdavBaseUrl ?? 'Not set' },
{ label: 'Auth mode', value: defaults.authMode ?? 'Auto' },
{ label: '兜底 URL', value: defaults.webdavBaseUrl ?? '未设置' },
{ label: '认证方式', value: defaults.authMode ?? '自动' },
{
label: 'Stored secrets',
label: '已保存凭据',
value: `username=${defaults.hasUsername ? 'yes' : 'no'}, password=${defaults.hasPassword ? 'yes' : 'no'}, passphrase=${defaults.hasPassphrase ? 'yes' : 'no'}`
},
{ label: 'Updated at', value: defaults.updatedAt ?? 'Unknown' }
{ label: '更新时间', value: defaults.updatedAt ?? '未知' }
]);
}
function renderTlsSummary() {
if (!state.tls) {
tlsSummaryElement.innerHTML = renderSummaryRows([{ label: 'Status', value: 'TLS settings not loaded yet' }]);
tlsSummaryElement.innerHTML = renderSummaryRows([{ label: '状态', value: 'TLS 配置尚未加载' }]);
return;
}
writeTlsForm(state.tls);
tlsDeleteCustomButton.disabled = !state.tls.hasCustomCertificate;
tlsSummaryElement.innerHTML = renderSummaryRows([
{ label: 'HTTP endpoint', value: state.tls.access?.httpBaseUrl ?? 'Unknown' },
{ label: 'HTTPS endpoint', value: state.tls.access?.httpsBaseUrl ?? 'Unknown' },
{ label: 'Configured mode', value: state.tls.mode ?? 'self-signed' },
{ label: 'Active certificate', value: state.tls.activeSource ?? 'self-signed' },
{ label: 'Primary domain', value: state.tls.primaryDomain || 'localhost' },
{ label: 'Extra SAN entries', value: (state.tls.subjectAltNames ?? []).join(', ') || 'None' },
{ label: 'HTTP 访问地址', value: state.tls.access?.httpBaseUrl ?? '未知' },
{ label: 'HTTPS 访问地址', value: state.tls.access?.httpsBaseUrl ?? '未知' },
{ label: '配置模式', value: state.tls.mode ?? 'self-signed' },
{ label: '当前生效证书', value: state.tls.activeSource ?? 'self-signed' },
{ label: '主域名', value: state.tls.primaryDomain || 'localhost' },
{ label: '额外 SAN', value: (state.tls.subjectAltNames ?? []).join(', ') || '' },
{
label: 'Validity',
value: state.tls.certificate ? `${state.tls.certificate.validFrom} -> ${state.tls.certificate.validTo}` : 'Unknown'
label: '有效期',
value: state.tls.certificate ? `${state.tls.certificate.validFrom} -> ${state.tls.certificate.validTo}` : '未知'
},
{ label: 'Fingerprint', value: state.tls.certificate?.fingerprint256 ?? 'Unknown' },
{ label: 'Last error', value: state.tls.lastErrorMessage ?? 'None' },
{ label: 'Cookie policy', value: state.tls.cookieNote ?? 'Shared between HTTP and HTTPS' }
{ label: '指纹', value: state.tls.certificate?.fingerprint256 ?? '未知' },
{ label: '最近错误', value: state.tls.lastErrorMessage ?? '' },
{ label: 'Cookie 说明', value: state.tls.cookieNote ?? 'HTTP/HTTPS 共用登录态' }
]);
}
@@ -224,16 +224,16 @@ function renderSources() {
sourceListElement.innerHTML = state.sources
.map((source) => {
const targetHint = source.webdav?.effectiveWebdavBaseUrl
? `<div class="summary-chip">Default target URL source: ${escapeHtml(source.webdav.effectiveWebdavBaseUrlSource ?? 'unknown')}</div>`
: '<div class="summary-chip">No effective default target URL yet</div>';
? `<div class="summary-chip">默认目标 URL 来源:${escapeHtml(source.webdav.effectiveWebdavBaseUrlSource ?? 'unknown')}</div>`
: '<div class="summary-chip">暂无可用的默认目标 URL</div>';
return renderSourceSummaryCard(
source,
`
${targetHint}
<div class="card-actions">
<a class="ghost-button button-link" href="${linkToSource(source.id)}#overview">Open workspace</a>
<button class="ghost-button danger-button" type="button" data-delete-source-id="${escapeHtml(source.id)}">Delete source</button>
<a class="ghost-button button-link" href="${linkToSource(source.id)}#overview">进入工作台</a>
<button class="ghost-button danger-button" type="button" data-delete-source-id="${escapeHtml(source.id)}">删除 source</button>
</div>
`
);
@@ -275,13 +275,13 @@ async function handleTaskDbUpload(event) {
const file = databaseInput.files?.[0];
if (!file) {
feedback.set('error', 'Choose a task SQLite file first.');
feedback.set('error', '请先选择一个任务 SQLite 文件。');
return;
}
uploadButton.disabled = true;
refreshButton.disabled = true;
feedback.set('info', 'Uploading task database...');
feedback.set('info', '正在上传任务数据库...');
try {
const formData = new FormData();
@@ -302,8 +302,8 @@ async function handleTaskDbUpload(event) {
feedback.set(
payload.reused ? 'info' : 'success',
payload.reused
? 'That task database was already uploaded, so the existing source was reused.'
: 'Task database uploaded successfully.'
? '这个任务数据库之前已经上传过,系统复用了现有 source。'
: '任务数据库上传成功。'
);
} catch (error) {
feedback.set('error', error.message);
@@ -319,12 +319,12 @@ async function handleServerDbUpload(event) {
const file = serverDbInput.files?.[0];
if (!file) {
feedback.set('error', 'Choose Duplicati-server.sqlite first.');
feedback.set('error', '请先选择 Duplicati-server.sqlite');
return;
}
serverDbUploadButton.disabled = true;
feedback.set('info', 'Uploading Server DB and refreshing task-name mappings...');
feedback.set('info', '正在上传 Server DB 并刷新任务名映射...');
try {
const formData = new FormData();
@@ -341,7 +341,7 @@ async function handleServerDbUpload(event) {
await Promise.all([loadSources(), loadServerDbSummary()]);
serverDbForm.reset();
feedback.set('success', 'Server DB uploaded successfully.');
feedback.set('success', 'Server DB 上传成功。');
} catch (error) {
feedback.set('error', error.message);
} finally {
@@ -353,7 +353,7 @@ async function handleGlobalDefaultsSave(event) {
event.preventDefault();
feedback.clear();
globalDefaultsSaveButton.disabled = true;
feedback.set('info', 'Saving global WebDAV defaults...');
feedback.set('info', '正在保存全局 WebDAV 默认值...');
try {
const payload = await fetchJson('/api/webdav-defaults', {
@@ -370,7 +370,7 @@ async function handleGlobalDefaultsSave(event) {
await loadSources();
feedback.set(
payload.defaults.configured ? 'success' : 'info',
payload.defaults.configured ? 'Global defaults saved.' : 'Global defaults cleared.'
payload.defaults.configured ? '全局默认值已保存。' : '全局默认值已清空。'
);
} catch (error) {
feedback.set('error', error.message);
@@ -384,7 +384,7 @@ async function handleTlsJsonSave(event) {
feedback.clear();
tlsSaveButton.disabled = true;
tlsUploadButton.disabled = true;
feedback.set('info', 'Saving TLS configuration and applying certificate...');
feedback.set('info', '正在保存 TLS 配置并应用证书...');
try {
const payload = await fetchJson('/api/system/tls', {
@@ -397,7 +397,7 @@ async function handleTlsJsonSave(event) {
state.tls = payload.tls;
renderTlsSummary();
renderOverviewSummary();
feedback.set('success', `TLS settings applied. Active certificate source: ${payload.tls.activeSource}.`);
feedback.set('success', `TLS 配置已应用,当前证书来源:${payload.tls.activeSource}`);
} catch (error) {
feedback.set('error', error.message);
} finally {
@@ -415,13 +415,13 @@ async function handleTlsUpload(event) {
const chainFile = document.querySelector('#tlsChainInput')?.files?.[0] ?? null;
if (!certificateFile || !privateKeyFile) {
feedback.set('error', 'Upload both the certificate file and the private key file.');
feedback.set('error', '请同时上传证书文件和私钥文件。');
return;
}
tlsSaveButton.disabled = true;
tlsUploadButton.disabled = true;
feedback.set('info', 'Uploading custom certificate...');
feedback.set('info', '正在上传自定义证书...');
try {
const formData = new FormData();
@@ -446,7 +446,7 @@ async function handleTlsUpload(event) {
renderTlsSummary();
renderOverviewSummary();
tlsUploadForm.reset();
feedback.set('success', 'Custom certificate uploaded and applied.');
feedback.set('success', '自定义证书已上传并生效。');
} catch (error) {
feedback.set('error', error.message);
} finally {
@@ -457,12 +457,12 @@ async function handleTlsUpload(event) {
async function handleDeleteCustomTls() {
feedback.clear();
if (!window.confirm('Delete the stored custom certificate and fall back to a self-signed certificate?')) {
if (!window.confirm('确定要删除当前自定义证书,并回退到自签证书吗?')) {
return;
}
tlsDeleteCustomButton.disabled = true;
feedback.set('info', 'Deleting custom certificate and switching back to self-signed...');
feedback.set('info', '正在删除自定义证书,并切回自签证书...');
try {
const payload = await fetchJson('/api/system/tls/custom-certificate', {
@@ -471,7 +471,7 @@ async function handleDeleteCustomTls() {
state.tls = payload.tls;
renderTlsSummary();
renderOverviewSummary();
feedback.set('success', 'Custom certificate removed. The service is back on a self-signed certificate.');
feedback.set('success', '自定义证书已删除,系统已回退到自签证书。');
} catch (error) {
feedback.set('error', error.message);
} finally {
@@ -484,20 +484,20 @@ async function deleteSource(sourceId) {
const source = state.sources.find((item) => item.id === sourceId);
const label = source?.displayName || source?.originalFilename || sourceId;
const confirmed = window.confirm(
`Delete this source?\n\n${label}\n\nThis removes only the selected source and its local enhancement data. It does not delete Duplicati-server.sqlite.`
`确定要删除这个 source 吗?\n\n${label}\n\n这只会删除当前 source 及其本地增强数据,不会删除 Duplicati-server.sqlite`
);
if (!confirmed) {
return;
}
feedback.set('info', `Deleting ${label}...`);
feedback.set('info', `正在删除 ${label}...`);
try {
const payload = await fetchJson(`/api/sources/${encodeURIComponent(sourceId)}`, {
method: 'DELETE'
});
await Promise.all([loadSources(), loadServerDbSummary()]);
feedback.set('success', `Deleted ${payload.deleted.originalFilename}. Server DB was kept untouched.`);
feedback.set('success', `已删除 ${payload.deleted.originalFilename}Server DB 保持不变。`);
} catch (error) {
feedback.set('error', error.message);
}
@@ -529,7 +529,7 @@ tlsDeleteCustomButton.addEventListener('click', () => {
refreshButton.addEventListener('click', () => {
feedback.clear();
sourceListElement.textContent = 'Refreshing sources...';
sourceListElement.textContent = '正在刷新数据源列表...';
void Promise.all([loadSources(), loadServerDbSummary(), loadGlobalDefaults(), loadTlsSummary()]).catch((error) => {
feedback.set('error', error.message);
});
@@ -571,8 +571,8 @@ async function bootstrap() {
const authentication = await loadAuthState();
authStatusElement.textContent = authentication.authenticated
? `Signed in as ${authentication.username}`
: 'Not signed in';
? `已登录:${authentication.username}`
: '未登录';
bindLogoutButton(logoutButton, authStatusElement);
await Promise.all([loadServerDbSummary(), loadGlobalDefaults(), loadTlsSummary(), loadSources()]);
@@ -582,7 +582,7 @@ void bootstrap().catch((error) => {
feedback.set('error', error.message);
sourceListElement.innerHTML = `
<div class="empty-state-card">
<p>Could not load the control center.</p>
<p>无法加载控制台。</p>
</div>
`;
});