feat(web): plans can be pinned and archived, and the plain list leaves archived plans out

Merged
DA
8d ago
+355-32

The service half of pinning and archiving plans. The desktop half (sidebar sections, the plan menu, the archived banner, Permanently delete) is stacked on this PR.

What changes

  • Migration 0071 adds nullable pinned_at and archived_at (timestamptz) to plan. It is hand-written per apps/web/server/README.md "The data layer", folded into plan-schema.ts, and drizzle-schema.test.ts checks it both ways. Both columns are nullable and the running deployment never selects them, so the migration is safe ahead of the promote.
  • PATCH /api/plans/{id} now takes pinned and archived as booleans, beside name and repository (the #1856 rename endpoint). Like the rest it is owner-scoped: another account's plan answers 404 and nothing is written.
    • true stamps now. Asking again keeps the first stamp, so a plan pinned twice keeps its place.
    • false clears the stamp.
    • Neither is a save, so updatedAt and the list's order stand.
    • Pin and archive are independent: a pinned plan that is archived and later unarchived comes back pinned.
  • The wire: the plan summary (and so the plan answer) carries optional pinnedAt and archivedAt, set only where they stand.
  • GET /api/plans leaves archived plans out unless ?archived=include asks for them (PLAN_LIST_QUERY). A desktop built before archiving never asks, so it sees none of the archived plans rather than all of them, unflagged.

Old desktops

Every shipped build, v0.7.1 through v0.11.0, reads the list and plan answers through HttpClientResponse.schemaBodyJson, which ignores unknown keys. I checked v0.7.1:packages/hosted/src/account-call.ts. The new keys are optional, and nothing they required (openedAt, repository) changes. The #1835 lesson, strict decoding of a missing key, does not apply here.

Tests

  • Store, on PGlite (hosted-plans.test.ts):
    • pin, repin (keeps its stamp), and unpin, with the list's order unchanged
    • archive leaves the default list, includeArchived lists it flagged, and unarchive brings it back with its pin
    • owner scoping for both
  • Routes (plans-app.test.ts):
    • PATCH answers stamped; the plain list excludes the archived plan and ?archived=include lists it; an unknown query value excludes it
    • non-boolean and empty PATCHes are refused
    • another account's plan is 404
  • The JSON Schema goldens move only by the two optional summary keys and the two optional update keys, re-recorded with LUKE_UPDATE_FIXTURES=1.

./scripts/check.sh passes locally. This PR has no Mac or UI change.

šŸ¤– Generated with Claude Code


Devin Review