mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
fix(config-backup): probe a custom folder before the retention fallback reads it, test that Save omits a displayed-only retention
This commit is contained in:
1 parent
9c0fc30927
commit
8a4ff775fb
9 files changed
+173
-25
No files matched your search
@@ -1374,6 +1374,25 @@ Object.assign(CodemanApp.prototype, {
|
||||
// ── Config backups ──────────────────────────────────────────────────────────
|
||||
// Server-side files, so these read the SAVED settings: a folder or retention edit applies after Save.
|
||||
|
||||
/**
|
||||
* The retention keys a Save sends: a value only once it was saved before or edited here. After a reset
|
||||
* the form merely DISPLAYS what the passes use (the retention the backups recorded), and writing that back
|
||||
* would replace it with whatever a different folder or an older default says.
|
||||
*/
|
||||
_configBackupRetentionPayload(prev) {
|
||||
const count = document.getElementById('appSettingsConfigBackupKeepCount');
|
||||
const days = document.getElementById('appSettingsConfigBackupKeepDays');
|
||||
const out = {};
|
||||
if (prev.configBackupKeepCount !== undefined || count.dataset.touched) {
|
||||
out.configBackupKeepCount = Math.min(500, Math.max(2, parseInt(count.value, 10) || 20));
|
||||
}
|
||||
if (prev.configBackupKeepDays !== undefined || days.dataset.touched) {
|
||||
const n = parseInt(days.value, 10);
|
||||
out.configBackupKeepDays = Math.min(3650, Math.max(0, Number.isNaN(n) ? 30 : n));
|
||||
}
|
||||
return out;
|
||||
},
|
||||
|
||||
/** Fill the backup list, and show the default folder as the input's placeholder. */
|
||||
async loadConfigBackups() {
|
||||
const out = this.$('configBackupList');
|
||||
@@ -2887,14 +2906,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
mcpSyncEnabled: document.getElementById('appSettingsMcpSync').checked,
|
||||
configBackupEnabled: document.getElementById('appSettingsConfigBackup').checked,
|
||||
configBackupDir: document.getElementById('appSettingsConfigBackupDir').value.trim(),
|
||||
// Sent only once saved before or edited here: a value merely displayed from the server (after a reset)
|
||||
// would otherwise be written back and replace the retention the backups recorded.
|
||||
...(_prev.configBackupKeepCount !== undefined || document.getElementById('appSettingsConfigBackupKeepCount').dataset.touched
|
||||
? { configBackupKeepCount: Math.min(500, Math.max(2, parseInt(document.getElementById('appSettingsConfigBackupKeepCount').value, 10) || 20)) }
|
||||
: {}),
|
||||
...(_prev.configBackupKeepDays !== undefined || document.getElementById('appSettingsConfigBackupKeepDays').dataset.touched
|
||||
? { configBackupKeepDays: Math.min(3650, Math.max(0, Number.isNaN(parseInt(document.getElementById('appSettingsConfigBackupKeepDays').value, 10)) ? 30 : parseInt(document.getElementById('appSettingsConfigBackupKeepDays').value, 10))) }
|
||||
: {}),
|
||||
...this._configBackupRetentionPayload(_prev),
|
||||
readMyMindEnabled: document.getElementById('appSettingsReadMyMind').checked,
|
||||
ultracodeFloatingWindows: document.getElementById('appSettingsUltracodeFloatingWindows').checked,
|
||||
showMultiMonitorButton: document.getElementById('appSettingsShowMultiMonitorButton').checked,
|
||||
|
||||
@@ -70,12 +70,23 @@ export interface ConfigBackupStatus {
|
||||
backups: ConfigBackupSummary[];
|
||||
}
|
||||
|
||||
export async function readEffectiveBackupSettings(): Promise<{ settings: ConfigBackupSettings; dir: string }> {
|
||||
/**
|
||||
* The settings in effect and the folder they name. A custom folder is probed (bounded, #516) BEFORE
|
||||
* the recorded-retention fallback reads it, so this throws `BackupDirUnavailableError` for a folder
|
||||
* that does not answer and never lists it; `present` is false for a custom folder that does not exist yet.
|
||||
*/
|
||||
export async function readEffectiveBackupSettings(): Promise<{
|
||||
settings: ConfigBackupSettings;
|
||||
dir: string;
|
||||
present: boolean;
|
||||
}> {
|
||||
const raw = await readJsonConfig<Record<string, unknown>>(SETTINGS_PATH, 'settings.json', {});
|
||||
const base = readConfigBackupSettings(raw);
|
||||
const dir = resolveConfigBackupDir(base, getDataDir());
|
||||
const present = await probeBackupDir(base, dir);
|
||||
// The same fallback the timer pass uses, so the form never shows 20 / 30 while a pass keeps 100 / 0.
|
||||
return { settings: withRecordedRetention(base, raw, dir, readConfigBackupOrigin(getDataDir())), dir };
|
||||
const settings = present ? withRecordedRetention(base, raw, dir, readConfigBackupOrigin(getDataDir())) : base;
|
||||
return { settings, dir, present };
|
||||
}
|
||||
|
||||
/** A user-chosen backup folder that is not safe to touch (bounded probe, as for case paths, #516). */
|
||||
@@ -158,16 +169,16 @@ export function registerConfigBackupRoutes(app: FastifyInstance): void {
|
||||
app.get('/api/config-backups', async (req, reply): Promise<ApiResponse<ConfigBackupStatus>> => {
|
||||
const denied = deny(req, reply);
|
||||
if (denied) return denied;
|
||||
const { settings, dir } = await readEffectiveBackupSettings();
|
||||
// Read-only: a listing must not create config-backup-id, and without one nothing can be ours.
|
||||
const origin = readConfigBackupOrigin(getDataDir());
|
||||
let present: boolean;
|
||||
let eff: Awaited<ReturnType<typeof readEffectiveBackupSettings>>;
|
||||
try {
|
||||
present = await probeBackupDir(settings, dir);
|
||||
eff = await readEffectiveBackupSettings();
|
||||
} catch (err) {
|
||||
reply.code(503);
|
||||
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, getErrorMessage(err));
|
||||
}
|
||||
const { settings, dir, present } = eff;
|
||||
// Read-only: a listing must not create config-backup-id, and without one nothing can be ours.
|
||||
const origin = readConfigBackupOrigin(getDataDir());
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
@@ -201,8 +212,7 @@ export function registerConfigBackupRoutes(app: FastifyInstance): void {
|
||||
return createErrorResponse(ApiErrorCode.INVALID_INPUT, '`from` is required (a backup id or "current")');
|
||||
}
|
||||
try {
|
||||
const { settings, dir } = await readEffectiveBackupSettings();
|
||||
await probeBackupDir(settings, dir);
|
||||
const { dir } = await readEffectiveBackupSettings();
|
||||
const data = diffConfigBackups({
|
||||
dataDir: getDataDir(),
|
||||
backupDir: dir,
|
||||
@@ -229,8 +239,7 @@ export function registerConfigBackupRoutes(app: FastifyInstance): void {
|
||||
if (denied) return denied;
|
||||
const { id } = req.params as { id: string };
|
||||
try {
|
||||
const { settings, dir } = await readEffectiveBackupSettings();
|
||||
await probeBackupDir(settings, dir);
|
||||
const { dir } = await readEffectiveBackupSettings();
|
||||
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();
|
||||
|
||||
Reference in new issue
Block a user