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;