From 3e1234112de0dae5fffc7a4d90ec7794eb5c94e0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 15:48:38 +0200 Subject: [PATCH] fix(demo): save the first baseline after a reopen that seeded too The demo seed runs in the schema bootstrap, at boot and again after every reopen. Since the baseline save moved out of the seed into DemoResetJob, only the boot read the seed's mark. A restore onto a demo instance with no baseline, whose archive held no admin trips, seeded the example trips during the reopen and then saved nothing, so the hourly reset kept logging "No baseline found"; the mark also stayed set for the rest of the process. Before the move, that path saved the baseline from inside the seed. DatabaseLifecycle now runs listeners after a reopen's schema bootstrap, and DemoResetJob registers one in demo mode that does the same check as at boot: save through the backup port when the seed just ran and no baseline exists. Each check clears the mark. A failed save is logged, never turned into a failed reopen. AdminModule imports DatabaseLifecycleModule from the shared database kernel for it. --- server/CLAUDE.md | 2 +- server/src/demo/demo-reset.ts | 9 ++- server/src/demo/demo-seed.ts | 4 +- server/src/nest/admin/admin.module.ts | 6 +- server/src/nest/admin/demo-reset.job.ts | 31 +++++++--- .../database/database-lifecycle.service.ts | 17 ++++++ server/tests/unit/nest/admin-jobs.test.ts | 60 ++++++++++++++++++- .../database-lifecycle.service.test.ts | 36 +++++++++++ wiki/Demo-Mode.md | 2 +- 9 files changed, 149 insertions(+), 18 deletions(-) diff --git a/server/CLAUDE.md b/server/CLAUDE.md index dc5a10e56..b342b42f0 100644 --- a/server/CLAUDE.md +++ b/server/CLAUDE.md @@ -42,7 +42,7 @@ Nest owns everything. Every domain is a DI module under `src/nest//` (`c - **Scheduling**: `src/nest/scheduling/` (`CronRegistrarService`) is the one way to schedule a cron. Jobs are `*.job.ts` providers in their owning domain. The registrar refuses to schedule under `NODE_ENV=test` (an integration test pins it). **Never use a `@Cron` decorator** — it bypasses that gate. - **Plugins** (`src/nest/plugins/`): the child-process runtime (`supervisor/`, `runtime/plugin-host-entry.ts`, `protocol/envelope.ts`) is **intentionally not Nest** — it is the security boundary. `host/rpc-host.ts` registers an RPC method at spawn only if the plugin holds the unlocking permission (registration = authorization). Install-time gates live in `install/`; the manifest's `trek` version range fails closed, and `TREK_PLUGINS_IGNORE_TREK_RANGE` downgrades that to a warning. The protocol tables in `protocol/envelope.ts` (+ `src/plugin-event-sink.ts`) are **generated** into `plugin-sdk/` and `shared/` — run `gen:plugin-facts` after touching them and commit the output. `runtime/egress-policy.ts` is a hand-kept mirror guarded by a parity test in `plugin-sdk/`; change both sides together. - **Storage**: `src/nest/storage/` is the upload driver abstraction (local / S3 / mirror behind one interface, with an admin surface and a usage job). Domain services never touch the filesystem or the S3 SDK directly. -- **DB** (`src/db/`): `better-sqlite3`, WAL, FK on. `database.ts` only holds the connection and applies pragmas. It does not build a schema, and importing it opens nothing (the first reader does). The connection's lifecycle belongs to the `DatabaseLifecycle` provider (`nest/database/database-lifecycle.service.ts`): `buildApp()` opens it through `open()` (which binds the ORM to later swaps and runs the schema bootstrap below), the shutdown closes it through `close()`, and a restore or the demo reset swaps the file only through the database backup port, `DATABASE_BACKUP` (`nest/database/database-backup.interface.ts`; the SQLite implementation is `nest/backup/sqlite-database-backup.ts`: `VACUUM INTO` snapshot, close, atomic file swap, `reopen()`). The port works on `DatabaseLifecycle.file`, the one resolved path (`TREK_DB_FILE` or `data/travel.db`), and a snapshot that fails fails the backup rather than archiving the live file. `MaintenanceRepository` is provided only by `MaintenanceModule`'s factory and injected everywhere else; no consumer constructs it. MikroORM's migrations (`db/migrations/`) are the only schema source, **including for tests**: the hand-kept `schema.ts`/`migrations.ts`/`seeds.ts` builders and the parity test that guarded them are gone. Seeding is the MikroORM seeders in `db/seeders/`, applied by `db/orm.ts` from `buildApp()` before anything reads. An install the retired positional runner migrated (a `schema_version` row, no `mikro_orm_migrations` rows) is baselined by `db/legacy-baseline.ts`: the migrations whose `Legacy migration step N` docstring is ≤ its version are recorded as executed (never replayed) in the same transaction as the migrator run, while the baseline migration itself still runs first, as the legacy runner's `createTables()` did on every boot; anything it cannot map refuses the boot. Before a boot applies pending migrations to a file database that holds anything, `db/pre-migrate-snapshot.ts` writes a `VACUUM INTO` copy next to it (`pre-migrate--