fix(config-backup): origin is a stored random id, Settings refills after a settings restore, clis.json reloads

Review round 2 on #661: the hostname in the origin orphaned every snapshot when a Docker container was recreated, so the origin is a random id kept in <data dir>/config-backup-id; after restoring settings.json the Settings form is re-read from /api/settings so Save cannot write the old values back; the restore confirm names the files and the credential ones; clis.json is reloaded (reloadCliRegistry) and users.json needs no restart; API reference and route docs corrected.
This commit is contained in:
Devvyn committed 2026-10-20 18:01:42 +08:00
1 parent 247740fc77
commit 8b27e4beb6
10 files changed
+186 -23

No files matched your search

+11 -2
View File
@@ -1482,7 +1482,11 @@ Object.assign(CodemanApp.prototype, {
},
async restoreConfigBackup(id) {
if (!confirm('Restore this backup? Its files replace the current ones (a copy of the current files is made first). Some files only take effect after a restart.')) return;
const names = ((this._configBackups || []).find((b) => b.id === id)?.files || []).map((f) => f.name);
const secret = names.filter((n) => ['.env', 'users.json', 'push-keys.json', 'custom-model-hosts.json', 'webhook.json'].includes(n));
const list = names.length ? `\n\nFiles replaced: ${names.join(', ')}` : '';
const warn = secret.length ? `\n\nThis rolls back credentials and accounts (${secret.join(', ')}): passwords, disabled accounts and keys return to how they were then.` : '';
if (!confirm(`Restore this backup? Its files replace the current ones (a copy of the current files is made first). Some files only take effect after a restart.${list}${warn}`)) return;
const res = await this._apiPost(`/api/config-backups/${encodeURIComponent(id)}/restore`, {});
let body = null;
try { body = res ? await res.json() : null; } catch { /* fall through */ }
@@ -1492,7 +1496,12 @@ Object.assign(CodemanApp.prototype, {
}
const d = body.data;
const restart = d.restartRequired?.length ? ` Restart Codeman to apply: ${d.restartRequired.join(', ')}.` : '';
this.showToast(`Restored ${d.restored.length} file${d.restored.length === 1 ? '' : 's'}. Reload this page to see settings.${restart}`, 'success');
// The form still holds the pre-restore values, and Save would PUT them over the restored file.
if (d.restored.includes('settings.json')) {
await this.loadAppSettingsFromServer();
this.openAppSettings();
}
this.showToast(`Restored ${d.restored.length} file${d.restored.length === 1 ? '' : 's'}.${restart}`, 'success');
this.loadConfigBackups();
},
+8 -4
View File
@@ -7,9 +7,9 @@
* GET /api/config-backups/diff?from=&to= — what differs between two snapshots, or a snapshot and the
* running config (`current`, the default for `to`).
*
* The files include credentials (`.env`, `users.json`, endpoint keys), so in multi-user mode all three
* The files include credentials (`.env`, `users.json`, endpoint keys), so in multi-user mode all four
* are admin only. Responses carry file NAMES and sizes, never file content, except the diff, which shows
* changed values but never a credential's (`.env`, `users.json`, `push-keys.json` and any key that looks
* changed values but never a credential's (`.env`, `users.json`, `push-keys.json`, `webhook.json`, `intents.json` and any key that looks
* like a token/secret/password are reported as "changed" only). Restore replaces files the running server
* may have cached: `restartRequired` says which ones only apply after a restart.
*/
@@ -18,6 +18,7 @@ import { createRequire } from 'node:module';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { ApiErrorCode, createErrorResponse, getErrorMessage, type ApiResponse } from '../../types.js';
import { isAdmin, readJsonConfig, SETTINGS_PATH } from '../route-helpers.js';
import { reloadCliRegistry } from '../../config/cli-registry/registry.js';
import { isMultiUserMode } from '../../config/multiuser.js';
import { getDataDir } from '../../config/instance.js';
import { intentStore } from '../../intent-store.js';
@@ -53,8 +54,9 @@ const APP_VERSION: string = (() => {
}
})();
/** Files whose running-process copy only reloads on restart (the rest are read per request). */
const RESTART_REQUIRED = new Set<string>(['clis.json', 'users.json', '.env', 'push-keys.json', 'settings.json']);
/** Files whose running-process copy only reloads on restart (the rest are read per request; `users.json`
* is re-read within a second, `clis.json` is reloaded below, `intents.json` by `reload()`). */
const RESTART_REQUIRED = new Set<string>(['.env', 'push-keys.json', 'settings.json']);
export interface ConfigBackupStatus {
settings: ConfigBackupSettings;
@@ -203,6 +205,8 @@ export function registerConfigBackupRoutes(app: FastifyInstance): void {
const out = restoreConfigBackup({ dataDir: getDataDir(), backupDir: dir, id, appVersion: APP_VERSION });
// IntentStore caches the file and rewrites it whole on the next prompt, which would undo the restore.
if (out.restored.includes('intents.json')) intentStore.reload();
// restoreConfigBackup writes clis.json directly (the one writer besides mutateRegistryFile).
if (out.restored.includes('clis.json')) reloadCliRegistry();
return {
success: true,
data: {