Commit graph

69 commits

Author SHA1 Message Date
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
cafbcf8b74 style: site back to 72rem + section-link is black/underlined/larger
- --content-max: 83rem → 72rem (back to the original width — the wider
  canvas tested less well in practice; sections felt under-furnished).
- .section-link: italic serif retained, but now black (--on-surface)
  instead of terracotta, underlined with a fine 0.5px stroke and 4px
  text-underline-offset, font-size bumped from text-body-md to
  text-title-lg (1.125rem). Hover slightly thickens the underline rather
  than swapping colour, so the link still reads as a link at rest.
- .section-link--ink modifier swaps to cream on the dark events card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:12:01 +02:00
66c3f6492f style: drop eyebrows + body italics across editorial pages
Applies the v3 rules globally: italics reserved for the Bifrost wordmark
and section-links; uppercase tracked eyebrows removed from page heads
and content sections.

Pages updated:
- /members: drops the 'MEMBERS' eyebrow + italic on h1 + italic on member
  name + italic on pull-quote.
- /events: drops the head eyebrow + 'Next up · Members only' /
  'Invitation by hand' hero eyebrows + 'Also coming up' / 'Past
  gatherings' sub-section eyebrows + italics on hero day, hero title,
  also-row titles, past-card titles. The past-gatherings header-row
  'View all →' link migrates to the bottom of the section as a
  section-link.
- /events/past: drops the eyebrow + italic h1; back-link uses
  .section-link.
- /dispatches/: drops the 'DISPATCHES' eyebrow + italic h1 + dispatch-row
  title italic + date column italic.
- /dispatches/[slug]: drops italic on the article title + h2/h3 inside
  rendered markdown + blockquote italic + adjacent prev/next title
  italic. Back-link migrates to .section-link.
- /roadmap: drops the 'ROADMAP' eyebrow and the .lead class on the
  subtitle.

Orphaned eyebrow class rules left in place; harmless and the next
visual pass can sweep them with the rest of the unused CSS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:52:29 +02:00
637055a73e feat(pulse): events box lighter + bundled coming-up, unboxed Fenja, horizontal roadmap, bigger council cards
Events card (--ink):
- 'Next up · Members only' and 'Invitation by hand' eyebrows removed.
- All ink-card text uses cream tones (rgba(232,224,208,...) at 92/75/70/65%)
  instead of the warm tan --ink-muted; the previous low-contrast labels
  read 'dark' on the indigo and now read uniformly light.
- Italic font removed everywhere on the card (hero day number, hero title,
  coming-up titles, etc.) — italic is reserved for the Bifrost wordmark
  and section-links only.
- Past gatherings dropped from /pulse entirely; the listing lives on
  /events and /events/past.
- 'Also coming up' is now a grid of small bundled sub-cards inside the
  blue surface (auto-fit minmax 220px). Each card shows date + title +
  meta only — no RSVP action, no per-row submit form.
- 'See all events →' section-link replaces the old past-gatherings
  'View all →' as the sole bottom-of-block link to /events.

Latest from Fenja (unboxed):
- Card surface dropped. Article sits on the cream page background.
- Excerpt extended via new dispatchLongPreview(d, 520) helper —
  sentence-boundary cut at ~520 chars (was ~200). Title in serif regular,
  not italic.
- 'Read the full dispatch →' section-link at the bottom.

Roadmap (horizontal):
- Three roadmap items become a 3-column grid of small white cards instead
  of a vertical list. Each card has status dot + title + status blurb
  with consistent min-height.
- 'See the full roadmap →' section-link at the bottom.

Council members (larger cards):
- Was a flowing pill row, now an auto-fit grid (minmax 260px) of larger
  white cards. Each card has a 56px avatar + name + title + company,
  with generous padding for whitespace. Company name is the new field.
- 'See who our council is made up of →' section-link at the bottom.

