File size: 10,634 Bytes
44737f9
37f231b
44737f9
1720172
44737f9
1720172
 
 
37f231b
 
 
 
 
 
 
 
 
 
44737f9
37f231b
 
 
 
 
44737f9
 
37f231b
 
 
 
 
 
44737f9
37f231b
 
 
44737f9
37f231b
 
 
 
44737f9
 
 
37f231b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44737f9
 
 
 
1720172
44737f9
37f231b
44737f9
37f231b
1720172
37f231b
1720172
 
44737f9
37f231b
 
 
 
 
 
44737f9
37f231b
 
 
 
44737f9
 
 
 
37f231b
44737f9
 
37f231b
44737f9
37f231b
1720172
 
 
37f231b
 
 
1720172
 
 
 
 
 
 
44737f9
37f231b
 
 
44737f9
1720172
 
37f231b
1720172
 
37f231b
44737f9
37f231b
44737f9
37f231b
 
44737f9
37f231b
44737f9
 
37f231b
44737f9
 
 
37f231b
44737f9
1720172
 
 
 
 
37f231b
1720172
 
 
 
 
44737f9
37f231b
 
 
 
44737f9
 
 
37f231b
 
 
 
 
44737f9
 
37f231b
 
 
 
44737f9
37f231b
1720172
37f231b
 
 
44737f9
37f231b
1720172
 
 
 
37f231b
 
 
 
 
 
 
 
44737f9
37f231b
 
 
 
 
44737f9
37f231b
 
44737f9
1720172
37f231b
1720172
37f231b
 
 
1720172
 
37f231b
44737f9
1720172
 
 
 
 
 
37f231b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1720172
37f231b
 
 
 
 
 
 
 
 
 
 
 
1720172
37f231b
1720172
37f231b
 
 
 
 
 
 
 
 
 
1720172
 
 
 
 
 
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// website/src/pages/Sentinel.jsx
// Live: SpacePromoter topology (from /health), infra event log, trigger buttons
import { useState, useEffect, useCallback } from 'react';
import { api } from '../lib/api';
import { relativeTime } from '../lib/utils';
import Card from '../components/Card';
import Badge from '../components/Badge';

const EVENT_COLORS = {
  promotion: 'yellow',
  recovery: 'green',
  health_degraded: 'red',
  kv_write_failed: 'red',
};

function NodeCard({ name, node }) {
  const alive = node?.is_alive;
  const status = node?.last_status;
  return (
    <div className="bg-surface border border-border rounded-lg p-5 space-y-2">
      <div className="flex items-center justify-between">
        <p className="text-[11px] font-mono text-muted uppercase tracking-widest">{node?.role ?? name}</p>
        <Badge variant={alive ? (status === 'ok' ? 'green' : 'yellow') : 'red'} dot>
          {alive ? (status === 'ok' ? 'healthy' : 'degraded') : 'down'}
        </Badge>
      </div>
      <p className="text-sm font-medium text-ink break-all">{node?.name ?? name}</p>
      <p className="text-[11px] font-mono text-muted break-all">{node?.url ?? 'β€”'}</p>
      <div className="flex gap-4 pt-1">
        <div className="text-center">
          <p className="text-[10px] font-mono text-muted">failures</p>
          <p className="text-sm font-bold text-ink">{node?.consecutive_failures ?? 0}</p>
        </div>
        <div className="text-center">
          <p className="text-[10px] font-mono text-muted">successes</p>
          <p className="text-sm font-bold text-ink">{node?.consecutive_successes ?? 0}</p>
        </div>
        <div className="flex-1 text-right">
          <p className="text-[10px] font-mono text-muted">last check</p>
          <p className="text-xs font-mono text-muted">
            {node?.last_check_at ? relativeTime(node.last_check_at) : 'β€”'}
          </p>
        </div>
      </div>
    </div>
  );
}

