File size: 5,168 Bytes
81ff144 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | import React, { useCallback, useEffect, useState } from 'react';
import { Activity, AlertTriangle, Database, RefreshCw, Server, ShieldCheck } from 'lucide-react';
import { motion } from 'framer-motion';
import { supabase } from '../services/supabase';
import { getApiUrl } from '../services/runtimeConfig';
interface MonitoringSummary {
status: string;
checks: Record<string, string>;
counts: Record<string, number>;
timestamp: string;
error?: string;
}
const emptySummary: MonitoringSummary = {
status: 'loading',
checks: { api: 'checking', database: 'checking' },
counts: {
projects: 0,
tasks: 0,
agents: 0,
task_runs: 0,
failed_tasks: 0,
pending_reviews: 0
},
timestamp: new Date().toISOString()
};
const MonitoringView: React.FC = () => {
const [summary, setSummary] = useState<MonitoringSummary>(emptySummary);
const [loading, setLoading] = useState(false);
const fetchFallbackSummary = useCallback(async (): Promise<MonitoringSummary> => {
const [projects, tasks, agents, runs, failed, reviews] = await Promise.all([
supabase.from('projects').select('id', { count: 'exact', head: true }),
supabase.from('tasks').select('id', { count: 'exact', head: true }),
supabase.from('agents').select('id', { count: 'exact', head: true }),
supabase.from('task_runs').select('id', { count: 'exact', head: true }),
supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('status', 'failed'),
supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('status', 'awaiting_approval')
]);
return {
status: 'ok',
checks: { api: 'unreachable', database: 'ok' },
counts: {
projects: projects.count ?? 0,
tasks: tasks.count ?? 0,
agents: agents.count ?? 0,
task_runs: runs.count ?? 0,
failed_tasks: failed.count ?? 0,
pending_reviews: reviews.count ?? 0
},
timestamp: new Date().toISOString(),
error: 'Backend monitoring endpoint unavailable; using Supabase fallback.'
};
}, []);
const refresh = useCallback(async () => {
setLoading(true);
const apiUrl = getApiUrl();
try {
const response = await fetch(`${apiUrl}/monitoring/summary`);
if (!response.ok) throw new Error(`Backend returned ${response.status}`);
setSummary(await response.json());
} catch {
setSummary(await fetchFallbackSummary());
} finally {
setLoading(false);
}
}, [fetchFallbackSummary]);
useEffect(() => {
refresh();
}, [refresh]);
const degraded = summary.status !== 'ok' || Object.values(summary.checks).some((check) => check === 'error');
return (
<div className="monitoring-page animate-fade-in">
<div className="monitoring-header">
<div className="panel-heading" style={{ marginBottom: 0 }}>
<Activity size={32} color="var(--accent)" />
<div>
<h2>Operations Monitor</h2>
<p style={{ color: 'var(--text-dim)', fontSize: '0.9rem' }}>Track platform health and workflow volume.</p>
</div>
</div>
<button className="btn btn-glass" onClick={refresh} disabled={loading}>
<RefreshCw size={18} />
{loading ? 'Refreshing...' : 'Refresh'}
</button>
</div>
<div className={`glass-panel monitoring-status ${degraded ? 'is-degraded' : 'is-ok'}`}>
{degraded ? <AlertTriangle size={24} /> : <ShieldCheck size={24} />}
<div>
<span>System Status</span>
<strong>{degraded ? 'Degraded' : 'Operational'}</strong>
{summary.error && <p>{summary.error}</p>}
</div>
</div>
<div className="monitoring-grid">
<MetricCard icon={<Server size={20} />} label="Projects" value={summary.counts.projects} />
<MetricCard icon={<Activity size={20} />} label="Tasks" value={summary.counts.tasks} />
<MetricCard icon={<Database size={20} />} label="Agents" value={summary.counts.agents} />
<MetricCard icon={<RefreshCw size={20} />} label="Runs" value={summary.counts.task_runs} />
<MetricCard icon={<AlertTriangle size={20} />} label="Failed" value={summary.counts.failed_tasks} danger />
<MetricCard icon={<ShieldCheck size={20} />} label="Reviews" value={summary.counts.pending_reviews} />
</div>
<div className="glass-panel monitoring-checks">
<h3>Checks</h3>
{Object.entries(summary.checks).map(([name, value]) => (
<div key={name}>
<span>{name}</span>
<strong className={`check-${value}`}>{value}</strong>
</div>
))}
<small>Updated {new Date(summary.timestamp).toLocaleString()}</small>
</div>
</div>
);
};
const MetricCard: React.FC<{ icon: React.ReactNode; label: string; value: number; danger?: boolean }> = ({ icon, label, value, danger }) => (
<motion.div className={`glass-panel monitoring-card ${danger ? 'is-danger' : ''}`} whileHover={{ y: -3 }}>
{icon}
<div>
<strong>{value}</strong>
<span>{label}</span>
</div>
</motion.div>
);
export default MonitoringView;
|