New <RoadmapInMotion> component renders between the dispatch banner
and the route's section header. Pulls the most-recent shipping item
(same selection rule the .rr-current marker uses) and prints the
first sentence of its description as a 18px serif italic line
preceded by an 'IN MOTION RIGHT NOW' tracked eyebrow.
A member who only spends 5 seconds on /roadmap now still walks away
with a sentence about what just shipped — no scroll, no hover.
firstSentenceOf() is the obvious regex against the first
[.!?](?=\s|$). Bails to the 200-char slice if no sentence boundary
fits (covers 'Dr.' / 'e.g.' confusables). Returns '' on null. The
strip hides itself entirely when there's no shipping item, or when
the shipping item has no description text.
Page subtitle: 'Hover any milestone for the full story.' →
'Tap or hover any milestone for the full story.' — touch devices
don't have hover, and the kind of detail that says we're paying
attention.
Admin description-field gains a helper note: 'For shipping items:
the first sentence appears on /roadmap as the "In motion right now"
line. Make it count.' Nudges good first-sentence writing without
adding a new field to maintain.
Banner margin under the dispatch banner reduces 56 → 40px because
the in-motion strip carries its own 36px bottom margin to the route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 0007 (spec said 0006 but 0006 was already roadmap_considering)
adds a single nullable metadata_text column to roadmap_items — a short
admin-set narrative cue shown on hover in the route cards. ~60 chars
suggested in admin helper text. Hidden in the UI when NULL.
db.ts: RoadmapItem type gains the field. createRoadmapItem + updateRoadmapItem
accept an optional metadata_text parameter. moveRoadmapItem passes it through
when swapping display_order between siblings so the helper preserves it.
Admin: /admin?tab=roadmap edit form gets a new 'Hover note' input under
the description, with the helper text and a 120-char hard cap. Empty
string saves as NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migration 0006 (the spec said 0005 but that number was already taken by
polls_on_dispatches from the previous session): rebuilds the
roadmap_items CHECK to ('shipping','in_beta','exploring','considering')
and renames any existing 'beta' rows to 'in_beta' in-place. FKs from
roadmap_attributions are preserved across the DROP/RENAME by toggling
PRAGMA foreign_keys off around the rebuild — attribution count unchanged
after migrate (verified 4 rows survive on the demo DB).
Tokens (src/styles/tokens.css): adds --on-ink, --on-ink-body,
--on-ink-muted, --ink-divider. The bleached #fffcf7 cream replaces the
warm #e8e0d0 --ink-text wherever it sits on indigo. Legacy --ink-text /
--ink-muted stay in tokens.css for now — if any later commit references
them they remain defined; the migration of existing call sites is
covered here.
Migrated to the new tokens in this pass:
- src/components/MembershipCard.astro (members/:slug card)
- src/pages/events.astro (hero invitation card)
Both render with cleaner whites on indigo as a side effect.
Code updates for the new status enum:
- db.ts: RoadmapStatus = shipping | in_beta | exploring | considering
- admin/RoadmapTab.astro: Status select gains Considering + In beta;
grouped section iteration covers all four
- admin/index.astro: validation list updated
- scripts/seed-roadmap.js: 'In progress' markdown bucket → 'in_beta'
- pulse.astro: roadmapStatusDot + roadmapStatusBlurb temporarily widened
(full rewrite of that section lands in step 7)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
New Dispatches tab — full CRUD matching the existing pattern (?tab=
querystring, plain HTML POST, hidden action field, redirect-with-?msg=).
Author select is restricted to Fenja-role users (defaults to the current
admin). Create form has a status toggle (draft / publish on save).
publish_dispatch stamps published_at via the existing helper; archive
preserves it. Body is a monospace textarea so admins can see markdown
without proportional kerning confusion.
Participants tab gains a per-row Edit link. When ?tab=participants&edit=ID
is set, the table is replaced by <UserEditTab>: title input, comma-
separated focus_tags input (parsed server-side via parseFocusTags), a
pull_quote textarea with a 200-char live counter, and a read-only
member_number display (set on role transition to cab). The inline role
dropdown + deactivate stay on the table.
EventsTab — adds audience, duration_label, action_label, notes_url
inputs. Kind <select> now labels office_hours as 'Studio hours' and
exposes working_session as the new fifth option.
Admin action-link / action-cell styles were missing on admin/index.astro
(they were defined only inside per-tab components); added to the page
stylesheet so the new Participants Edit link inherits the same look.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four new tabs added to /admin, matching the existing pattern exactly: ?tab=
querystring, plain HTML POST with hidden action field, redirect-with-?msg=
success path, existing .tabs / .section / .data-table / .form-grid / .input
/ .select / .btn-primary / .danger-btn classes — no new form library, no JS
beyond `confirm()`.
Pulses tab — create / edit / publish / close / delete + a results view
(?view=ID) with per-option vote counts and bar charts. Publish writes the
'pulse_opened' activity row and calls notifyPulseOpened() so members can
be notified the same way once the integration lands.
Roadmap tab — full CRUD + multi-select attribution (checkbox grid of
council + pilot users) + up/down arrow reorder within each status column
(JS-free, swaps display_order with neighbour). Status transition to
'shipping' stamps shipped_at exactly once and writes 'roadmap_shipped'
activity.
Events tab — full CRUD + an RSVP summary view (?view=ID) showing going /
interested / declined counts. Slug is required on create and readonly on
edit (it's the URL handle).
Activity tab — read-only debug table of the last 200 activity rows. Per
your call: shipping with the rest, not optional. Saves hours of "why
isn't the ticker showing X" later.
Tab views extracted to src/components/admin/{Pulses,Roadmap,Events,Activity}Tab.astro
to keep admin/index.astro navigable; the POST handlers and data loading
stay in index.astro as the single dispatch point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>