feat(web): the plan template keeps a version history, read a page at a time and restored as a revision
What
PR 1 of 4 for "Settings › Plan template looks and works exactly like a plan, with version history". This is the service side: the template's revisions, written on save and read and restored like a plan's.
Storage. A new table, account_plan_template_revision (packages/database/src/schemas/plan-template-revision.ts), keyed (user_id, seq), cascading with the user:
| column | |
|---|---|
actor | developer, or earlier for the template the account held when its history began |
document | the template's whole text after the revision; null = the built-in default (a reset) |
changes | sections changed, summed over the saves the revision holds |
started_at, at | first and latest save |
restored_from | the revision a restore brought back |
The migration is additive and drizzle-generated (20261013035159_account_plan_template_revision.sql). It only creates a table that no released build reads, so it is a pure expand step with no contract to follow.
Writes (account-store.ts's writePlanTemplate, models/plan-template-revisions.ts):
- Each write runs in one transaction. It opens the account's preference row if it has none, then locks it (
FOR UPDATE), writes the template, and records the revision, so two writes never take oneseq. - The developer's writes within 10 minutes move the newest revision on. This is the same
PLAN_REVISION.JOIN_WITHIN_MSjoin that plan revisions use. - A reset (
null) and a restore each write a revision of their own. Neither joins anything, and nothing joins them. - The first write with no history first records what stood (the account's own template, or the default) as seq 1 under
earlier, so the first edit has something to differ from. - A write that changes nothing records nothing.
- Restoring a default revision writes
null, so the account goes back to the default (custom: false) rather than keeping a copy of it.
API (server/account-app.ts, three route files under routes/account/plan-template/, rewrites regenerated):
GET /api/account/plan-template/revisions?before=returns a page, newest first, in the plan history's page shape.GET /api/account/plan-template/revisions/{seq}returns{ revision: { seq, document, previous } }. The default is drawn as its words.POST /api/account/plan-template/revisions/{seq}/restoreanswers the samePlanTemplateAnsweras aPUT.
The wire schemas live in @sidecar/hosted/plan-document-template. A template line is a PlanRevisionLine plus an optional default: true, so the desktop's existing version-history pane can draw it. JSON Schema goldens are recorded.
Tests
apps/web/tests/account-plan-template.test.ts, on PGlite, through the routes:
- the first write keeps the default as
earlier, then the write, each read beside the one before it - a template from before history is kept as itself
- writes within 10 minutes are one revision, and one after that starts another
- an unchanged write keeps no revision
- a reset is its own default revision that the next write does not join
- a restore is a revision naming the one it restored, and nothing joins it
- restoring the default goes back to
custom: false - the history pages at 50
- another account reads none of the history and restores none of it
- a bad
seqor the wrong method is refused
Each of these fails without the change. These tests sit beside plan-revisions.test.ts in apps/web/tests, as the plan history's do, because packages/database has no Effect test harness.
./scripts/check.sh exits 0.
Not in this PR
- PR 2: the desktop plumbing (hosted client, gateway, host, main acts).
- PR 3: the template page drawn by the plan's own document.
- PR 4: the template's version history pane.
This PR changes nothing in the Mac app. CI could not verify the Mac, and ./scripts/verify.sh was not run (LUKE-159 is the manual Mac pass).
🤖 Generated with Claude Code