- Mobile: a hamburger nav with a dropdown, and ≤767/720px breakpoints
across pages that collapse multi-column grids to one column and cut the
112px desktop side padding down for phones; admin gets a phone pass too.
- Readability: bump the type-scale tokens and the small hardcoded sizes
across user-facing pages (roadmap route excepted — already enlarged).
- Pulse votes now sit in a warm terracotta-tinted panel so they stand out.
- Header: 50% larger Fenja AI logo, the dot vertically centred to it, and a
rebalanced "Project Bifrost" lockup (smaller, matched cap heights).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin can now upload a png/jpg event photo (SPEC §8 exception added):
- new image-upload admin field kind with live preview, uploading via
POST /api/admin/upload (fenja-only, type + 5MB validation);
- files stored under data/uploads (gitignored, BIFROST_UPLOAD_DIR
overridable) and served by GET /uploads/[file] with a traversal guard.
Reworks the /pulse event card: the greeting moved inside a taller box, the
"next gathering" label sits above the date + title, and the photo renders
as a top-right background that blends into the indigo via gradient masks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
The Create Invitation flow rendered "/invite?t=…" instead of
"https://host/invite?t=…" because the origin was gated on an unset
PUBLIC_ORIGIN env var.
Solution: OpContext now carries `origin` (always set by the route
handler from Astro.url.origin), and invitations.ts builds the magic
link from it. No env vars required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Last resource lands. /admin/activity tails the activity table —
votes, RSVPs, office-hour bookings, roadmap ships, pulse opens —
with last-7-days / last-30-days filters. Pure read view: no form,
no summary, no ops.
src/admin/components/ResourceListView.astro: rows fall back to <div>
when the resource has no panel pathway (form: null AND no summary).
Activity rows aren't clickable now — previously they'd dirty the URL
with a ?edit= that resolved to nothing.
The registry is complete: 7 resources across 3 groups, matching the
sidebar layout described in the spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
The Backstage rebuild's first three resource configs. /admin/dispatches,
/admin/roadmap, and /admin/events now resolve through the dynamic route
with full list views, edit panels, and the publish/archive actions.
- src/admin/resources/dispatches.ts — kind/status/author/excerpt/body
fields, embedded pulse sub-form (pulse_question + multi-text options +
opens/closes datetimes), publish/archive actions, notifyCount on
drafts so the sidebar lights up terracotta until they ship.
- src/admin/resources/roadmap.ts — title/description/status/target/
display_order/metadata_text plus a multi-select-async for attributed
members. ops.update writes via setRoadmapAttributions() after the
basic save so the pivot table stays in sync.
- src/admin/resources/events.ts — full event fields; ops.create
auto-generates a unique slug from the title when blank.
- src/admin/embeds/PulseSubForm.astro — reads the dispatch's current
pulse via getPulseById(), renders question + options + opens/closes.
Pulses follow their parent dispatch's lifecycle (draft → open on
publish, → closed on archive); no status field of their own.
- src/admin/components/ResourceEditPanel.astro — dispatches on
embed.component, renders PulseSubForm for 'pulse-sub-form'.
- src/admin/resource-types.ts — renamed column .valueOf to .value
(collision with Object.prototype.valueOf was breaking TS structural
matching); OpContext now optionally carries the raw FormData so
resources with sub-forms can read embed fields.
- src/pages/admin/[resource].astro — passes formData into opCtx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The production route every Backstage resource lives under. Resolves
the resource from the URL segment against the registry, gates on
user.role === 'fenja', and renders the AdminLayout shell with the
ResourceListView + (optionally) ResourceEditPanel.
POST dispatch keyed by _action:
- save: parses formdata per field.kind (multi-text/multi-select-async
use getAll(), number coerces, others coerce to string), validates
via validateForResource, then routes to ops.update(id) when
?edit=<id> is set or ops.create() when ?new=1. Redirects with
?msg=saved | ?msg=created. On failure, re-renders the panel with
errors + the submitted values.
- delete: calls ops.delete(id), redirects with ?msg=deleted.
- <action.key>: looks up the action in resource.actions and runs its
handler, redirects with ?msg=action_<key>.
404s when the resource key isn't in the registry — most keys won't
resolve until steps 8-10 land. A small .bs-flash banner above the
list surfaces the ?msg= text (or the error message after a failed
save).
Old /admin (?tab=...) continues to work alongside.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Right-slide panel that renders a resource's edit form. Driven by the
URL: ?new=1 opens a fresh form, ?edit=<id> hydrates with the current
item. POSTs back to the same URL with _action (save | delete | <action
key>); the route handler in step 7 dispatches.
- FieldRenderer.astro: dispatches on field.kind, wraps each field with
label + helper text + error state.
- fields/*.astro: one component per kind — Text, Textarea, Markdown
(with Write/Preview toggle), Select, SelectAsync, MultiSelectAsync,
MultiText (with add/remove), Date, Datetime, Number, Readonly.
- ResourceEditPanel.astro: header (title + close X), scrollable body,
sticky footer (save + per-resource secondary actions + destructive
delete when ops.delete is defined and item exists). Scrim closes on
click, Esc, or the close link. Confirm-before-submit honours
action.confirmText. Embedded sub-form sections render a placeholder
until step 8 wires the pulse renderer.
- admin.css: panel chrome + scrim + slide-in keyframes, full field
styling for every kind, mobile full-screen modal collapse.
- preview.astro: exercises every field kind so the panel can be
eyeballed in a logged-in session. Try /admin/preview?new=1 and
/admin/preview?edit=<id>.
Autosave deferred to Phase 2 per the approved deltas.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shared list-rendering component every resource will use. Reads
URL state (?filter, ?q, ?page) and derives:
- active filter (with isDefault fallback)
- active column set (columnsByFilter[filterKey] override → columns)
- filtered + searched + sorted + paginated row set
Rows are full anchor elements pointing at ?edit=<id> so the table
is fully keyboard-navigable and works without JS. The "+ New" button
is suppressed when resource.form is null (activity, join_requests).
- ResourceListView.astro: page header (eyebrow + serif h1 + optional
description + new-item button), toolbar (search form + filter
chips), grid table with --bs-grid-cols set from column widths,
pagination, mobile card collapse.
- ListCell.astro: discriminated render for text / pill / relative-date
/ number / tag-list columns.
- admin.css: list-view styles plus the full pill palette (decision,
update, note, bts, published, draft, archived, open, closed,
pending, accepted, expired, approved, declined, shipping, in-beta,
exploring, considering, active, departed, pilot, cab, fenja).
- preview.astro: inline sample dispatches resource so the list view
renders against real DB rows. Step 8 moves this to its production
config; this inline copy disappears with the preview route in
step 11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two-pane Backstage chrome: sticky topbar with the wordmark + " /
Backstage" lockup and a "Back to the portal" link, plus a left
sidebar that walks the resource registry and renders grouped
links with active-state and count badges.
- src/admin/components/AdminLayout.astro — the shell. Pre-resolves
list-counts and notify-counts per resource so the sidebar can
render badges without async work in markup. Renders an empty
state until resources land.
- src/admin/resources/index.ts — empty registry stub. Three groups
declared (publishing, council, system); resources populated in
steps 8–10.
- src/admin/admin.css — Backstage tokens (--admin-sidebar-bg,
--admin-active-accent, etc.) and the shell styles (bs-topbar,
bs-sidebar, bs-resource, bs-count). Mobile collapses the sidebar
above the main pane.
- src/pages/admin/preview.astro — temporary smoke-test route at
/admin/preview. Deleted in step 11 when the new admin replaces
the old one.
Old /admin (?tab=…) is untouched and continues to work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Establishes the load-bearing type surface for the Backstage rebuild:
- src/admin/resource-types.ts — discriminated unions for Field, Column,
Filter, Action, plus the top-level Resource and ResourceGroup. Strict
per the maintainability bar: a config object missing a required key
fails TypeScript.
- src/admin/validate.ts — validateForResource() derives validation
from the field definitions (required, maxLength, multi-text min/max,
number bounds, date parse, visibleWhen-aware).
- tests/admin-validate.test.ts — 8 cases locking the validator API:
required, maxLength, visibleWhen skip & reveal, multi-text bounds,
number bounds, all-valid, form-null short-circuit.
No consumers yet. Next commit pulls these into admin.css and the
shared layout components.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the ~300-line <style> block from src/pages/admin/index.astro
into a dedicated stylesheet, imported from the page frontmatter.
No rule changes — verbatim extraction so the existing admin UI
continues to render identically.
This is the first commit of the Backstage rebuild: it establishes
the shared admin stylesheet that the resource-pattern components
will consume in subsequent steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>