Context
Today, closing a signup is a one-way door. The status machine in src/services/signups.ts allows draft → open (publish) and open → closed (close), but there's no closed → open transition. An organizer who accidentally closes a signup, or who closes one early but later wants to extend it, has no recovery — their only option is to archive it and start over.
The detail header now exposes Close while the signup is open (#19), but a closed signup shows no action at all besides Preview / Export. The status pill says closed and that's the end of it.
Proposed approach
Add a reopenSignup service + API + header button that mirrors the close path:
- Service (
src/services/signups.ts): new reopenSignup(db, actor, id) delegating to transitionStatus(db, actor, id, 'closed', 'open', 'signup.reopened'). Add signup.reopened as an activity event kind.
- API (
src/app/api/signups/[id]/reopen/route.ts): POST handler matching …/close/route.ts.
- Action (
src/app/app/(chrome)/signups/[id]/actions.ts): reopenAction(signupId) analogous to closeAction.
- UI (
src/components/signup/SignupHeader.tsx): when status === 'closed', render a Reopen button next to Preview/Export — same brand-fill styling as Publish/Close, mutually exclusive with both.
Status machine after this change:
draft → open (publish)
open ↔ closed (close / reopen)
- any
→ archived (terminal — still one-way, intentionally)
archived stays terminal because it implies the organizer is done; reopening from archive is out of scope.
Open questions
- Reminder jobs on reopen: closing a signup doesn't currently cancel pending reminders (worth confirming) — does reopen need to re-schedule anything, or does the existing
singletonKey: commitmentId job model handle it?
- Public page caching:
/s/[slug] renders different copy for closed vs open — confirm the existing layout revalidation in revalidateSignup is sufficient (it should be, since close already relies on it).
- Confirmation prompt: close currently has no confirm step. Reopen probably doesn't need one either, but worth a thought — accidental reopen is much less destructive than accidental close.
Tests
- Unit: extend the status-transition tests in
src/services/*.test.ts to cover the new edge.
- DB: add a
closed → open → closed round-trip in src/services/*.db.test.ts to confirm activity log entries land correctly.
Context
Today, closing a signup is a one-way door. The status machine in
src/services/signups.tsallowsdraft → open(publish) andopen → closed(close), but there's noclosed → opentransition. An organizer who accidentally closes a signup, or who closes one early but later wants to extend it, has no recovery — their only option is to archive it and start over.The detail header now exposes Close while the signup is
open(#19), but aclosedsignup shows no action at all besides Preview / Export. The status pill saysclosedand that's the end of it.Proposed approach
Add a
reopenSignupservice + API + header button that mirrors the close path:src/services/signups.ts): newreopenSignup(db, actor, id)delegating totransitionStatus(db, actor, id, 'closed', 'open', 'signup.reopened'). Addsignup.reopenedas an activity event kind.src/app/api/signups/[id]/reopen/route.ts): POST handler matching…/close/route.ts.src/app/app/(chrome)/signups/[id]/actions.ts):reopenAction(signupId)analogous tocloseAction.src/components/signup/SignupHeader.tsx): whenstatus === 'closed', render a Reopen button next to Preview/Export — same brand-fill styling as Publish/Close, mutually exclusive with both.Status machine after this change:
draft → open(publish)open ↔ closed(close / reopen)→ archived(terminal — still one-way, intentionally)archivedstays terminal because it implies the organizer is done; reopening from archive is out of scope.Open questions
singletonKey: commitmentIdjob model handle it?/s/[slug]renders different copy forclosedvsopen— confirm the existing layout revalidation inrevalidateSignupis sufficient (it should be, since close already relies on it).Tests
src/services/*.test.tsto cover the new edge.closed → open → closedround-trip insrc/services/*.db.test.tsto confirm activity log entries land correctly.