Files
Dialectic.Frontend/src/pages/AgentActivity.tsx
hzhang 3dbb5abaf6 feat(frontend): v2 rewrite — Vite + React + TS readonly SPA
Replaces the v1 CRA app (which targeted the obsolete Python Dialectic
backend) with a fresh Vite + React 18 + TypeScript scaffold that talks
to Dialectic.Backend Go v2.

Pages (all readonly — propose/signup/post are agent-only by design):
  - /                       TopicList — filter by status, paginated
  - /topics/:id             TopicDetail — meta + camps + transcript
                            (polling every 8s)
  - /topics/:id/verdict     Verdict permalink (shareable)
  - /agents/:id             AgentActivity — admin diagnostics card

Stack:
  - Vite 5 + React 18 + react-router-dom 6
  - Pure ESM, NodeNext-style imports, .tsx
  - Style: ~/STYLE.md tokens (IBM Plex Mono + Major Mono Display +
    --acid #d8ff3e on --ink #080a0d, with subtle blueprint grid wash)

Auth:
  - v1 dev-bypass only — VITE_OIDC_DEV_BYPASS auto-attaches
    x-dev-bypass header. Real Keycloak OIDC redirect ships as v2.
  - Admin endpoints (x-dialectic-admin-key) prompt on first visit
    and store key in localStorage. Never baked into bundle. Never
    sent to non-admin endpoints.

Backend pairing:
  - Dialectic.Backend@0b16b52 adds GET /api/admin/agents/{id} for the
    AgentActivity page. AgentActivity calls it via the admin-key
    branch in api.ts.

Deploy:
  - Multi-stage Dockerfile (node:22-alpine build → nginx:1.27-alpine
    serve). nginx.conf reverse-proxies /api/ → dialectic-backend:8090
    so the browser sees one origin (no CORS).

Reuses the existing hzhang/Dialectic.Frontend repo — old CRA contents
nuked in this commit. History preserved on master.
2026-05-24 00:15:35 +01:00

133 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getAgentSummary, HttpError } from '../api';
import type { AgentSummary } from '../types';
import { Link } from 'react-router-dom';
import { fmtRelative, fmtTime } from '../util';
export function AgentActivityPage() {
const { id } = useParams<{ id: string }>();
const [data, setData] = useState<AgentSummary | null>(null);
const [err, setErr] = useState<string | null>(null);
const [needsKey, setNeedsKey] = useState(false);
const [pendingKey, setPendingKey] = useState('');
useEffect(() => {
if (!id) return;
const ac = new AbortController();
getAgentSummary(id, ac.signal)
.then((d) => {
setData(d);
setErr(null);
setNeedsKey(false);
})
.catch((e) => {
if ((e as Error).name === 'AbortError') return;
if (e instanceof HttpError && (e.status === 401 || e.status === 403)) {
setNeedsKey(true);
setErr(null);
} else {
setErr((e as Error).message);
}
});
return () => ac.abort();
}, [id]);
function saveKey() {
if (!pendingKey.trim()) return;
localStorage.setItem('dialectic-admin-key', pendingKey.trim());
setPendingKey('');
window.location.reload();
}
if (!id) return <div className="err">missing agent id</div>;
return (
<div>
<span className="eyebrow">agent</span>
<h1>{id}</h1>
{needsKey && (
<div className="aa-admin-prompt">
<div>
this page calls an <strong>admin endpoint</strong> paste your
<code> x-dialectic-admin-key</code> (stored in localStorage; never
sent except to <code>/api/admin/*</code>).
</div>
<input
type="password"
value={pendingKey}
onChange={(e) => setPendingKey(e.target.value)}
placeholder="admin key…"
/>
<div className="row" style={{ marginTop: 8 }}>
<button onClick={saveKey}>save + reload</button>
<span className="muted">key never leaves this browser</span>
</div>
</div>
)}
{err && <div className="err">{err}</div>}
{data && (
<>
<div className="aa-summary-grid">
<div className="aa-card">
<div className="aa-card-value">
{data.key_provisioned ? 'yes' : 'no'}
</div>
<div className="aa-card-label">key provisioned</div>
</div>
<div className="aa-card">
<div className="aa-card-value">{data.signups_count}</div>
<div className="aa-card-label">signups</div>
</div>
<div className="aa-card">
<div className="aa-card-value">{data.arguments_count}</div>
<div className="aa-card-label">arguments</div>
</div>
<div className="aa-card">
<div className="aa-card-value">{data.verdicts_count}</div>
<div className="aa-card-label">verdicts</div>
</div>
</div>
<h2>recent topics</h2>
{data.recent_topics.length === 0 ? (
<div className="td-empty">no recent topics for this agent</div>
) : (
<table className="tl-table">
<thead>
<tr>
<th>topic</th>
<th>role</th>
<th>status</th>
<th>last action</th>
</tr>
</thead>
<tbody>
{data.recent_topics.map((t) => (
<tr key={t.topic_id}>
<td>
<Link to={`/topics/${t.topic_id}`}>{t.title}</Link>
</td>
<td>
<span className={`camp-${t.role}`}>{t.role}</span>
</td>
<td>
<span className={`pill pill-${t.status}`}>{t.status}</span>
</td>
<td title={fmtTime(t.last_action_at)}>
{fmtRelative(t.last_action_at)}
</td>
</tr>
))}
</tbody>
</table>
)}
</>
)}
</div>
);
}