// CIL[BEGIN chunk_id="devops_dashboard_module_header_1785000011" type="module_header" identity="module_header" parent="none"] // CIL[DOC-MACRO purpose="Render the DevOps dashboard with performance metrics and observation tools"] // CIL[DOC-RESPONSIBILITY responsibility="Load observability summary and render metric charts, trace tables, and observation quick links"] // CIL[DOC-CONTEXT module="workbench.pages.devops.DevopsDashboard" layer="frontend-page" related_files="[\"custom/apps/workbench/functions/custom_workbench-observability.ts\",\"custom/apps/workbench/pages/observe/SignalsPage.tsx\",\"custom/apps/workbench/pages/observe/LogsPage.tsx\"]"] // CIL[DOC-CONTRACT inputs="none" outputs="JSX DevOps dashboard" errors="fetch errors rendered inline" side_effects="calls /api/custom_workbench-observability and /api/admin-data"] // CIL[DOC-MICRO logic_summary="Fetches an observability summary, signal/service counts, renders metric charts, recent traces, business outcomes, and navigation cards" notable_decisions=["Combines RPC-backed observability with runtime record counts","Uses Recharts for time-series visualisation"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] import { useEffect, useMemo, useState, Fragment } from 'react' import { useNavigate } from 'react-router-dom' import { apiFetch } from '@core/lib/api' import { formatDateTime, formatNumber, cn } from '@core/lib/utils' import { useAppPath } from '@core/hooks/useAppPath' import { Card, CardContent, CardHeader, CardTitle } from '@core/components/ui/card' import { Button } from '@core/components/ui/button' import { NativeSelect } from '@core/components/ui/native-select' import { LoadingSpinner } from '@core/components/ui/LoadingSpinner' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@core/components/ui/table' import { MetricChart } from '../../components/MetricChart' import { Pagination } from '../../components/Pagination' import { TraceTree } from '../../components/TraceTree' import { Gauge, Activity, ScrollText, AlertTriangle, Clock, Server, Signal, TrendingUp, RefreshCw, BarChart3, Kanban, ChevronDown, ChevronRight, } from 'lucide-react' // CIL[END chunk_id="devops_dashboard_module_header_1785000011"] // CIL[BEGIN chunk_id="devops_dashboard_dashboard_1785000012" type="function" identity="DevopsDashboard" parent="devops_dashboard_module_header_1785000011"] // CIL[DOC-MACRO purpose="DevOps dashboard implementation"] // CIL[DOC-RESPONSIBILITY responsibility="Manage dashboard state, fetch data, and render all sections"] // CIL[DOC-CONTEXT module="workbench.pages.devops.DevopsDashboard" layer="frontend-page" related_files="[\"custom/apps/workbench/functions/custom_workbench-observability.ts\",\"custom/apps/workbench/pages/observe/PerformancePage.tsx\"]"] // CIL[DOC-CONTRACT inputs="none" outputs="JSX" errors="fetch errors rendered inline" side_effects="network requests on mount and range change"] // CIL[DOC-MICRO logic_summary="Provides range selector, fetches summary/counts, renders stat cards, charts, tables, and quick links" notable_decisions=["Default range is 24 hours","Uses admin-data meta.total_count for signal/service totals"]] // CIL[DOC-CONFIDENCE pass="first" score="0.90" flags="none"] // CIL[DOC-STABILITY stability="experimental"] // CIL[DOC-AUDIENCE audience="developer"] const RANGE_OPTIONS = [ { label: 'Last hour', value: '1h', ms: 60 * 60 * 1000 }, { label: 'Last 24 hours', value: '24h', ms: 24 * 60 * 60 * 1000 }, { label: 'Last 7 days', value: '7d', ms: 7 * 24 * 60 * 60 * 1000 }, { label: 'Last 30 days', value: '30d', ms: 30 * 24 * 60 * 60 * 1000 }, ] function rangeDates(value: string) { const now = new Date() const option = RANGE_OPTIONS.find((o) => o.value === value) || RANGE_OPTIONS[1] return { from: new Date(now.getTime() - option.ms).toISOString(), to: now.toISOString(), } } function useCount(typeSlug: string) { const [count, setCount] = useState(0) useEffect(() => { apiFetch(`/api/admin-data?entity=items&type_slug=${typeSlug}&limit=1`) .then((r) => r.json()) .then((json) => setCount(json?.meta?.total_count || 0)) .catch(() => setCount(0)) }, [typeSlug]) return count } interface SummaryData { date_from: string date_to: string granularity: string logs: { total: number error_count: number user_action_count: number event_type_count: number levels: string[] sources: string[] source_types: string[] } traces: { total: number error_count: number total_spans: number avg_duration_ms: number recent: any[] } outcomes: { intent: string; outcome: string; count: number }[] metrics: { request_count: { bucket: string; value: number }[] error_rate: { bucket: string; value: number }[] avg_handler_duration: { bucket: string; value: number }[] } } function StatCard({ title, value, icon: Icon, href, alert, }: { title: string value: string | number icon: React.ElementType href?: string alert?: boolean }) { const navigate = useNavigate() const appPath = useAppPath() const content = ( <> {title}
{value}
) if (href) { return ( navigate(appPath(href))}> {content} ) } return {content} } export default function DevopsDashboard() { const appPath = useAppPath() const navigate = useNavigate() const [range, setRange] = useState('24h') const [summary, setSummary] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [tracePage, setTracePage] = useState(1) const [tracePageSize, setTracePageSize] = useState(10) const [traceRows, setTraceRows] = useState([]) const [traceTotal, setTraceTotal] = useState(0) const [traceExpanded, setTraceExpanded] = useState(null) const [outcomePage, setOutcomePage] = useState(1) const [outcomePageSize, setOutcomePageSize] = useState(10) const [outcomeRows, setOutcomeRows] = useState([]) const [outcomeTotal, setOutcomeTotal] = useState(0) const [outcomeExpanded, setOutcomeExpanded] = useState(null) const signalCount = useCount('workbench_signal') const serviceCount = useCount('workbench_service') const baseUrl = () => { const { from, to } = rangeDates(range) return `/api/custom_workbench-observability?date_from=${encodeURIComponent(from)}&date_to=${encodeURIComponent(to)}` } const load = async () => { setLoading(true) setError(null) try { const res = await apiFetch(`${baseUrl()}&action=summary`) const json = await res.json() if (json.error) throw new Error(json.error) setSummary(json.data || null) } catch (e: any) { setError(e.message || 'Failed to load observability summary') } finally { setLoading(false) } } const loadTraces = async () => { try { const offset = (tracePage - 1) * tracePageSize const res = await apiFetch( `${baseUrl()}&action=traces&limit=${tracePageSize}&offset=${offset}` ) const json = await res.json() if (json.error) throw new Error(json.error) setTraceRows((json.data?.data || []).map((r: any) => ({ ...r, duration_ms: Number(r.duration_ms || 0), span_count: Number(r.span_count || 0), error_count: Number(r.error_count || 0), }))) setTraceTotal(json.data?.total || 0) } catch (e: any) { setError(e.message || 'Failed to load traces') } } const loadOutcomes = async () => { try { const offset = (outcomePage - 1) * outcomePageSize const res = await apiFetch( `${baseUrl()}&action=outcomes&limit=${outcomePageSize}&offset=${offset}` ) const json = await res.json() if (json.error) throw new Error(json.error) setOutcomeRows(json.data?.data || []) setOutcomeTotal(json.data?.total || 0) } catch (e: any) { setError(e.message || 'Failed to load outcomes') } } useEffect(() => { load() loadTraces() loadOutcomes() }, [range]) useEffect(() => { loadTraces() }, [range, tracePage, tracePageSize]) useEffect(() => { loadOutcomes() }, [range, outcomePage, outcomePageSize]) const requestData = useMemo( () => (summary?.metrics?.request_count || []).map((row: any) => ({ bucket: row.bucket, value: Number(row.value) })), [summary] ) const errorData = useMemo( () => (summary?.metrics?.error_rate || []).map((row: any) => ({ bucket: row.bucket, value: Number(row.value) })), [summary] ) const durationData = useMemo( () => (summary?.metrics?.avg_handler_duration || []).map((row: any) => ({ bucket: row.bucket, value: Number(row.value) })), [summary] ) const topSources = useMemo(() => { return (summary?.logs?.sources || []).slice(0, 5) }, [summary]) const avgDuration = useMemo(() => { if (summary?.traces?.avg_duration_ms) return summary.traces.avg_duration_ms const points = summary?.metrics?.avg_handler_duration || [] if (points.length === 0) return null const avg = points.reduce((sum, p) => sum + Number(p.value || 0), 0) / points.length return avg || null }, [summary]) const statCards = [ { title: 'Log Events', value: formatNumber(summary?.logs?.total || 0), icon: ScrollText, href: '/logging/logs' }, { title: 'Log Errors', value: formatNumber(summary?.logs?.error_count || 0), icon: AlertTriangle, alert: (summary?.logs?.error_count || 0) > 0 }, { title: 'Traces', value: formatNumber(summary?.traces?.total || 0), icon: Activity }, { title: 'Trace Errors', value: formatNumber(summary?.traces?.error_count || 0), icon: AlertTriangle, alert: (summary?.traces?.error_count || 0) > 0 }, { title: 'Avg Trace Duration', value: avgDuration != null ? `${avgDuration.toFixed(0)} ms` : '—', icon: Clock }, { title: 'Signals', value: formatNumber(signalCount), icon: Signal, href: '/logging/signals' }, { title: 'Services', value: formatNumber(serviceCount), icon: Server, href: '/logging/services' }, { title: 'DEVOPS Board', value: 'Open', icon: Kanban, href: '/devops/kanban' }, ] return (

DevOps Main Dashboard

Performance metrics, traces, and observability signals

setRange(e.target.value)} className="w-40"> {RANGE_OPTIONS.map((o) => ( ))}
{error && (
{error}
)} {loading && !summary && (
)} {summary && ( <>
{statCards.map((card) => ( ))}
formatNumber(Number(v))} /> `${(Number(v) * 100).toFixed(1)}%`} yTickFormatter={(v) => `${(Number(v) * 100).toFixed(0)}%`} /> `${Number(v).toFixed(0)} ms`} yTickFormatter={(v) => `${Number(v).toFixed(0)} ms`} />
Recent Traces {traceRows.length === 0 ? (

No traces in the selected range.

) : ( <>
Trace Root Span Started Duration Spans Errors Outcome {traceRows.map((row: any) => ( setTraceExpanded(traceExpanded === row.trace_id ? null : row.trace_id)} > {traceExpanded === row.trace_id ? ( ) : ( )} {row.trace_id?.slice(0, 8)}… {row.root_span_name || '—'} {formatDateTime(row.started_at)} {(row.duration_ms || 0).toFixed(0)} ms {formatNumber(row.span_count || 0)} {formatNumber(row.error_count || 0)} 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700' )} > {row.outcome === 'failure' || (row.error_count || 0) > 0 ? 'Error' : 'OK'} {traceExpanded === row.trace_id && ( )} ))}
)}
Business Outcomes {outcomeRows.length === 0 ? (

No business outcomes recorded.

) : ( <>
Intent Outcome Count {outcomeRows.map((row: any, idx: number) => ( setOutcomeExpanded(outcomeExpanded === `${row.intent}-${row.outcome}-${idx}` ? null : `${row.intent}-${row.outcome}-${idx}`)} > {outcomeExpanded === `${row.intent}-${row.outcome}-${idx}` ? ( ) : ( )} {row.intent} {row.outcome} {formatNumber(row.count || 0)} {outcomeExpanded === `${row.intent}-${row.outcome}-${idx}` && (
{JSON.stringify(row, null, 2)}
)}
))}
)}
Observation Tools {topSources.length > 0 && ( )} )}
) } // CIL[END chunk_id="devops_dashboard_dashboard_1785000012"]