Schema (migration 0005): dispatches gains a nullable pulse_id FK to
pulses(id) ON DELETE SET NULL. Partial index on the populated rows.
The pulses + votes tables themselves are unchanged — vote uniqueness,
status derivation, and the existing tests still hold; only the entity
relationship changes.
db.ts:
- Dispatch type gains pulse_id. New DispatchWithPoll = DispatchWithAuthor
+ a hydrated poll (pulse + counts + viewer's vote).
- createDispatch accepts an optional poll input — if provided, creates the
pulse first in the same transaction and stamps dispatches.pulse_id.
- updateDispatch grows two new arguments: poll (input or null) and a
pollExplicit flag. The flag distinguishes "leave the existing poll
alone" (undefined) from "the admin actively chose to detach / replace
it" (true). The detach path nulls pulse_id; the replace path mutates
the existing pulse in place via updatePulse so vote history survives.
- publishDispatch / archiveDispatch are now wrappers that also publishPulse
/ closePulse on the attached pulse. Dispatch state drives poll state.
- getDispatchWithPoll(dispatchId, viewerId) — single call for the page
renderers.
Admin:
- The Pulses tab is removed from the admin tab nav. The route + POST
handlers stay in place so existing draft pulses aren't orphaned, but
the entity is no longer a place admins go to think.
- DispatchesTab form gains a poll fieldset: question + 4 option inputs
(first two required if any are filled) + opens_at + closes_at. A
hidden poll_explicit flag tells the server the form intentionally
asserted the poll state (so leaving the fields blank during edit
detaches rather than no-ops). On edit, fields prefill from the
attached pulse if present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>