fix(health): re-run integrity check on Refresh health, not just re-read cache

The dashboard's corruption banner refresh action was a no-op for clearing
stale corruption flags after the user repaired the DB. Database.
scheduleBackgroundIntegrityCheck runs the integrity check exactly once at
engine boot and then early-returns forever after, so corruptionDetected
was sticky for the life of the process. POST /api/health/refresh just
read the cached flag back.

Add Database.refreshIntegrityCheck() and TaskStore.refreshDatabaseHealth()
which synchronously re-run the integrity check and update the cached
state, and have the route use them. After REINDEX / fn db --vacuum / any
in-place repair, users can now clear the banner without restarting the
engine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-20 23:02:53 -07:00
parent 85f0de9881
commit 5c15031416
5 changed files with 65 additions and 1 deletions

View File

@@ -60,3 +60,31 @@ describe("TaskStore.getDatabaseHealth", () => {
expect(health.corruptionErrors).toEqual(["one", "two", "three", "four", "five"]);
});
});
describe("TaskStore.refreshDatabaseHealth", () => {
const harness = createTaskStoreTestHarness();
let store: TaskStore;
beforeEach(async () => {
await harness.beforeEach();
store = harness.store();
});
it("clears a stale corruption flag after the underlying DB is repaired", () => {
const db = store.getDatabase();
db.corruptionDetected = true;
db.integrityCheckErrors = ["wrong # of entries in index sqlite_autoindex_tasks_1"];
db.integrityCheckLastRunAt = "2026-05-11T12:34:56.000Z";
expect(store.getDatabaseHealth().corruptionDetected).toBe(true);
const health = store.refreshDatabaseHealth();
expect(health.corruptionDetected).toBe(false);
expect(health.healthy).toBe(true);
expect(health.corruptionErrors).toEqual([]);
expect(health.isRunning).toBe(false);
expect(health.lastCheckedAt).not.toBeNull();
expect(health.lastCheckedAt?.toISOString()).not.toBe("2026-05-11T12:34:56.000Z");
});
});

View File

@@ -1455,6 +1455,25 @@ export class Database {
return { ok: true };
}
/**
* Synchronously re-run `integrityCheck()` and update the cached corruption
* state (`corruptionDetected`, `integrityCheckErrors`, `integrityCheckLastRunAt`).
*
* The background scheduler in `scheduleBackgroundIntegrityCheck()` runs the
* check exactly once at boot; without this on-demand path the
* `corruptionDetected` flag is sticky for the life of the process, which
* leaves the "Refresh health" UI a no-op after the user repairs the DB
* (e.g. via `REINDEX`).
*/
refreshIntegrityCheck(): { ok: true } | { ok: false; errors: string[] } {
const integrity = this.integrityCheck();
this.integrityCheckPending = false;
this.integrityCheckLastRunAt = new Date().toISOString();
this.corruptionDetected = !integrity.ok;
this.integrityCheckErrors = integrity.ok ? [] : [...integrity.errors];
return integrity;
}
recoverDatabase(outputPath: string): boolean {
if (this.inMemory) {
return false;

View File

@@ -9465,6 +9465,18 @@ ${stepsSection}`;
};
}
/**
* Force-run an integrity check synchronously and return the refreshed health
* snapshot. Used by `POST /api/health/refresh` so users can clear a stale
* corruption banner after they've repaired the database in place
* (e.g. via `REINDEX` or `fn db --vacuum`) without having to restart the
* engine to re-arm the once-at-boot background check.
*/
refreshDatabaseHealth(): ReturnType<TaskStore["getDatabaseHealth"]> {
this.db.refreshIntegrityCheck();
return this.getDatabaseHealth();
}
getDistributedTaskIdAllocator(): DistributedTaskIdAllocator {
if (!this.distributedTaskIdAllocator) {
this.distributedTaskIdAllocator = createDistributedTaskIdAllocator(this.db);

View File

@@ -1329,7 +1329,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
app.post("/api/health/refresh", (_req, res) => {
const report = store.refreshTaskIdIntegrityReport();
const database = store.getDatabaseHealth();
const database = store.refreshDatabaseHealth();
res.json({
status: !database.healthy || database.corruptionDetected || report.status === "anomaly" ? "degraded" : "ok",
version: cliPackageVersion,