Commit graph

16 commits

Author SHA1 Message Date
a9e8a57642 feat(roadmap): feature bullets, live items, larger scroll-locked route
- Add per-item feature bullets (features column, migration 0009, db helpers,
  admin field) rendered as plus-icon lists on desktop + mobile.
- Reseed with the live roadmap items; status labels renamed
  (In dev / Planning) and "— Alpha" suffix dropped from titles.
- Enlarge the route and lock the roadmap page to sideways-only scroll so the
  timeline stays on screen; full-bleed edge-to-edge width; nudge the header
  down toward the page centre.
- Small-caps stage suffix helper (splitStageSuffix) in format.ts.
2026-06-18 16:04:56 +02:00
096c9bc297 feat(auth): self-service password change + admin password reset
- /account gains a Change password form (verify current, 8+ char new,
  confirm match) backed by updateUserPassword + verifyPassword/hashPassword.
- Admin users resource gains a "Reset password" action that generates a
  fresh temp password, sets it immediately, and reveals it once in the panel
  (new temp-password action-result, reusing the copy-box UI) for the admin
  to send to the user.
- Backstage top-left logo now links to the portal (main menu).

Temp passwords are generated + hashed at request time; never stored in git
or logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:42:45 +02:00
4c4df45f0c feat(admin): editable member email + Danish slug folding
Lets a fenja admin edit a member's email in the People resource (the
field was read-only). Email is required, format- and uniqueness-checked,
and normalised to lowercase on save; collisions surface as a form error
via the new updateUserEmail() helper.

