Commit graph

9 commits

Author SHA1 Message Date
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
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
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
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
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