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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user