Also folds ø/æ/å in slugifyName so Danish names produce clean member
slugs (soren-friis, not s-ren-friis) — NFKD leaves those letters intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:44:28 +02:00
59842432bd feat(roadmap): add planned status
Adds a fifth roadmap status, `planned`, for items that are committed and
scheduled but not yet started — sitting between `in_beta` and `exploring`
in the progression. Rendered with the design system's indigo pigment
(#5a6d83) on the route, carousel, legend, and admin pill.

Migration 0008 widens the status CHECK constraint via a table rebuild
(SQLite can't alter it in place), preserving rows and attributions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 11:30:06 +02:00
4aaf0957dd fix: nine test-note follow-ups
1. Markdown preview in the admin edit panel now re-renders from the
   textarea's current value on every toggle (dynamic-imports marked on
   the client). Previously the panel showed the server-rendered seed
   value forever, so new dispatches always previewed empty.

2. Pulse sub-form drops the opens_at field (opens on dispatch publish
   automatically) and changes closes_at to a date input — the chosen
   day is treated as end-of-day in the DB.

3. /dispatches/[slug] reading width widened 50% (720 → 1080px).

4. Roadmap display_order cascades on insert / update / delete:
   inserting at N bumps N..end up by 1, deleting N pulls N+1..end
   down by 1, moving from A to B shifts the intermediate range by 1
   in the appropriate direction. Order stays dense — no gaps, no
   collisions. All three transitions run in a transaction.

5. /roadmap always anchors at scrollLeft=0 on mount so the first
   milestone aligns with the content-column left edge. Previously
   the page jumped to the last-shipping milestone, which felt random
   once items past the viewport landed.

6. Events admin list shows the actual date (fmtDateTime) instead of
   "in 3 days" — easier to scan when planning across months.

7. duration_label is auto-computed from starts_at + ends_at on save
   (minutes < 90, hours < 4, "Half day", "Full day", "N days").
   The manual field is gone from the admin form; the column on the
   member-facing event pages keeps reading the stored value as before.

8. Pulse hero still skips office hours per the existing logic — no
   change. Confirmed via the test note's clarification.

9. Pulse "also coming up" strip relabeled to Previous + Upcoming.
   Previous = most recent past non-office-hours event. Upcoming =
   next non-office-hours event after the hero. Each card now carries
   a small terracotta eyebrow with the label.

Typecheck clean, build clean, 147/147 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:46:06 +02:00
e9a986d484 feat(admin): council-group resources (users, invitations, join requests)
Three more resources land. /admin/users replaces the old participants
tab, /admin/invitations replaces the old invites tab, /admin/join_requests
replaces the read-only join queue.

- src/admin/resources/users.ts ("People"): single resource for all users,
  filter chips swap visible columns (council shows member_number +
  focus_tags; pilots/team show role + last_seen_at). Form fields are
  conditional — title / pull_quote / focus_tags / cab_joined_date /
  member_number render only when role === cab. No ops.create (users
  come via invites); deactivateUser is the delete handler.
- src/admin/resources/invitations.ts: form-for-create, summary-for-view.
  Create generates a token via generateInviteToken(), stores its hash,
  surfaces the magic link as a one-shot ?invite_url= block in the panel.
  Revoke is an action (sets expires_at = now); the row stays for audit.
- src/admin/resources/join-requests.ts: form: null, review-mode panel
  with the user's summary + approve_as_cab / decline actions.

Plumbing to support the above:
- src/admin/resource-types.ts: new Resource.summary callback (read-only
  field pairs for review panels); OpContext.result lets ops surface
  ActionResults (e.g. invite-link).
- src/admin/components/ResourceEditPanel.astro: review mode when an
  existing item is shown and resource.summary is defined; renders the
  ?invite_url= block above the summary with a copy-to-clipboard button.
- src/admin/components/ResourceListView.astro: "+ New" suppressed when
  ops.create is undefined.
- src/pages/admin/[resource].astro: captures ctx.result and action
  handler return values, propagates them via &invite_url=...; routes to
  the list view (not the row) when an action removes the item.
- src/lib/db.ts: adds getJoinRequestById, deleteJoinRequest,
  getInviteById.

Deviation from the original delta: no approve_as_pilot action and no
invite-link result on join-request approval. The existing
join_requests schema only stores user_id — requests come from
already-authenticated pilots asking for a CAB upgrade, not from
strangers needing an invite. The schema change for stranger sign-ups
is left for a future follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:32:26 +02:00
d17d9b93a7 feat(db): roadmap_items.metadata_text + admin field
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>
2026-05-12 11:39:18 +02:00
5ddaad3da3 feat(vote): show percentages after voting + allow change-of-mind
The pulse vote widget on /pulse and on /dispatches/[slug] now behaves
the way the v5 spec asks:

- Eyebrow shortens: 'This week's pulse' → 'This week's'.
- Status line copy changes shape depending on whether the viewer has
  voted yet. Pre-vote: 'Vote to see how the council weighs in · Closes
  TUESDAY' — sets expectations that the percentages reveal after a
  vote. Post-vote: '2 of 7 voted · Closes TUESDAY · Click to change' —
  tells the viewer they can change their mind.
- Each option now renders a right-aligned tabular-nums percentage badge,
  but only after the viewer has voted. Pre-vote there's no percentage
  on screen at all — voting is a commitment, not a peek.
- Options stay clickable after voting (no `disabled`). Re-clicking a
  different option changes the vote.

DB: new helper castOrChangeVote(pulseId, userId, optionIndex) does an
UPSERT — INSERT on first vote, UPDATE option_index + voted_at on
subsequent. Returns true if this was the brand-new vote, so the caller
can write the 'voted' activity row exactly once and not double-count
changes-of-mind in the feed. castVote(...) (INSERT OR IGNORE) stays in
db.ts for callers that explicitly want first-vote-wins semantics.

Status-class rename: .locked → .closed on both pulse-option and
inline-poll-option. The class now reflects what it actually represents
(the pulse is closed) rather than the false invariant 'the user has
voted and can't change'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:20:16 +02:00
9ae8422527 feat(db): roadmap_items gains 'considering' + 'in_beta' rename, --on-ink tokens
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>
2026-05-12 10:46:39 +02:00
867661ee3d feat(polls): polls attach to dispatches — standalone Pulses entity retired
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>
2026-05-12 10:14:50 +02:00
368ce3ac8c feat(db): dispatches + member-number allocation + focus-tags parser
db.ts:
  - User type gains pull_quote, member_number, focus_tags; all SELECT lists
    updated. getAllCabMembers (member_number asc) and countCabMembers (used
    by /pulse denominator) added.
  - createUser allocates a member_number in-transaction when role=cab.
  - updateUserRole returns { allocated: number | null } so admin can surface
    the assignment; allocation is one-way: pilot→cab→pilot→cab keeps the
    original number.
  - allocateMemberNumber: MAX(member_number)+1, idempotent, never reuses.
  - updateUserAdminFields: title / pull_quote / focus_tags (parsed array).
  - createEvent / updateEvent extended for audience, duration_label,
    action_label, notes_url.
  - Dispatch CRUD: create / update / publish (stamps published_at) /
    archive / delete. getDispatchById, getLatestPublishedDispatches,
    getAllDispatchesForAdmin, getAdjacentDispatches (prev/next in published
    order).
  - getEventAttendees(slug, status) backs the upcoming-event avatar pile.

format.ts:
  - AVATAR_PIGMENTS (terracotta/copper/walnut/indigo/heather) + pigmentForId
    (id % palette, deterministic).
  - parseFocusTags: trim, strip ASCII control chars (\x00-\x1F\x7F),
    collapse internal whitespace, dedupe, cap 3 × 24.
  - readFocusTags (safe JSON.parse for display).
  - dispatchSlug / parseDispatchSlug: {id}-{kebab(title)}; renames don't
    break links because the id leads.
  - dispatchKindLabel, stripMarkdownLight, dispatchExcerptParas (two-paragraph
    excerpt with sentence-boundary cut).

Tests: member-number allocation (idempotent, never reuses, allocates on
role transition) and focus_tags parser (control chars, whitespace collapse,
dedupe, cap). 24/24 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:55:35 +02:00
1735487ab9 feat(db): pulse/vote/roadmap/event/activity helpers + derivePulseStatus
Adds typed query functions for the council-portal entities. Pulse status is
stored AND derived: draft and closed are sticky, open auto-decays to closed
once now ≥ closes_at. Draft → open is an explicit admin Publish action, not
date-driven, so admins can stage a pulse without surprise auto-publishing.

Roadmap updateRoadmapItem stamps shipped_at the first time status transitions
to 'shipping' and never resets it; returns { shippedNow } so callers can fire
the roadmap_shipped activity row exactly once.

Event RSVPs reuse the existing attendance table with kind='event'; no
parallel Rsvp table. setEventRsvp upserts on UNIQUE(user_id, meeting_slug).

getLitQuarters drives the CouncilMark dot pattern from
roadmap_attributions × shipped_at — admin-curated, not derived from votes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:41:49 +02:00
56992ed4ca feat(db): configurable path, user field extensions, slug generation
- BIFROST_DB_PATH env var overrides the default bifrost.db path; lets
  vitest open ':memory:' per suite without touching prod data.
- Extend User/UserPublic with title, cab_joined_date, slug.
- Update SELECT lists for getUserPublicById and getAllUsersPublic.
- Add getUserBySlug for /members/:slug routes.
- Add slugifyName + generateUniqueSlug; createUser now auto-slugs from name
  and stamps cab_joined_date for cab-role users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:40:24 +02:00
Jonathan
4bed3a5fe0 feat: join_requests table and join CTA flow 2026-04-19 20:29:09 +02:00
Jonathan
9de5602d2d feat: authentication and invite flow 2026-04-18 22:45:25 +02:00
Jonathan
0dc2dbd849 feat: database schema, migrations, and seed data 2026-04-18 22:43:16 +02:00