Commit graph

14 commits

Author SHA1 Message Date
fab927884c fix(db): migrate.js honors BIFROST_DB_PATH
So production migrations hit the same SQLite file the running app uses
(src/lib/db.ts), instead of a repo-local bifrost.db. Mirrors the pattern
already in seed-production.js and seed-roadmap.js. Falls back to the dev
db when unset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:54:22 +02:00
378ee989bb feat(db): production seed script with the real pilot data
Adds scripts/seed-production.js (db:seed:production / db:setup:production):
the curated pilot data — 8 council members, 2 Fenja admins, 10 roadmap
items, the launch event, the pulse vote, and the welcome dispatch — so a
production DB can be built reproducibly from git instead of committing the
binary bifrost.db.

Idempotent (every insert guarded). No credentials in the repo: council
accounts get a random unusable hash; admin temp passwords are hashed at
run time from ADMIN_SEED_PASSWORD (placeholder printed if unset, change on
first login). Run on deploy as: pnpm db:setup:production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:16:40 +02:00
8ca5e88618 feat(banner): editorial dispatch banner — title + 2-paragraph excerpt + author block
LatestDispatchBanner rebuilt from the three-column horizontal row into
an editorial card that reads as something to engage with, not a
database row.

New layout:
  - Meta row (1fr / auto): tracked 'LATEST DISPATCH · {relative}' +
    kind pill on the left; 'All dispatches →' tracked link on the right.
  - 30px serif headline beneath, max-width 720px so a real title can
    breathe across two lines if needed.
  - Body grid (1fr / auto): the prose on the left, an author block on
    the right. Prose splits into two paragraphs — p1 in primary text,
    p2 in muted (--on-surface-variant) ending in an ellipsis if the
    source extends beyond what was rendered. Author block has a small
    'Jonathan / team' stack alongside a 36px serif italic initial in
    an ink-coloured circle, plus a terracotta 'Read full dispatch →'
    CTA with a 1px terracotta bottom border.

Kind pills get per-kind tinted backgrounds (decision → indigo, update
→ copper, behind_the_scenes → walnut, note → terracotta) matching the
established kind-pigment mapping.

splitExcerpt helper added to src/lib/format.ts:
  - Prefers a markdown \\n\\n paragraph break (admin-controllable);
  - Falls back to the first sentence boundary past character 120;
  - Returns [first, null] when no good split exists — banner renders
    just p1 in that case and skips p2 entirely.

Admin: the excerpt field on /admin?tab=dispatches grows from a
single-line input to a 4-row textarea with the spec'd helper text
nudging admins to write 2-4 sentences with a blank-line break.

Seed: the decision dispatch's excerpt rewritten as the spec's
two-paragraph block so the new layout has real content to render.
Body stays unchanged.

Mobile: the body collapses to single-column; the author block jumps
above the prose with order: -1, so the byline reads first on small
screens and the text flows freely below it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:38:51 +02:00
788989fe35 chore(seed): roadmap copy refresh — status reflects 'currently live', not 'shipping date'
The previous seed conflated status='shipping' with 'about to ship' —
'Audit log export' was status=shipping target='Next week' which is
actually a queued release, not a live one. Refined so:

  status='shipping' → live in production now
  status='in_beta'  → not yet live, GA target set
  status='exploring' → on the long horizon
  status='considering' → not committed

Updated distribution: 2 shipping / 2 in_beta / 3 exploring / 2
considering. travelledStop = (1 + 0.5) / 9 ≈ 0.17, so the gradient
visibly transitions from travelled to ahead right at the 'you are
here' marker — the visual story matches the data.

Targets rewritten to read in this new register:
  - Traceability layer       Live since March
  - Document ingestion       Live since late May  ← .rr-current
  - Audit log export         GA next week (now in_beta)
  - Agentic query mode       July
  - Contextual memory        Q3 2026
  - Multi-organisation graphs Q3 2026
  - Multi-tenant isolation   Q4 2026
  - Federated learning hooks 2027 (considering)
  - Open evaluation framework 2027 (considering)

Descriptions rewritten so the In motion strip pulls a meaningful first
sentence from item #2 — 'Indexing PDF, Word, and plain text with
proper chunking.'

shipped_at backdated on items 1-2 only (60 days / 7 days ago), so the
.rr-current marker lands on the most recently-shipped item (Document
ingestion), not the about-to-GA in_beta item.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:01:31 +02:00
0fde7e493b chore(seed): /roadmap demo — 9 items spanning shipping → considering
Replaces the 7-item roadmap seed with the 9-item layout from the v5
spec. Distribution: 3 shipping → 1 in_beta → 3 exploring → 2 considering.
travelledStop computes to (2 + 0.5) / 9 ≈ 0.28, so the route gradient
visibly reads as 'travelled-then-ahead' rather than one solid tone.

Each item gets a target string and either a metadata_text (8 of 9) or
a fresh attribution (the one without metadata_text, 'Multi-tenant
isolation', attributed to Camilla — so the route card surfaces the
'Shaped by Camilla' trailing line via the fallback path).

metadata_text varies across the spec'd cues — 'Shaped by Lars in our
March session' / 'Pilot-tested with Mette's team' / 'Builds on
traceability layer' / 'Request beta access →' / '2 council requests' /
'Open question on key custody' / 'Council input wanted' / 'Long-term
direction'.

Attribution coverage now spans 6 of the 7 cab members so multiple
'Shaped by ...' trailers exist if metadata_text were ever cleared.

The first three shipping items get realistic shipped_at backdates
(-21 / -7 / -1 days) so the 'most recent shipping' detection lands on
'Audit log export' — which becomes the pulsed 'you are here' dot on
the route.

