feat(web): plans can be pinned and archived, and the plain list leaves archived plans out
Merged
DA
8d ago+355+355+012345678901234567890123456789-32-32-01234567890123456789
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_atandarchived_at(timestamptz) toplan. It is hand-written perapps/web/server/README.md"The data layer", folded intoplan-schema.ts, anddrizzle-schema.test.tschecks 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 takespinnedandarchivedas booleans, besidenameandrepository(the #1856 rename endpoint). Like the rest it is owner-scoped: another account's plan answers 404 and nothing is written.truestamps now. Asking again keeps the first stamp, so a plan pinned twice keeps its place.falseclears the stamp.- Neither is a save, so
updatedAtand 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
pinnedAtandarchivedAt, set only where they stand. GET /api/plansleaves archived plans out unless?archived=includeasks 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,
includeArchivedlists 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=includelists it; an unknown query value excludes it - non-boolean and empty PATCHes are refused
- another account's plan is 404
- PATCH answers stamped; the plain list excludes the archived plan and
- 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