Commit graph

10 commits

Author SHA1 Message Date
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
18d371b368 feat(admin): activity resource (read-only debug feed)
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>
2026-05-12 16:38:11 +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
dd9ea68fab feat(admin): publishing-group resources (dispatches, roadmap, events)
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>
2026-05-12 16:24:13 +02:00
3aaa21e6af feat(admin): /admin/[resource] dynamic route + POST dispatch
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>
2026-05-12 16:15:17 +02:00
09a10061b2 feat(admin): ResourceEditPanel + field renderers (no autosave)
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>
2026-05-12 16:10:39 +02:00
cc9332e6e2 feat(admin): ResourceListView + ListCell with filter-conditional columns
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>
2026-05-12 16:05:15 +02:00
dd7215d828 feat(admin): AdminLayout shell + empty resource registry
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>
2026-05-12 16:00:57 +02:00
ea056fff7b feat(admin): add Resource type definitions + form validator
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>
2026-05-12 15:56:42 +02:00
103bfa2f0c refactor(admin): extract inline styles to src/admin/admin.css
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>
2026-05-12 15:53:10 +02:00