project-bifrost-platform/src/admin/resources/activity.ts
Jonathan Hvid 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

102 lines
2.8 KiB
TypeScript

/* ---------------------------------------------------------------------------
* Activity resource — read-only debug feed.
*
* Activity rows are emitted by side effects elsewhere in the app (voting,
* RSVPs, roadmap-ship transitions, pulse opens). The admin view is a tail
* of the table for monitoring; no create, no edit, no delete.
* ------------------------------------------------------------------------- */
import {
getAllActivityForAdmin,
type ActivityKind,
type ActivityRow,
} from '../../lib/db';
import type { Resource } from '../resource-types';
const KIND_LABEL: Record<ActivityKind, string> = {
voted: 'Voted',
rsvped: 'RSVPed',
booked_office_hours: 'Booked office hours',
roadmap_shipped: 'Roadmap shipped',
pulse_opened: 'Pulse opened',
};
const KIND_PILL_CLASS: Record<ActivityKind, string> = {
voted: 'pill-update',
rsvped: 'pill-published',
booked_office_hours: 'pill-bts',
roadmap_shipped: 'pill-shipping',
pulse_opened: 'pill-pending',
};
const DAY_MS = 24 * 60 * 60 * 1000;
export const activityResource: Resource<ActivityRow> = {
key: 'activity',
label: 'Activity',
pluralLabel: 'Activity',
singularLabel: 'Event',
groupKey: 'system',
description: 'Recent member actions: votes, RSVPs, office-hour bookings, pulse opens, roadmap ships.',
list: {
queryFn: () => getAllActivityForAdmin(200),
columns: [
{
key: 'actor_name',
label: 'Actor',
primary: true,
width: '1.5fr',
render: (item) => ({
title: item.actor_name,
subtitle: item.actor_role,
}),
},
{
key: 'kind',
label: 'Kind',
kind: 'pill',
width: '160px',
pillVariants: Object.fromEntries(
(Object.keys(KIND_LABEL) as ActivityKind[]).map((k) => [
k,
{ label: KIND_LABEL[k], class: KIND_PILL_CLASS[k] },
]),
),
},
{
key: 'subject',
label: 'Subject',
width: '1fr',
render: (item) => ({
title: `${item.subject_type} #${item.subject_id}`,
}),
},
{
key: 'created_at',
label: 'When',
kind: 'relative-date',
width: '120px',
},
],
filters: [
{ key: 'all', label: 'All', predicate: () => true, isDefault: true },
{
key: 'last_7_days',
label: 'Last 7 days',
predicate: (i) => Date.now() - new Date(i.created_at).getTime() < 7 * DAY_MS,
},
{
key: 'last_30_days',
label: 'Last 30 days',
predicate: (i) => Date.now() - new Date(i.created_at).getTime() < 30 * DAY_MS,
},
],
defaultSort: { key: 'created_at', direction: 'desc' },
pageSize: 100,
},
// Pure read view — no form, no summary, no ops, no actions.
form: null,
ops: {},
};