SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
3.8 KB · 83 lines tsx
Raw Blame History
1'use client';23import Link from 'next/link';4import { useState } from 'react';5import { PNum, StatusDot } from '@/components/ui/primitives';6import { adminFetch } from '@/lib/admin-fetch';7import { fmtDuration } from '@/lib/format';8import { Time } from '@/lib/time';9import type { AdminIncident } from '@/lib/types';10import { AdminPage, ErrorNote, Toast, btnCls, inputCls, useAdmin } from './shared';1112export function IncidentsReview() {13  const [status, setStatus] = useState('all');14  const { data, err, reload } = useAdmin<{ incidents: AdminIncident[] }>('/incidents', { status });15  const [notes, setNotes] = useState<Record<string, string>>({});16  const [msg, setMsg] = useState<string | null>(null);17  const [actErr, setActErr] = useState<string | null>(null);1819  const review = async (i: AdminIncident, verdict: 'confirmed' | 'dismissed' | 'unreviewed') => {20    setActErr(null);21    try {22      await adminFetch(`/incidents/${encodeURIComponent(i.event_id)}`, { method: 'PATCH', body: { review: verdict, note: notes[i.event_id] ?? i.note ?? '' } });23      setMsg(`${i.title}: ${verdict}`);24      reload();25    } catch (e) {26      setActErr(String(e));27    } finally {28      setTimeout(() => setMsg(null), 3000);29    }30  };3132  return (33    <AdminPage34      title="Incidents review"35      desc="Confirm or dismiss detected incidents and leave a note. Reviews feed the replay/validation dataset; they never alter the public record of what the engine detected."36      right={37        <select value={status} onChange={(e) => setStatus(e.target.value)} className={inputCls} aria-label="Status filter">38          {['all', 'active', 'resolved'].map((s) => (39            <option key={s}>{s}</option>40          ))}41        </select>42      }43    >44      <ErrorNote err={err ?? actErr} />45      <Toast msg={msg} />46      <ul className="divide-y divide-line">47        {(data?.incidents ?? []).map((i) => (48          <li key={i.event_id} className="grid gap-3 py-3 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto]">49            <div className="min-w-0">50              <div className="flex flex-wrap items-baseline gap-x-3">51                <PNum value={i.peak_pressure} className="text-[18px]" />52                <Link href={`/event/${i.slug}`} className="text-[13.5px] text-ink hover:text-accent">53                  {i.title}54                </Link>55                <StatusDot status={i.status} />56                <span className="label" style={{ color: i.review === 'confirmed' ? 'var(--ok)' : i.review === 'dismissed' ? 'var(--bad)' : 'var(--ink-3)' }}>57                  {i.review ?? 'unreviewed'}58                </span>59              </div>60              <p className="num mt-0.5 text-[11px] text-ink-3">61                {i.type} · {i.scope_label} · <Time ts={i.started_at} /> · {fmtDuration(i.duration_s)} · conf {Math.round(i.confidence * 100)} %62              </p>63              <p className="mt-1 text-[12px] text-ink-2">{i.summary}</p>64            </div>65            <textarea value={notes[i.event_id] ?? i.note ?? ''} onChange={(e) => setNotes({ ...notes, [i.event_id]: e.target.value })} placeholder="review note…" rows={2} className={`${inputCls} h-auto w-full py-1.5`} aria-label={`Note for ${i.title}`} />66            <div className="flex gap-2 lg:flex-col">67              <button type="button" className={`${btnCls} border-ok/60 text-ok`} onClick={() => void review(i, 'confirmed')}>68                confirm69              </button>70              <button type="button" className={`${btnCls} border-bad/60 text-bad`} onClick={() => void review(i, 'dismissed')}>71                dismiss72              </button>73              <button type="button" className={btnCls} onClick={() => void review(i, 'unreviewed')}>74                save note75              </button>76            </div>77          </li>78        ))}79      </ul>80    </AdminPage>81  );82}83