fix(collector,trainer): migrate an existing collector DB on open; trainer handlers answer 500 JSON
The reviewer host's collector.sqlite was created by an earlier build, before the `kind` column. CREATE TABLE IF NOT EXISTS shapes only a new database, so every query naming the column failed: the collector's /health (container unhealthy), every booth ingest, and the trainer's readiness — whose stdlib server printed the traceback and dropped the socket, which the collector could only render as "trainer not reachable: fetch failed". Nine days like that. - CollectorDb.#migrate(): PRAGMA table_info against the list of columns added since the first deploy; ALTER TABLE ADD COLUMN for each missing one (all nullable or defaulted). Append to that list whenever a column joins the CREATE. Test replays the original schema: health, ingest, stats, a legacy row reads back with the defaults. - Trainer Handler._guarded(): any unexpected exception → 500 JSON naming it, never a dropped connection; /health keeps answering. Test drives readiness against an old-schema DB. - The collector's training status proxy includes the trainer's error text. Wiki: the incident and the schema rule (vision-review-outbox), what the message means (bodytype-classifier-training), log. Deploy: the new collector migrates on start; nothing manual. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -71,6 +71,39 @@ export class CollectorDb {
|
||||
CREATE INDEX IF NOT EXISTS items_pending ON items (reviewed_at, received_at);
|
||||
CREATE INDEX IF NOT EXISTS items_booth ON items (booth, received_at);
|
||||
`);
|
||||
this.#migrate();
|
||||
}
|
||||
|
||||
/** Columns added after the first deploy, with the DDL that adds them to an EXISTING
|
||||
* table. `CREATE TABLE IF NOT EXISTS` above only shapes a NEW database; a volume that
|
||||
* was created by an earlier build keeps its old columns, and every query naming a new
|
||||
* one then fails ("no such column: kind" — art-docker-station, 2026-09-16: the
|
||||
* collector's /health, every ingest, and the trainer's readiness all broke on a DB
|
||||
* from before `kind`). Each entry must be addable to a populated table, i.e. nullable
|
||||
* or carrying a DEFAULT. Append here whenever a column joins the CREATE above. */
|
||||
static readonly #ADDED_COLUMNS: ReadonlyArray<readonly [name: string, ddl: string]> = [
|
||||
["kind", "TEXT NOT NULL DEFAULT 'wash'"],
|
||||
["operator_ref", "TEXT NOT NULL DEFAULT ''"],
|
||||
["operator_category_id", "TEXT NOT NULL DEFAULT ''"],
|
||||
["operator_category_name", "TEXT NOT NULL DEFAULT ''"],
|
||||
["operator_classes", "TEXT NOT NULL DEFAULT '[]'"],
|
||||
["vision_category_id", "TEXT"],
|
||||
["downgraded", "INTEGER NOT NULL DEFAULT 0"],
|
||||
];
|
||||
|
||||
/** Bring an existing `items` table up to the current column set (idempotent). */
|
||||
#migrate(): void {
|
||||
const present = new Set(
|
||||
(this.#db.prepare("PRAGMA table_info(items)").all() as { name: string }[]).map((c) => c.name),
|
||||
);
|
||||
for (const [name, ddl] of CollectorDb.#ADDED_COLUMNS) {
|
||||
if (!present.has(name)) this.#db.exec(`ALTER TABLE items ADD COLUMN ${name} ${ddl}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** The current column names of `items` (for tests and diagnostics). */
|
||||
columns(): string[] {
|
||||
return (this.#db.prepare("PRAGMA table_info(items)").all() as { name: string }[]).map((c) => c.name);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
||||
Reference in New Issue
Block a user