Add attention inbox and AI usage observability

This commit is contained in:
jjbrunton committed 2026-08-09 08:52:32 +01:00
1 parent ca59ba0f1f
commit 83b5f1f5b6
30 files changed
+702 -28

No files matched your search

@@ -0,0 +1,16 @@
import { AttentionResponseSchema } from '../../../../../packages/api-contracts/src/index.js';
import { createDashboardRequest } from '../../api/request.js';
export function createAttentionApi(fetchImplementation?: typeof fetch) {
const request = createDashboardRequest(fetchImplementation);
return {
async getAttention() {
const response = await request('/api/attention');
const payload: unknown = await response.json();
if (!response.ok) throw new Error(`Attention inbox unavailable (${response.status})`);
return AttentionResponseSchema.parse(payload);
},
};
}
export const attentionApi = createAttentionApi();
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import type { AttentionItem } from '../../../../../packages/api-contracts/src/index.js';
import { AccountOwnerBadge } from '../../components/account-owner-badge.js';
import { attentionApi } from './attention-api.js';
import './attention.css';
function formatTime(timestamp: number): string {
return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(timestamp);
}
function itemAction(item: AttentionItem) {
if (!item.conversationId) return null;
return <Link className="button secondary" to={`/conversations/${encodeURIComponent(item.conversationId)}`}>Open conversation</Link>;
}
export function AttentionPage() {
const query = useQuery({ queryKey: ['attention'], queryFn: attentionApi.getAttention, refetchInterval: 30_000 });
if (query.isPending) return <div className="page-state" role="status">Loading attention inbox…</div>;
if (query.isError) return <div className="page-state error-state" role="alert"><strong>Attention inbox unavailable</strong><span>{query.error.message}</span><button className="button secondary" onClick={() => void query.refetch()} type="button">Retry</button></div>;
const critical = query.data.items.filter((item) => item.severity === 'critical').length;
return (
<div className="page attention-page">
<header className="page-header"><div><span className="eyebrow">Operator queue</span><h1>Needs attention</h1><p>Boundaries, human handoffs, delivery failures, account problems, and Reddit cooldowns in one actionable inbox.</p></div><button className="button secondary" onClick={() => void query.refetch()} type="button">Refresh</button></header>
<section aria-label="Attention totals" className="status-strip"><div><span className={`status-dot ${query.data.items.length === 0 ? 'running' : 'stopped'}`} aria-hidden="true" /><div><small>Open items</small><strong>{query.data.items.length}</strong></div></div><span className={critical > 0 ? 'pill warning' : 'pill success'}>{critical} critical</span><span className="status-note">Updated {formatTime(query.data.generatedAt)}</span></section>
{query.data.items.length === 0 ? <div className="empty-state panel"><strong>Nothing needs attention.</strong><span>Automation and account health have no outstanding operator items.</span></div> : (
<ol aria-label="Attention items" className="attention-list">{query.data.items.map((item) => (
<li className={`attention-item is-${item.severity}`} key={item.id}><div className="attention-item-heading"><div><span className={`pill ${item.severity === 'critical' ? 'warning' : 'neutral'}`}>{item.kind.replaceAll('-', ' ')}</span><h2>{item.title}</h2></div><time dateTime={new Date(item.occurredAt).toISOString()}>{formatTime(item.occurredAt)}</time></div><p>{item.detail}</p>{item.owner ? <AccountOwnerBadge owner={item.owner} /> : null}{itemAction(item)}</li>
))}</ol>
)}
</div>
);
}
@@ -0,0 +1,8 @@
.attention-list { display: grid; gap: .8rem; padding: 0; margin: 0; list-style: none; }
.attention-item { display: grid; gap: .8rem; padding: 1rem; background: var(--surface); border: 1px solid var(--border); border-left: .3rem solid var(--warning); border-radius: .9rem; }
.attention-item.is-critical { border-left-color: var(--danger); }
.attention-item-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; }
.attention-item-heading > div { display: grid; gap: .55rem; }
.attention-item-heading time { color: var(--muted); font-size: .8rem; text-align: right; }
.attention-item p { margin: 0; color: var(--muted); }
.attention-item .button { justify-self: start; text-decoration: none; }