Smoke as Lars: /roadmap header reads 'What we are building.',
LatestDispatchBanner shows the deprioritising-public-cloud decision,
all nine route titles render, metadata_text trailing lines present in
the DOM, .rr-current marker on the most recent shipping milestone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:45:27 +02:00
39996ab93e feat(pulse): council marquee auto-scrolls all 7 members across the page
Council section gets a proper carousel — a horizontal marquee that
moves continuously across the page, listing every cab member in turn
rather than a fixed-size grid.

Implementation:
- Members rendered twice in a single flex track; CSS keyframe translates
  from translateX(0) to translateX(-50%) over 40s+ (duration scales with
  member count via the --marquee-duration inline custom prop, capped at
  6 sec per member or 28 sec minimum). At -50% the first copy is fully
  offscreen and the second copy occupies the visible window seamlessly;
  the loop resets without a visible jump.
- aria-hidden on the duplicated copies so screen readers don't double-
  announce.
- mask-image fades both edges so members slide in and out softly rather
  than clipping at the container edge.
- Paused on hover so a reader can stop and parse a tile.
- prefers-reduced-motion: animation off and the strip becomes a quietly
  scrollable horizontal list — keyboard / trackpad users can pan
  manually instead of relying on the animation.

Seed adds 3 more cab members for a total of 7 (Mads Lindberg, Camilla
Storm, Frederik Lund) with backdated cab_joined_date so member_numbers
allocate 5/6/7. Each gets title + pull_quote + focus_tags consistent
with the existing four. Tenure spread is now 3 → 24 weeks across the
seven members so /members renders meaningfully varied 'member since'
dates.

The previous 4-tile grid + 5th-tile-as-link case is gone; the marquee
loops the full set so no truncation is needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:21:53 +02:00
cde98f9454 feat(pulse): spacing pass + council section header + 7-item roadmap seed
Spacing — explicit per-section margins on /pulse rather than a single
gap. Page is padding: 40px 36px 80px now. Transitions match the spec:

  greeting   ─ 48px ─ below nav
  greeting   ─ 56px ─ hero
  hero       ─ 18px ─ also coming up    (intentionally tight; related)
  also       ─ 72px ─ editorial row
  editorial  ─ 72px ─ roadmap
  roadmap    ─ 72px ─ council

The hero, editorial, roadmap and council transitions all sit at 72px so
the page reads as four distinct registers rather than a slab stack. The
hero → also-coming-up gap stays deliberately tight at 18px because the
two are a pair (the strip is the lighter outro to the indigo card).

Council section restructured to match the roadmap carousel framing:
  - Outer card chrome dropped — no more single white surface wrapping the
    grid. Section is just a header row + a 4-column grid of tiles.
  - Header row: 22px serif 'The council' on the left, 11px terracotta
    tracked uppercase 'See who our council is made up of →' on the right.
    Same pattern as the roadmap header.
  - Tiles: 38px avatar (down from 56), 15px serif name, 11px title,
    10px tracked organisation. No background, no border. 24px grid gap.
  - First 4 members render; if more, a 5th tile replaces the would-be
    fifth member with a right-aligned 'See all N council members →' link.
    With the current 4-member seed this case isn't exercised but the
    branch is in place for when the council grows.
  - 2-up on tablets, 1-up below 520px.

Seed update: roadmap now has 7 items spanning all four statuses (2
shipping / 1 in_beta / 2 exploring / 2 considering) ordered by
display_order 1..7. Traceability layer carries the 'Shaped by Lars'
attribution; Agentic query mode is attributed to Anna; Contextual memory
to Henriette. The rest are unattributed so the attribution trailer's
hidden case is exercised too. With 7 items the carousel arrows engage
and the right-edge fade is visible at start.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:58:34 +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
9800d0a448 feat(pulse): two-box Fenja+poll, prominent hero, single-bg council, more air
Layout (per the v4 follow-up spec):

1b. Latest from Fenja is now a two-box layout when there's an attached
    poll: article on the left (wider), poll widget on the right. Without
    a poll, the article box takes the full row. Both boxes are surfaced
    on --surface-card with the same generous padding so they read as
    sibling pieces.

1c. Featured excerpt is extended to ~720 chars (was ~520) via a wider
    threshold on dispatchLongPreview. Below the article+poll row, the
    next two most-recent published dispatches render as minimalist rows
    — just title + kind + relative time, separated by ghost borders.

2.  Hero event: date column is now 150px wide (was 110px); grid uses
    align-items: center so the date+detail columns are vertically aligned
    rather than top-stuck. Day number scaled up to 3.5rem (was 2.75).
    Outer card padding bumped from --space-7 to --space-10. Hero title
    bumped to 2rem.

3.  More air: page-level section gap --space-10 → --space-12. Each
    on-page card has been re-padded; outer page horizontal padding goes
    down to --space-16 from --space-20 to match the narrower canvas.

6.  Council members no longer have individual card chrome. One outer
    --surface-card wraps the whole grid; each member cell is just an
    avatar + name + title + company stack with no background or border.
    Cells use a larger 6/8 grid gap so they don't crowd each other.

Inline poll widget on /dispatches/[slug]: when a dispatch has an
attached pulse, the article body is followed by a compact poll card
matching the /pulse-side widget. Vote POST handled inline; the page
re-renders with the locked + result-bar state.

scripts/seed-demo.js: the existing 'Which milestone should we anchor Q3
around?' pulse now attaches to the decision dispatch ('We are
deprioritising public-cloud parity for Q3') via pulse_id. Other
dispatches stay poll-free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 10:19:00 +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
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
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
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
Jonathan
0dc2dbd849 feat: database schema, migrations, and seed data 2026-04-18 22:43:16 +02:00