General (eyebrows + italics): all uppercase tracked eyebrow labels gone
from /pulse — date label, 'Latest from Fenja', 'From the roadmap', 'The
council', etc. Italic body text removed throughout — greeting, titles,
member names, dispatch title, roadmap titles. The Bifrost wordmark in the
header and the .section-link utility class are the only remaining italics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:47:44 +02:00
ca3686de29 feat(layout): wider canvas + 'Project Bifrost' wordmark + section-link utility
- Width: --content-max 72rem → 83rem (15% wider, per the v3 follow-up
  spec). Every page that uses var(--content-max) gets the new bound.
- Wordmark in the top-left nav: Fenja logo · "Project Bifrost". 'Bifrost'
  is serif italic with a horizontal pigment-rainbow gradient
  (terracotta → ochre → copper → indigo → heather), background-clip:text.
  The bullet separator uses --on-surface-muted at 1rem.
- Global .section-link utility class: serif italic, terracotta, no
  underline, no all-caps. Modifier --ink for use on the dark events card.
  This becomes the only italic body text on the site (along with the
  Bifrost wordmark); everywhere else loses italics in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 09:44:59 +02:00
6b30593abb chore(seed): fake email domains + extend wipe chain
Swaps real-looking organisation domains in seed fixtures for the
virkN.dk placeholder pattern, so demos and screenshots can't be misread
as implying real-world relationships with the originals.

- mette@ssi.dkmette@virk1.dk
- lars@rigspolitiet.dklars@virk2.dk
- jonathan@fenja.aijonathan@studio.test (separate fake domain for the
  team account, kept distinct from the council virkN namespace)
- anna@kommune.dkanna@virk3.dk
- soren@energinet.dksoren@virk4.dk
- henriette@dnv.dkhenriette@virk5.dk

Organisation strings get the same treatment ('Virksomhed 1' …).

Also fixes two latent bugs surfaced while re-seeding:
- seed.js's INSERT didn't populate the slug column added in migration
  0003. After a re-seed the three base users had NULL slugs. Add a
  kebab-from-name fallback in the INSERT so slugs round-trip.
- seed.js's DELETE chain pre-dated the Phase 1/2 schema additions and
  failed FK constraints (pulses/dispatches/events/votes/activity/
  join_requests/roadmap_attributions). Extend the wipe order so all
  user-referencing tables clear before users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:12:47 +02:00
3240e0f052 feat(pulse): simplify home — events on top, merged roadmap+fenja, members strip
Restructures /pulse around three blocks instead of seven, per the
follow-up simplification spec.

Nav: Events and Members drop out of the top bar. Becomes
Pulse · Roadmap · [Admin]. Members and Events remain reachable via the
two new on-page links ('See who our council is made up of →' and
'View all →' under past gatherings).

/pulse render order:
1. Greeting (unchanged)
2. Events card (--ink). One blue card now holds all three sub-sections:
   - Hero NEXT UP / INVITATION BY HAND treatment for the soonest event,
     full date+title+desc+capacity+RSVP CTA. AvatarPile of confirmed.
   - 0.5px ink-muted divider, then ALSO COMING UP — compact list of other
     upcoming events with their action-label fallback. Less visual weight,
     same dark surface.
   - Divider, then PAST GATHERINGS — compact list with notes / no-notes
     indicator, plus a 'View all →' link to /events/past.
   - Empty state retains the visual weight of the card if nothing is up.
3. Combined Roadmap + Latest from Fenja (--surface-card). One white card,
   two stacked sub-sections separated by a 1px divider. Top is the single
   most recent published dispatch (was 'Latest from the studio', now
   labeled 'LATEST FROM FENJA'; 'All updates →' link to /dispatches). Bottom
   is the three most-recently-updated roadmap items + 'See the full roadmap →'.
4. Members strip (--surface-card). Every cab user as a pill (avatar + name
   + title) flowing horizontally. Header has the 'See who our council is
   made up of →' link to /members.

Removed from /pulse:
- This-week's-pulse voting block (deferred → todo.md, idea is to fold
  poll-shaped dispatches into the Latest from Fenja stream)