function InfraEvent({ ev }) {
  let parsed;
  try { parsed = typeof ev === 'string' ? JSON.parse(ev) : ev; } catch { parsed = { event_type: 'unknown', message: String(ev) }; }
  return (
    <div className="flex gap-4 py-3 border-b border-border last:border-0">
      <Badge variant={EVENT_COLORS[parsed.event_type] ?? 'gray'} dot>
        {parsed.event_type ?? '?'}
      </Badge>
      <div className="flex-1 min-w-0">
        <p className="text-xs text-ink leading-snug">{parsed.message ?? 'β€”'}</p>
        {parsed.timestamp && (
          <p className="text-[11px] font-mono text-muted mt-0.5">{relativeTime(parsed.timestamp)}</p>
        )}
      </div>
    </div>
  );
}

export default function Sentinel() {
  const [health, setHealth] = useState(null);
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [evLoading, setEvLoading] = useState(true);
  const [triggering, setTriggering] = useState(false);
  const [triggered, setTriggered] = useState(null);
  const [triggerError, setTriggerError] = useState(null);

  const fetchHealth = useCallback(async () => {
    try { const d = await api.health(); setHealth(d); } catch { /* offline */ }
    finally { setLoading(false); }
  }, []);

  const fetchEvents = useCallback(async () => {
    setEvLoading(true);
    try {
      const d = await api.infra.events();
      setEvents(Array.isArray(d) ? d : d?.events ?? []);
    } catch { setEvents([]); }
    finally { setEvLoading(false); }
  }, []);

  useEffect(() => {
    fetchHealth();
    fetchEvents();
    const iv = setInterval(fetchHealth, 30_000);
    return () => clearInterval(iv);
  }, [fetchHealth, fetchEvents]);

  const triggerEvent = async (type) => {
    setTriggering(true);
    setTriggerError(null);
    try {
      await api.sentinel.trigger(type);
      setTriggered(type);
      setTimeout(() => setTriggered(null), 4000);
    } catch (e) {
      setTriggerError(e.message);
    } finally {
      setTriggering(false);
    }
  };

  const promoter = health?.promoter ?? null;
  const nodes = promoter?.nodes ?? {};
  const sentinelActive = health?.sentinel_active;
  const promoterActive = health?.promoter_active;

  return (
    <div className="space-y-6">
      <div className="flex items-start justify-between">
        <div>
          <h1 className="text-2xl font-bold tracking-tight">Sentinel</h1>
          <p className="text-sm text-muted mt-0.5">God Layer. Gemini 2.5 Pro. Dedicated key. Watches everything.</p>
        </div>
        <div className="flex gap-2">
          <button
            onClick={() => { fetchHealth(); fetchEvents(); }}
            className="text-xs font-mono text-muted hover:text-ink border border-border px-3 py-1.5 rounded transition-colors"
          >
            ↻ Refresh
          </button>
          <button
            onClick={() => triggerEvent('weekly_audit')}
            disabled={triggering}
            className="text-sm font-mono px-4 py-2 border border-border rounded hover:bg-bg transition-colors disabled:opacity-40"
          >
            {triggering ? '...' : triggered === 'weekly_audit' ? 'βœ“ Triggered' : 'Trigger Audit'}
          </button>
        </div>
      </div>

      {triggerError && (
        <div className="text-xs font-mono text-status-red border border-red-200 bg-red-50 px-4 py-3 rounded-lg">
          βœ— {triggerError}
        </div>
      )}

      <div className="grid grid-cols-3 gap-4">
        {[
          {
            label: 'Sentinel',
            value: loading ? 'β€”' : (sentinelActive ? 'Active' : 'Inactive'),
            sub: 'Gemini 2.5 Pro. Dedicated key.',
            variant: loading ? 'gray' : (sentinelActive ? 'green' : 'red'),
          },
          {
            label: 'SpacePromoter',
            value: loading ? 'β€”' : (promoterActive ? 'Running' : 'Off'),
            sub: promoter
              ? `${promoter.check_interval_seconds}s interval Β· failovers: ${promoter.failover_count ?? 0}`
              : 'Health loop + CF KV promotion',
            variant: loading ? 'gray' : (promoterActive ? 'green' : 'gray'),
          },
          {
            label: 'Degraded?',
            value: promoter?.in_degraded_state ? 'YES' : 'No',
            sub: `Max failovers: ${promoter?.max_failover_attempts ?? 5}`,
            variant: promoter?.in_degraded_state ? 'red' : 'green',
          },
        ].map(({ label, value, sub, variant }) => (
          <div key={label} className="bg-surface border border-border rounded-lg p-5">
            <div className="flex items-center justify-between mb-2">
              <p className="text-[11px] text-muted font-mono uppercase tracking-widest">{label}</p>
              <Badge variant={variant} dot>{value}</Badge>
            </div>
            <p className="text-xs text-muted leading-relaxed">{sub}</p>
          </div>
        ))}
      </div>

      <div>
        <h2 className="text-xs font-mono text-muted uppercase tracking-widest mb-3">Space Topology</h2>
        {loading ? (
          <div className="text-xs font-mono text-muted py-6 text-center">Loading topology...</div>
        ) : Object.keys(nodes).length === 0 ? (
          <div className="bg-surface border border-border rounded-lg px-5 py-8 text-center">
            <p className="text-sm text-muted">No topology data.</p>
            <p className="text-xs font-mono text-muted mt-1">SpacePromoter inactive or Brain offline.</p>
          </div>
        ) : (
          <div className="grid grid-cols-2 gap-4">
            {Object.entries(nodes).map(([name, node]) => (
              <NodeCard key={name} name={name} node={node} />
            ))}
          </div>
        )}
      </div>

      <Card title="Trigger Schedule">
        <div className="px-5 py-2 space-y-0">
          {[
            { when: 'Every request', what: 'Reads KV routing table. Validates Space health. <50ms.' },
            { when: 'Every failure', what: 'Instant incident analysis β†’ Notion page β†’ KV rerouting β†’ Discord DM.' },
            { when: 'Sunday 23:59', what: '1M context window reads full week logs β†’ structured report β†’ Notion + Discord.' },
            { when: 'Project start', what: 'Reads brief β†’ writes operational plan: DevOps, memory arch, MOA config, tool assignments.' },
          ].map(({ when, what }) => (
            <div key={when} className="flex gap-5 py-3 border-b border-border last:border-0">
              <div className="w-36 text-xs font-mono text-ink font-medium flex-shrink-0 pt-0.5">{when}</div>
              <div className="text-xs text-muted leading-relaxed">{what}</div>
            </div>
          ))}
        </div>
      </Card>

      <Card title="Manual Actions">
        <div className="px-5 py-4 flex flex-wrap gap-3">
          {[
            { label: 'Trigger Audit', type: 'weekly_audit' },
            { label: 'Health Check', type: 'health_check' },
            { label: 'Force KV Sync', type: 'routing_override' },
          ].map(({ label, type }) => (
            <button
              key={type}
              onClick={() => triggerEvent(type)}
              disabled={triggering}
              className="text-xs font-mono px-4 py-2 border border-border rounded hover:bg-bg transition-colors disabled:opacity-40"
            >
              {triggering ? '...' : triggered === type ? `βœ“ ${label}` : label}
            </button>
          ))}
        </div>
      </Card>

      <div className="grid grid-cols-2 gap-4">
        <Card title="Infrastructure Events">
          {evLoading ? (
            <div className="px-5 py-6 text-xs font-mono text-muted text-center">Loading...</div>
          ) : events.length === 0 ? (
            <div className="px-5 py-6 text-center">
              <p className="text-xs font-mono text-muted">No events. System stable.</p>
            </div>
          ) : (
            <div className="px-5 py-2">
              {events.slice(-20).reverse().map((ev, i) => <InfraEvent key={i} ev={ev} />)}
            </div>
          )}
        </Card>

        <Card title="Weekly Reports">
          <div className="px-5 py-6 text-center space-y-3">
            <p className="text-xs font-mono text-muted">No reports yet.</p>
            <p className="text-xs text-muted">First report: this Sunday 23:59 UTC.</p>
            <button
              onClick={() => triggerEvent('weekly_audit')}
              disabled={triggering}
              className="text-xs font-mono px-4 py-2 border border-border rounded hover:bg-bg transition-colors disabled:opacity-40"
            >
              Force now
            </button>
          </div>
        </Card>
      </div>
    </div>
  );
}