Skip to content

Commit a500968

Browse files
haasonsaasclaude
andcommitted
Add Events and Admin pages for wide event visualization
Events page: full Ensemble-style review event viewer with stat cards, duration/token timeline charts, filterable/sortable event table with expandable detail rows showing per-file waterfall, hotspots, and pass breakdown. Admin page: system-wide analytics dashboard with usage metrics, model breakdown table, repository activity charts, error rate monitoring, latency percentiles (p50/p95/p99), and slow file outliers. Backend: add /api/events endpoint returning ReviewEvent objects with source/model/status filtering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c63abe commit a500968

8 files changed

Lines changed: 909 additions & 1 deletion

File tree

src/server/api.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,64 @@ pub struct ListReviewsParams {
7373
pub per_page: Option<usize>,
7474
}
7575

76+
#[derive(Deserialize)]
77+
pub struct ListEventsParams {
78+
pub source: Option<String>,
79+
pub model: Option<String>,
80+
pub status: Option<String>,
81+
}
82+
83+
use super::state::ReviewEvent;
84+
85+
/// Returns all wide events from completed/failed reviews, sorted newest-first.
86+
pub async fn list_events(
87+
State(state): State<Arc<AppState>>,
88+
Query(params): Query<ListEventsParams>,
89+
) -> Json<Vec<ReviewEvent>> {
90+
let reviews = state.reviews.read().await;
91+
let mut events: Vec<ReviewEvent> = reviews
92+
.values()
93+
.filter_map(|s| s.event.clone())
94+
.filter(|e| {
95+
let source_ok = params
96+
.source
97+
.as_ref()
98+
.map_or(true, |f| e.diff_source.eq_ignore_ascii_case(f));
99+
let model_ok = params
100+
.model
101+
.as_ref()
102+
.map_or(true, |f| e.model.eq_ignore_ascii_case(f));
103+
let status_ok = params.status.as_ref().map_or(true, |f| {
104+
e.event_type.eq_ignore_ascii_case(&format!("review.{}", f))
105+
});
106+
source_ok && model_ok && status_ok
107+
})
108+
.collect();
109+
events.sort_by(|a, b| b.review_id.cmp(&a.review_id));
110+
events.sort_by(|a, b| {
111+
b.duration_ms
112+
.cmp(&a.duration_ms)
113+
.then(b.review_id.cmp(&a.review_id))
114+
});
115+
// Sort by review start time proxy: we use completed reviews ordering
116+
// Re-sort by the review order (newest first) using the review map ordering
117+
let id_order: std::collections::HashMap<String, usize> = {
118+
let mut ordered: Vec<_> = reviews
119+
.values()
120+
.filter(|s| s.event.is_some())
121+
.map(|s| (s.id.clone(), s.started_at))
122+
.collect();
123+
ordered.sort_by(|a, b| b.1.cmp(&a.1));
124+
ordered
125+
.into_iter()
126+
.enumerate()
127+
.map(|(i, (id, _))| (id, i))
128+
.collect()
129+
};
130+
events.sort_by_key(|e| id_order.get(&e.review_id).copied().unwrap_or(usize::MAX));
131+
Json(events)
132+
}
133+
76134
// === Handlers ===
77135

78136
pub async fn get_status(State(state): State<Arc<AppState>>) -> Json<StatusResponse> {

src/server/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ pub async fn start_server(config: Config, host: &str, port: u16) -> anyhow::Resu
9393
.route("/status", get(api::get_status))
9494
.route("/review", post(api::start_review))
9595
.route("/reviews", get(api::list_reviews))
96+
.route("/events", get(api::list_events))
9697
.route("/review/{id}", get(api::get_review))
9798
.route("/review/{id}/feedback", post(api::submit_feedback))
9899
.route("/doctor", get(api::get_doctor))

web/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { Analytics } from './pages/Analytics'
99
import { Settings } from './pages/Settings'
1010
import { Doctor } from './pages/Doctor'
1111
import { Repos } from './pages/Repos'
12+
import { Events } from './pages/Events'
13+
import { Admin } from './pages/Admin'
1214

1315
const queryClient = new QueryClient({
1416
defaultOptions: {
@@ -32,6 +34,8 @@ export default function App() {
3234
<Route path="/analytics" element={<Analytics />} />
3335
<Route path="/settings" element={<Settings />} />
3436
<Route path="/repos" element={<Repos />} />
37+
<Route path="/events" element={<Events />} />
38+
<Route path="/admin" element={<Admin />} />
3539
<Route path="/doctor" element={<Doctor />} />
3640
<Route path="/docs" element={
3741
<div className="p-6 max-w-3xl mx-auto">

web/src/api/client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,13 @@ export const api = {
9090
method: 'POST',
9191
body: JSON.stringify({ diff_source: 'raw', diff_content: diffContent, title }),
9292
}),
93+
94+
listEvents: (params?: { source?: string; model?: string; status?: string }) => {
95+
const qs = new URLSearchParams()
96+
if (params?.source) qs.set('source', params.source)
97+
if (params?.model) qs.set('model', params.model)
98+
if (params?.status) qs.set('status', params.status)
99+
const suffix = qs.toString() ? `?${qs}` : ''
100+
return request<import('./types').ReviewEvent[]>(`/events${suffix}`)
101+
},
93102
}

web/src/api/hooks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ export function useGhPrs(repo: string | undefined, state?: string) {
101101
})
102102
}
103103

104+
export function useEvents() {
105+
return useQuery({
106+
queryKey: ['events'],
107+
queryFn: () => api.listEvents(),
108+
refetchInterval: REFETCH.reviews,
109+
})
110+
}
111+
104112
export function useStartPrReview() {
105113
const queryClient = useQueryClient()
106114
return useMutation({

web/src/components/Layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NavLink, Outlet } from 'react-router-dom'
2-
import { Home, Settings, Stethoscope, ScrollText, GitCompareArrows, BarChart3, BookOpen, GitPullRequestDraft } from 'lucide-react'
2+
import { Home, Settings, Stethoscope, ScrollText, GitCompareArrows, BarChart3, BookOpen, GitPullRequestDraft, Activity, Shield } from 'lucide-react'
33
import { useStatus } from '../api/hooks'
44

55
const sections = [
@@ -12,6 +12,7 @@ const sections = [
1212
label: 'REVIEW',
1313
items: [
1414
{ to: '/history', icon: ScrollText, label: 'Logs' },
15+
{ to: '/events', icon: Activity, label: 'Events' },
1516
{ to: '/repos', icon: GitPullRequestDraft, label: 'Repos' },
1617
{ to: '/analytics', icon: BarChart3, label: 'Analytics' },
1718
{ to: '/settings', icon: Settings, label: 'Settings' },
@@ -20,6 +21,7 @@ const sections = [
2021
{
2122
label: 'SYSTEM',
2223
items: [
24+
{ to: '/admin', icon: Shield, label: 'Admin' },
2325
{ to: '/doctor', icon: Stethoscope, label: 'Doctor' },
2426
{ to: '/docs', icon: BookOpen, label: 'Documentation' },
2527
],

0 commit comments

Comments
 (0)