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>
44 lines
1.3 KiB
Text
44 lines
1.3 KiB
Text
---
|
|
import type { ActivityRow } from '../../lib/db';
|
|
import { fmtDateTime } from '../../lib/markdown';
|
|
|
|
interface Props {
|
|
rows: ActivityRow[];
|
|
}
|
|
|
|
const { rows } = Astro.props;
|
|
---
|
|
<div class="tab-content">
|
|
<section class="section">
|
|
<h2 class="label-sm section-heading">Recent activity</h2>
|
|
<p class="body-sm section-note">
|
|
The raw activity feed — what powers the ticker on /pulse. Read-only debug view.
|
|
Showing up to 200 most-recent events; the ticker takes the last 12 within 7 days.
|
|
</p>
|
|
|
|
{rows.length === 0 ? (
|
|
<p class="body-sm empty-msg">No activity recorded yet.</p>
|
|
) : (
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th class="label-sm">When</th>
|
|
<th class="label-sm">Actor</th>
|
|
<th class="label-sm">Kind</th>
|
|
<th class="label-sm">Subject</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map(r => (
|
|
<tr>
|
|
<td class="body-sm muted">{fmtDateTime(r.created_at)}</td>
|
|
<td class="body-sm">{r.actor_name} <span class="muted">({r.actor_role})</span></td>
|
|
<td class="body-sm" style="text-transform:lowercase">{r.kind}</td>
|
|
<td class="body-sm muted">{r.subject_type} #{r.subject_id}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
)}
|
|
</section>
|
|
</div>
|