- MembershipCard (the COUNCIL · NNN identity card)
- RecentlyFromTheCouncil (deferred → todo.md)
- Bottom event-row with the two small dinner + studio hours cards (events
  moved to the top hero card, so these were duplicates)

POST handler is now RSVP-only — vote handling went with the pulse block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:12:38 +02:00
1518bfa3d3 docs: capture deferred home-page features in todo.md
Two things lifted off /pulse in the follow-up pass need somewhere to live
so they aren't forgotten:

- The 'Recently from the council' feed (RecentlyFromTheCouncil reading
  from the contributions table). Component and underlying table stay in
  place; only the /pulse embed is gone. Records the options for re-
  introducing it: inline in the Latest-from-Fenja card, a dedicated
  Voices section further down, or stop surfacing on the home page.

- 'This week's pulse' voting card. pulses/votes schema + admin Pulses tab
  stay. Records the merge-into-dispatches idea — fold poll-shaped
  dispatches into the Latest from Fenja stream rather than rebuilding a
  parallel block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:04:40 +02:00
ed2c272d3a chore: Studio hours rename + Phase 2 demo seed
Studio hours rename pass (one commit, grep-driven):
  - /pulse right-card eyebrow: 'Office hours' → 'Studio hours'
  - scripts/seed-demo.js: event title + description match the spec
    ('Studio hours with Jonathan' · '30-minute slots. Open agenda. Drop in
    when you've got something to talk through.')
  - No code-level enum changes — the kind value office_hours is preserved
    for back-compat; display labels switch wherever surfaced (admin form
    select, /pulse eyebrow, /events list and meta)

scripts/seed-demo.js — Phase 2 demo state. Destructive in scope (wipes the
data tables it owns then re-inserts), idempotent on re-run:
  - 4 cab members: Lars (existing) + Anna Kjær / Søren Vedel / Henriette
    Rask. cab_joined_date staggered 24/6/4/2 weeks ago so tenures vary.
    title, pull_quote, focus_tags populated per spec. member_number
    backfilled via the same SQL pattern as migration 0004 (deterministic).
  - 1 active pulse with 2 of 4 council members voted. Vote count on /pulse
    now reads '2 of 4 council members have weighed in.' — the line voted
    test was designed to lock down.
  - 4 roadmap items: Traceability layer (shipping, attributed to Lars),
    Document ingestion (beta, attributed to Anna + Søren), Contextual
    memory (exploring, attributed to Henriette), Agentic query mode
    (exploring, unattributed).
  - 3 contributions, most recent ('inline annotations' idea by Søren) has
    3 reactions — populates the RecentlyFromTheCouncil card.
  - 4 published dispatches at 2/5/9/12 days ago covering all four kinds
    (decision / behind_the_scenes / update / note). Real-ish prose so the
    excerpt cutter has actual sentence boundaries to find.
  - Events: hero dinner 5w out, Studio hours 2w out, a working_session 3w
    out (exercising the new kind), April roundtable 3w ago with a
    notes_url, March launch dinner 7.5w ago without notes (exercises both
    past-card thumb modes). Hero dinner has 1 confirmed RSVP (Lars) to
    drive the avatar pile at small scale.
  - Activity rows for the (now-hidden but still-written) feed so admin's
    Activity tab has something to display.

