feat(settings): data-safe editors, deep-linked search, Data/About/Cost pages
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
This commit is contained in:
+56
-13
@@ -357,18 +357,35 @@ homeRoutes("/claude-home", "CLAUDE_HOME", "claude_home", getClaudeHome, setClaud
|
||||
// into the current home at its spawn, so a move needs no repair here.
|
||||
homeRoutes("/codex-home", "CODEX_HOME", "codex_home", getCodexHome, setCodexHome);
|
||||
|
||||
// POST /api/settings/cleanup — abandon stale sessions, purge old data
|
||||
// POST /api/settings/cleanup — abandon stale sessions, purge old data.
|
||||
// `purge_days` is a whole number ≥ 1 (0.1 days would purge almost everything);
|
||||
// `dry_run: true` answers the same counts and changes nothing.
|
||||
const PURGE_CHUNK = 500;
|
||||
router.post("/cleanup", (req, res) => {
|
||||
const { abandon_hours, purge_days } = req.body;
|
||||
const dryRun = req.body.dry_run === true;
|
||||
if (purge_days !== undefined && purge_days !== null) {
|
||||
if (!Number.isInteger(purge_days) || purge_days < 1) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: "INVALID_PURGE_DAYS",
|
||||
message: "purge_days must be a whole number of days, 1 or more",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
const result = { abandoned: 0, purged_sessions: 0, purged_events: 0, purged_agents: 0 };
|
||||
if (dryRun) result.dry_run = true;
|
||||
// A dry run abandons nothing, so the purge below would not see the sessions a
|
||||
// real run abandons first and then purges; it adds them back from this list.
|
||||
let wouldAbandon = [];
|
||||
|
||||
if (abandon_hours && typeof abandon_hours === "number" && abandon_hours > 0) {
|
||||
// Mark active sessions with no recent events as abandoned
|
||||
const cutoff = new Date(Date.now() - abandon_hours * 3600 * 1000).toISOString();
|
||||
const stale = db
|
||||
.prepare(
|
||||
`SELECT s.id FROM sessions s
|
||||
`SELECT s.id, s.started_at FROM sessions s
|
||||
WHERE s.status = 'active'
|
||||
AND s.started_at < ?
|
||||
AND NOT EXISTS (
|
||||
@@ -380,17 +397,22 @@ router.post("/cleanup", (req, res) => {
|
||||
const abandon = db.prepare(
|
||||
"UPDATE sessions SET status = 'abandoned', ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = ?"
|
||||
);
|
||||
// Also complete any lingering agents
|
||||
const complete = db.prepare(
|
||||
"UPDATE agents SET status = 'completed', ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE session_id = ? AND status IN ('waiting','working')"
|
||||
);
|
||||
db.transaction(() => {
|
||||
for (const row of stale) {
|
||||
abandon.run(row.id);
|
||||
complete.run(row.id);
|
||||
}
|
||||
})();
|
||||
result.abandoned = stale.length;
|
||||
if (dryRun) {
|
||||
result.abandoned = stale.length;
|
||||
wouldAbandon = stale;
|
||||
} else {
|
||||
// Also complete any lingering agents
|
||||
const complete = db.prepare(
|
||||
"UPDATE agents SET status = 'completed', ended_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE session_id = ? AND status IN ('waiting','working')"
|
||||
);
|
||||
db.transaction(() => {
|
||||
for (const row of stale) {
|
||||
abandon.run(row.id);
|
||||
complete.run(row.id);
|
||||
}
|
||||
})();
|
||||
result.abandoned = stale.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (purge_days && typeof purge_days === "number" && purge_days > 0) {
|
||||
@@ -402,6 +424,27 @@ router.post("/cleanup", (req, res) => {
|
||||
)
|
||||
.all(cutoff);
|
||||
|
||||
if (dryRun) {
|
||||
// Exactly the ids the real run deletes: those already purgeable, plus
|
||||
// the stale ones it abandons first that are also older than the cutoff.
|
||||
const ids = [
|
||||
...toDelete.map((r) => r.id),
|
||||
...wouldAbandon.filter((r) => r.started_at < cutoff).map((r) => r.id),
|
||||
];
|
||||
for (let i = 0; i < ids.length; i += PURGE_CHUNK) {
|
||||
const chunk = ids.slice(i, i + PURGE_CHUNK);
|
||||
const placeholders = chunk.map(() => "?").join(",");
|
||||
result.purged_events += db
|
||||
.prepare(`SELECT COUNT(*) AS n FROM events WHERE session_id IN (${placeholders})`)
|
||||
.get(...chunk).n;
|
||||
result.purged_agents += db
|
||||
.prepare(`SELECT COUNT(*) AS n FROM agents WHERE session_id IN (${placeholders})`)
|
||||
.get(...chunk).n;
|
||||
}
|
||||
result.purged_sessions = ids.length;
|
||||
return res.json({ ok: true, ...result });
|
||||
}
|
||||
|
||||
// One transaction, in chunks: a single IN (...) list of every id would
|
||||
// pass SQLite's bound-variable limit on a large history.
|
||||
db.transaction(() => {
|
||||
|
||||
Reference in New Issue
Block a user