feat(web): the plan template keeps a version history, read a page at a time and restored as a revision

Merged
DA
7d ago
+5964-17

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
actordeveloper, or earlier for the template the account held when its history began
documentthe template's whole text after the revision; null = the built-in default (a reset)
changessections changed, summed over the saves the revision holds
started_at, atfirst and latest save
restored_fromthe 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 one seq.
  • The developer's writes within 10 minutes move the newest revision on. This is the same PLAN_REVISION.JOIN_WITHIN_MS join 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}/restore answers the same PlanTemplateAnswer as a PUT.

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 seq or 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


Open workspace in Conductor


Devin Review