Smoke (curl as Lars): /pulse renders 'Good afternoon, Lars.' · COUNCIL · 001
· '2 of 4' · Latest from the studio · Recently from the council · Studio
hours. /members shows all four members with pull quotes + focus pills.
/events shows the dinner hero, 'Save your seat →', Studio hours +
working session in 'also coming up', April and March in 'past gatherings'.
/dispatches lists all four; /dispatches/{slug} renders body + adjacent
prev/next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:16:24 +02:00
fd3f433933 feat(admin): Dispatches tab + user-edit form + extended event form
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>
2026-05-11 16:10:20 +02:00
1bf1993040 feat(page): /dispatches index + /dispatches/[slug] detail
/dispatches: editorial header (DISPATCHES · 'Notes from the studio.' ·
'Decisions, half-built ideas, and things we've changed our mind about.')
+ a vertical list of every published dispatch. Each row is a single link
target with a three-column grid: 180px byline (avatar + author name + title
or role label) / 1fr body (serif italic title + kind pill, then a single-
paragraph excerpt) / 130px date column. 0.5px bottom borders, hover tint.

/dispatches/[slug]: 720px single-column read view. Header is kind pill +
publish date, serif italic title at 2rem, author byline with 32px avatar.
Body uses the existing renderMd() (marked) with serif italic h2s, copper
blockquotes, mono code blocks. Footer is a 0.5px divider then two adj-card
links (prev / next in published order) on opposite ends — the missing side
renders an empty grid slot so layout is preserved.

Canonical-slug redirect: if /dispatches/12-old-title is hit but the title
has since changed, the page issues a 302 to /dispatches/12-new-title. id
is the authority, kebab title is for readability.

format.ts: adds roleLabel (pilot/cab/fenja → 'Pilot' / 'Council' /
'Fenja team') for the byline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:07:13 +02:00
b0e6d7e18b feat(page): /events + /events/past + AvatarPile component
/events:
- Header: EVENTS · 'Where the council gathers.' · one-line subtitle
- Hero invitation card on --ink for the soonest non-office_hours event:
  NEXT UP · MEMBERS ONLY / INVITATION BY HAND eyebrow strip, two-column
  date/detail body separated by a 0.5px vertical line, foot strip with
  '{capacity} seats · {confirmed} confirmed' + AvatarPile of confirmed
  attendees and the RSVP CTA. The RSVP button toggles between cream-on-ink
  'Save your seat →' and outlined 'You're confirmed ✓ Change'. Empty-state
  card retains the visual weight when no upcoming non-office_hours event.
- ALSO COMING UP — every other upcoming event including office_hours.
  Three-column rows; the right column uses event.action_label or falls back
  to defaultActionLabel(kind). Studio hours surfaces with 'Book a slot →'.
- PAST GATHERINGS — two-column grid. Each card has a 56px thumb: photo_url
  if set, else a copper-tinted notes square when notes_url is present, else
  a deterministic two-pigment gradient block. View all → links to /events/past.

/events/past — same card component, full list of starts_at < now() events.
No boolean past flag column; filter is purely date-based.

AvatarPile (src/components/AvatarPile.astro) — reusable. Overlapping circle
slots with a 1.5px border in a caller-provided colour (defaults to surface,
the hero card overrides to --ink so circles read on dark). Stacks z-index
so leftmost is on top; +N overflow chip at the end.

format.ts: adds eventKindLabel (office_hours → 'Studio hours') and
defaultActionLabel per kind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:05:47 +02:00
58faeffbc2 feat(page): /members — editorial masthead
Header: MEMBERS eyebrow, serif italic 'The council.', subtitle pitched at
'an invited circle of operators' — no number, no seat count anywhere.

One row per cab user, ordered by member_number asc. Three-column grid
(60px / 1fr / 130px, 18px gap) with 0.5px bottom borders. Avatar uses the
deterministic id-pigment palette; the same id always yields the same
colour across the portal.

Body column: serif italic name; if the row belongs to the signed-in user,
a small uppercase tracked 'You' tag in terracotta. One metadata line —
title · organisation · Member since {month year} — title is dropped if
null. The pull-quote block is only rendered when pull_quote is non-null;
its 2px left border is the user's pigment at 40% opacity (50% on the
signed-in user's own row).

Right column: up to 3 focus-tag pills, vertical right-aligned, tinted in
the same pigment family (15% bg / 100% colour). Empty focus_tags renders
nothing — no placeholder pills.

Signed-in user's row gets a 4% pigment tint and rounded corners; no border
between rows in that case.

No filters, no search, no sort, no header count, no empty-seat row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:03:35 +02:00
243a456880 feat(pulse): nav restructure, white surfaces, membership card, dispatches
Nav (AppLayout.astro): brand stays left; everything else is a single
right-aligned flex group — Pulse · Roadmap · Members · Events · [Admin
if fenja] · 0.5px vertical divider · name · Sign out. The .nav-user
wrapper is gone; the name and logout button now belong to the same flex
flow as the link list.

/pulse:
- ActivityTicker render removed. The component file gets a one-line
  deprecation comment; the activity table and write hooks stay in place
  for later use.
- 'X others online now' chip strip removed — including all its supporting
  helpers and styles in the page.
- CouncilMark replaced with <MembershipCard> in the right column of the
  preview row. The roadmap preview is now a white --surface-card with
  0.5px border; pulse-card switches to the same white surface and
  --radius-lg. The .chosen pulse option uses --pigment-terracotta border
  and a 6% terracotta tint via color-mix.
- <DispatchesSection limit={4} /> and <RecentlyFromTheCouncil /> stacked
  below the preview row, in the position the online-now strip vacated.
- Vote-count denominator pulls from countCabMembers() and renders via
  voteCountSentence(votes, total) — a new helper covering 0/1/5+ cases.
- Event row: dark dinner card now uses --ink/--ink-text; light card uses
  --surface-card with 0.5px border.

Tests: 3 new cases for voteCountSentence (0/1/5). 36/36 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:02:46 +02:00
3e9fadcf79 feat(component): RecentlyFromTheCouncil — pulls the most recent contribution
White-card surface. Header row with secondary 'Recently from the council'
eyebrow and a terracotta 'All contributions →' link. Body is one card:
22px id-pigment avatar, full name, relative time, contribution-type pill
(idea→copper, inspiration→ochre, question→indigo), then the body rendered
as a serif italic pull-quote with a 40%-opacity terracotta left border,
clamped to 3 lines via -webkit-line-clamp with markdown stripped before
trimming. Footer shows the reaction count.

Hidden entirely when the contributions table has no visible rows — no
empty-state placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:58:29 +02:00
3b602a787b feat(component): DispatchesSection + reusable Avatar + tests
DispatchesSection (white-card, used on /pulse):
- Header row: LATEST FROM THE STUDIO + All dispatches → link
- Featured: 26px avatar byline (name · title · relative time + kind pill),
  serif italic title, two-paragraph excerpt (lead in body tone, trail in
  --secondary with ellipsis) cut on the nearest sentence boundary, then
  the terracotta uppercase 'Read the full dispatch →' link
- Earlier: 1px divider + EARLIER label + up to 3 rows with 22px avatar,
  serif italic title (single-line ellipsis), relative time
- Hidden entirely when zero published dispatches; divider + earlier list
  omitted when exactly one published dispatch exists

Avatar component (src/components/Avatar.astro) — pure presentational,
takes id + name + size, paints a deterministic-pigment circle with serif
italic initials. Reused by DispatchesSection now and by /members, /events,
and RecentlyFromTheCouncil in the next commits.

format.ts: adds dispatchKindPigment (decision→terracotta, update→indigo,
behind_the_scenes→ochre, note→heather) for pill backgrounds.

Tests: 9 cases covering create-as-draft vs published, publishDispatch
idempotency (re-publish preserves published_at), archive preserves
published_at, the published feed excludes drafts/archived, adjacent
prev/next, and slug round-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:57:52 +02:00
302caf8896 feat(component): MembershipCard — dark indigo identity card
Replaces the CouncilMark slot on /pulse. Dark --ink surface, --ink-text
foreground, --ink-muted for labels. Top row: 22px cream circle with the
N monogram, COUNCIL · NNN (zero-padded to 3) on the right; defensive
fallback COUNCIL · MEMBER when member_number is null. First and last name
on separate lines in serif italic, split on the last space so compound
surnames hang together. MEMBER SINCE / month-year block sits above the
focus-tag pill row. Pills render only when focus_tags is non-empty —
no placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:56:17 +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
865347f682 feat(db): migration 0004 — phase 2 schema + 4 new tokens
users: adds pull_quote, member_number (unique partial index, NULLs allowed),
focus_tags. title already exists from 0003 — not re-added. Backfills
member_number for existing cab users in COALESCE(cab_joined_date,
created_at) asc, tiebreak id asc. Lars gets #1.

events: rebuild required to widen the kind CHECK constraint (SQLite can't
ALTER it in place). Adds working_session as a new kind. Same rebuild adds
four new columns: audience, duration_label, action_label, notes_url. Data
preserved.

dispatches: new entity, status enum draft/published/archived, kind enum
decision/update/behind_the_scenes/note. published_at nullable until
publishPulse-equivalent stamps it. Indexes on (status, published_at) and
author_id.

Tokens (src/styles/tokens.css): adds --surface-card, --surface-card-border,
--ink, --ink-text, --ink-muted. Spec called these out as the only
additions; existing --radius-lg covers the spec's --border-radius-lg
reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:46:53 +02:00
7f6668f909 docs: Phase 0.5 audit — Phase 2 preflight
Confirmed before writing code:
  - latest migration: 0003_council_portal.sql
  - /pulse render order: greeting · ActivityTicker · pulse card · preview
    row (roadmap + CouncilMark) · room-row (online chip strip) · event row
  - CouncilMark exists and works (one lit dot for Lars in seed); not broken
  - ActivityTicker exists at src/components/ActivityTicker.astro and renders
    at pulse.astro:169
  - 'online now' strip lives at pulse.astro:266-290
  - vote-count denominator is already count(role='cab'); '1 of 1' on the
    smoke run was seed shape, not a bug — Phase 2 seed populates 4 CAB users
  - events.kind enum: ('dinner','office_hours','summit','virtual'). Adding
    'working_session' needs a CHECK rebuild
  - 'Office hours with the founder' → exactly one match in
    scripts/seed-demo.js:106

Divergences resolved with the user before step 2:
  1. Migration 0004 must skip users.title (already added in 0003) and only
     add pull_quote, member_number, focus_tags.
  2. Token name: spec writes --border-radius-lg; use existing --radius-lg.
  3. --secondary stays warm-walnut accent. Muted body text uses
     --on-surface-variant.
  4. Member-number backfill: order by cab_joined_date asc (NULLS via
     COALESCE to created_at), tiebreak user.id asc. Deterministic across
     re-runs and machines.
  5. focus_tags parser: trim, dedupe, strip control chars, collapse internal
     whitespace to single space, cap 3 entries × 24 chars.
  6. /events/past filters on starts_at < now() — no boolean past flag column.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:46:02 +02:00
fe27811d16 chore(demo): seed-demo.js + utc fix for last_seen_at
scripts/seed-demo.js — one open Pulse with realistic context, marks
"Traceability layer" as shipping with shipped_at -2 days and attributes
it to the cab user, two events (dinner in 5 weeks, office hours in 2
weeks), six hand-crafted activity rows mixing all 5 activity kinds so
the ticker has something to scroll on first load. Idempotent: skips if
any pulses exist. Backdates Lars's cab_joined_date so the greeting
renders "2 years, 4 months". Wired to db:setup and db:seed:demo.

Also fixes a parse bug on /pulse: SQL stores last_seen_at as
'YYYY-MM-DD HH:MM:SS' UTC, but new Date(string) parses that as local
time — on a non-UTC server the freshness check was wrong by the server's
offset. Coerce to UTC ISO before parsing. Manual smoke as Lars now shows
two member chips in "online now"; admin tabs all render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:04:11 +02:00
f6e7337c5e feat(admin): pulses, roadmap, events, activity tabs
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>
2026-05-11 14:57:44 +02:00
4611b687c9 feat(lib): notify() stub for pulse-opened events
Today this just logs to console. Wired up in step 10's admin pulse-publish
handler so the integration point exists from day one — only the body
changes when we pick a transactional email or Slack webhook later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:53:40 +02:00
351d90a3e8 feat(pulse): full member-portal landing at /pulse
Sections (top to bottom, all wrapped in a cascade-entry animation):
- greeting block: MONDAY · 11 MAY label, serif-italic "Good morning, X."
  greeting (server time, Europe/Copenhagen), tenure line that uses
  cab_joined_date for cab members and falls back to created_at
- ActivityTicker fed by getRecentActivity (hidden when empty)
- this-week Pulse card: live breathing dot + time-left countdown, italic
  serif question, 2×2 grid of option buttons that submit a hidden-action
  vote form. Once voted, options lock and show a distribution bar fill;
  the user's choice keeps a terracotta border. Empty state when no pulse
  is open.
- 2-col roadmap preview + council mark: three most-recently-updated items
  with status dot (copper/ochre/muted), and the user's CouncilMark md with
  the "{n} of 12 quarters" stat
- members-in-the-room: 4 chips for other CAB members seen within 5 min,
  +N overflow chip; pulsing copper dot for the "online now" indicator
- two-column event row: dark indigo "members-only" card (next dinner or
  summit) + light "office hours" card (next office_hours event). Hidden
  per-card when no matching upcoming event exists.

Vote POST uses the existing admin form pattern (hidden action field,
redirect on success). Activity row for 'voted' is written inline here;
step 11 covers the other activity sources.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:52:51 +02:00
39c9c805cd feat(component): ActivityTicker + format helpers
ActivityTicker — pure presentational. Renders a horizontal strip of items
that scrolls right→left over 38s, with CSS mask fades on both edges and
pause-on-hover. Items are duplicated so translateX(-50%) wraps seamlessly.
Hidden entirely when empty (no "no activity" placeholder).

format.ts — small set of formatters used by the ticker and the upcoming
/pulse greeting:
- relativeTime (2m / 3h / 5d / just now)
- redactName ("Maya Rasmussen" → "Maya R.")
- tenureSince (e.g. "2 years, 4 months")
- pulseDateLabel ("MONDAY · 11 MAY", Europe/Copenhagen)
- timeOfDay (morning/afternoon/evening, Europe/Copenhagen)
- tickerItem (ActivityRow + subject label → display struct, with role-dot
  colours mapped to existing pigments: copper for pilot, terracotta for
  cab, indigo for fenja)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:50:10 +02:00
6f23c47e7a feat(component): CouncilMark — generative member sigil
SVG component, sizes sm (48) / md (100) / lg (200) on a 120 viewBox.

12 quarter positions on a circle, i=0 = current quarter at 12 o'clock,
going clockwise into the past. A position is lit iff the member has a
roadmap_attribution to an item with shipped_at in that quarter. Pulse
votes do NOT light a quarter — the sigil represents consequential
contribution, not participation.

Visual layers:
- faint dotted outer ring rotating 90s linear
- small grey dots on empty quarters
- terracotta-filled polygon (15% fill) connecting lit dots, draws in
  over 1.6s on mount via stroke-dasharray
- terracotta dots on lit quarters with staggered 2.4s breathing
- serif italic initials in the centre, walnut secondary

Uses --pigment-terracotta instead of the spec's coral — no new tokens.
Honours prefers-reduced-motion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:48:47 +02:00
3cb76b33c8 feat(home): role-based / redirect via homeRouteForRole
cab and fenja are sent to /pulse on landing; pilot continues to see the
existing editorial home content in place. Uses the same helper the test
suite already covers, so behaviour is locked in.

Adds a minimal /pulse stub so the redirect target resolves; step 7 replaces
it with the full member view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:47:28 +02:00
267ba34747 feat(nav): restructure header + add footer for council portal
Nav now reads Pulse · Roadmap · Members · Events · [Admin]. Old links
(Home / Vision / Product / Updates / Contribute) are dropped from the
header; Vision moves to the footer alongside a Council manifesto stub.

Header keeps the name + Sign out on the right exactly as before — Sign out
is NOT duplicated in the footer.

Footer: small Fenja icon mark + © year + two text links. Uses the existing
ghost-border, no shadows, matches editorial flatness.

/council-manifesto added as a one-screen stub so the link doesn't 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:46:55 +02:00
20209db2d8 test: vitest suite — pulse status, vote uniqueness, home route
Three test files covering the Phase 1 invariants:
- derivePulseStatus: draft/closed are sticky; open auto-closes by date.
- vote UNIQUE(pulse_id, user_id): castVote (OR IGNORE) keeps the first vote
  silently, a raw second INSERT raises a constraint error, and uniqueness is
  per-pulse (same user on a different pulse is fine).
- homeRouteForRole: cab/fenja → /pulse, pilot → null (render existing home).

tests/setup.ts opens BIFROST_DB_PATH=':memory:' and applies all migrations
before tests run, so the in-memory DB has the live schema. Each vitest fork
gets its own globalThis → its own fresh in-memory DB.

The homeRouteForRole helper extraction makes the / role-redirect testable
without booting Astro. Step 6 will use it from /index.astro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:45:55 +02:00
fba369a36d chore(seed): one-time roadmap seed from content/roadmap.md
Parses the H2 sections (In progress / Next / Later) into roadmap_items rows.
Maps In-progress → beta (actively built, tested with pilots) and Next/Later
→ exploring with a target hint. Idempotent: skips entirely if the table is
already populated, so admin edits are never overwritten.

content/roadmap.md stays in the repo as the seed source. Once admin starts
editing via /admin, the DB is the source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:44:09 +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
53cb9a7e49 feat(db): add migration 0003 for council portal schema
Adds title/cab_joined_date/slug to users, extends attendance with kind enum
and 'interested' status, and creates pulses, votes, roadmap_items,
roadmap_attributions, events, and activity tables.

Slug backfill covers the three seed users; new-user slug generation will
live in db.ts. roadmap_items has shipped_at to drive the council mark
(simpler than an audit table). roadmap_attributions is admin-curated only.

Also logs the pre-existing /api/contributions/[id]/edit 302-only bug in
KNOWN_ISSUES.md so it isn't lost; out of scope for this work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:39:07 +02:00
Jonathan
d054b56bf7 chore: remove participants and calendar pages and nav links 2026-04-19 20:51:55 +02:00
Jonathan
a0931ea527 refactor: home page CTA as proper button with confirmation state 2026-04-19 20:46:43 +02:00
Jonathan
bc8e08d022 refactor: home page structure and pacing 2026-04-19 20:46:06 +02:00
Jonathan
f6e6fb255d fix: hero lockup — Fenja wordmark scale and colour 2026-04-19 20:44:57 +02:00
Jonathan
6cbab5ba36 chore: add Innofounder logo to public, wire to home page 2026-04-19 20:36:54 +02:00
Jonathan
49f5d976fb docs: document decisions D-15 through D-18 2026-04-19 20:32:09 +02:00
Jonathan
2d3391f531 feat: product page with architecture and platform framing 2026-04-19 20:31:51 +02:00
Jonathan
f7bd9085de feat: vision page as manifesto 2026-04-19 20:30:57 +02:00
Jonathan
d9c75a1921 refactor: rebuild home as welcome and pitch page 2026-04-19 20:30:14 +02:00
Jonathan
4bed3a5fe0 feat: join_requests table and join CTA flow 2026-04-19 20:29:09 +02:00
Jonathan
fa5e6d8414 docs: add editorial patterns (pulled quote, stat figure, arch diagram) to style guide 2026-04-19 20:28:10 +02:00
Jonathan
26173b7396 docs: add ProjectLockup to style guide 2026-04-19 20:27:20 +02:00
Jonathan
5a7af0b0d8 feat: ProjectLockup component 2026-04-19 20:26:45 +02:00
Jonathan
f8c7152fa9 chore: remove preview page in preparation for product page 2026-04-19 20:26:31 +02:00
Jonathan
a6cad10a72 docs: HANDOVER.md — build summary and next steps 2026-04-18 22:54:48 +02:00
Jonathan
82861ca4d2 chore: typecheck and build clean 2026-04-18 22:54:25 +02:00
Jonathan
7f02600c05 feat: admin panel 2026-04-18 22:52:29 +02:00