ghostdrive1 commited on
Commit
44737f9
Β·
1 Parent(s): cde8149

v26: live Projects + Sentinel pages, updated api.js

Browse files
website/src/lib/api.js CHANGED
@@ -1,3 +1,7 @@
 
 
 
 
1
  const API_BASE = import.meta.env.VITE_API_URL || 'https://ultron-api.ghostdriveg1.workers.dev';
2
  const AUTH_TOKEN = import.meta.env.VITE_AUTH_TOKEN || '';
3
 
@@ -6,6 +10,7 @@ async function request(path, options = {}) {
6
  ...options,
7
  headers: {
8
  'Content-Type': 'application/json',
 
9
  Authorization: `Bearer ${AUTH_TOKEN}`,
10
  ...options.headers,
11
  },
@@ -15,31 +20,38 @@ async function request(path, options = {}) {
15
  }
16
 
17
  export const api = {
18
- health: () => request('/api/health'),
 
19
 
 
20
  infer: (payload) =>
21
- request('/api/infer', { method: 'POST', body: JSON.stringify(payload) }),
22
 
 
23
  keys: {
24
- list: () => request('/api/keys'),
25
- add: (key) => request('/api/keys', { method: 'POST', body: JSON.stringify(key) }),
26
- remove: (keyId) => request(`/api/keys/${keyId}`, { method: 'DELETE' }),
27
  },
28
 
 
29
  sentinel: {
30
- trigger: (type) =>
31
- request('/api/sentinel/event', {
32
  method: 'POST',
33
- body: JSON.stringify({ type, source: 'website' }),
 
 
 
34
  }),
35
- reports: () => request('/api/sentinel/reports'),
36
  },
37
 
 
38
  memory: {
39
- stm: (channelId) => request(`/api/memory/stm/${channelId}`),
40
  },
41
 
42
- projects: {
43
- list: () => request('/api/projects'),
 
 
44
  },
45
  };
 
1
+ // website/src/lib/api.js
2
+ // All API calls proxied through CF API Worker β†’ Brain
3
+ // Worker URL: https://ultron-api.ghostdriveg1.workers.dev
4
+
5
  const API_BASE = import.meta.env.VITE_API_URL || 'https://ultron-api.ghostdriveg1.workers.dev';
6
  const AUTH_TOKEN = import.meta.env.VITE_AUTH_TOKEN || '';
7
 
 
10
  ...options,
11
  headers: {
12
  'Content-Type': 'application/json',
13
+ 'X-Ultron-Token': AUTH_TOKEN,
14
  Authorization: `Bearer ${AUTH_TOKEN}`,
15
  ...options.headers,
16
  },
 
20
  }
21
 
22
  export const api = {
23
+ // ── Brain health (includes pool + lifecycle + promoter status) ────────
24
+ health: () => request('/health'),
25
 
26
+ // ── Infer (direct to brain) ───────────────────────────────────────────
27
  infer: (payload) =>
28
+ request('/infer', { method: 'POST', body: JSON.stringify(payload) }),
29
 
30
+ // ── Key pool ──────────────────────────────────────────────────────────
31
  keys: {
32
+ status: () => request('/keys'),
 
 
33
  },
34
 
35
+ // ── Sentinel ──────────────────────────────────────────────────────────
36
  sentinel: {
37
+ trigger: (eventType, payload = {}) =>
38
+ request('/sentinel/event', {
39
  method: 'POST',
40
+ body: JSON.stringify({
41
+ event_type: eventType,
42
+ payload: { source: 'website', ...payload },
43
+ }),
44
  }),
 
45
  },
46
 
47
+ // ── Memory ────────────────────────────────────────────────────────────
48
  memory: {
49
+ stm: (channelId) => request(`/memory/stm/${channelId}`),
50
  },
51
 
52
+ // ── R&D loop history ─────────────────────────────────────────────────
53
+ rd: {
54
+ history: (userId, limit = 30) =>
55
+ request(`/rd/history/${userId}?limit=${limit}`),
56
  },
57
  };
website/src/pages/Projects.jsx CHANGED
@@ -1,30 +1,255 @@
 
 
 
 
 
 
 
1
  import Card from '../components/Card';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  export default function Projects() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  return (
5
  <div className="space-y-6">
6
- <div>
7
- <h1 className="text-2xl font-bold tracking-tight">Projects</h1>
8
- <p className="text-sm text-muted mt-0.5">Every task, deadline, and autonomous R&D session logged here.</p>
 
 
 
 
 
 
 
 
 
 
 
9
  </div>
10
 
11
- <Card title="Project Timeline">
12
- <div className="px-5 py-10 text-center">
13
- <p className="text-3xl mb-4 text-border">β—·</p>
14
- <p className="text-sm font-medium text-ink">No projects yet</p>
15
- <p className="text-xs text-muted mt-1">Give Ultron a task with a deadline via the brain /infer endpoint.</p>
16
- <p className="text-xs text-muted mt-3 max-w-sm mx-auto leading-relaxed">
17
- Each project records: what was built by deadline, autonomous R&D improvements
18
- made after completion, and Sentinel's analysis.
19
- </p>
20
  </div>
21
- </Card>
22
 
23
- <Card title="Autonomous R&D Log">
24
- <div className="px-5 py-6 text-center">
25
- <p className="text-xs text-muted font-mono">Post-deadline improvement sessions will appear here.</p>
26
- <p className="text-xs text-muted mt-1">Powered by rd_loop.py + EverMemOS Foresight signals.</p>
 
 
 
 
27
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </Card>
29
  </div>
30
  );
 
1
+ // website/src/pages/Projects.jsx
2
+ // R&D loop history + improvement timeline
3
+ // Data source: GET /rd/history/{user_id} β†’ rd_loop.py
4
+
5
+ import { useState, useEffect, useCallback } from 'react';
6
+ import { api } from '../lib/api';
7
+ import { relativeTime } from '../lib/utils';
8
  import Card from '../components/Card';
9
+ import Badge from '../components/Badge';
10
+
11
+ const GHOST_UID = '1356180323058057326';
12
+
13
+ const STATUS_VARIANT = {
14
+ implemented: 'green',
15
+ proposed: 'yellow',
16
+ rejected: 'red',
17
+ pending: 'gray',
18
+ };
19
+
20
+ const STATUS_ICON = {
21
+ implemented: 'βœ“',
22
+ proposed: 'β—Œ',
23
+ rejected: 'βœ—',
24
+ pending: 'β—·',
25
+ };
26
+
27
+ function ImprovementRow({ item }) {
28
+ const [expanded, setExpanded] = useState(false);
29
+ const status = item.status || 'pending';
30
+ const ts = item.implemented_at || item.proposed_at || item.created_at;
31
+
32
+ return (
33
+ <div className="border-b border-border last:border-0">
34
+ <button
35
+ onClick={() => setExpanded((v) => !v)}
36
+ className="w-full flex items-start gap-4 py-3.5 text-left hover:bg-bg/50 transition-colors px-5"
37
+ >
38
+ <span
39
+ className={`mt-0.5 text-xs font-mono flex-shrink-0 ${
40
+ status === 'implemented'
41
+ ? 'text-status-green'
42
+ : status === 'rejected'
43
+ ? 'text-status-red'
44
+ : 'text-status-yellow'
45
+ }`}
46
+ >
47
+ {STATUS_ICON[status] || 'β—Œ'}
48
+ </span>
49
+ <div className="flex-1 min-w-0">
50
+ <p className="text-sm font-medium text-ink leading-snug">
51
+ {item.title || item.summary || 'Untitled improvement'}
52
+ </p>
53
+ <p className="text-xs text-muted mt-0.5">
54
+ {item.category || 'general'} Β· {ts ? relativeTime(ts) : 'β€”'}
55
+ </p>
56
+ </div>
57
+ <Badge variant={STATUS_VARIANT[status] || 'gray'}>{status}</Badge>
58
+ <span className="text-xs text-muted ml-2 mt-0.5">{expanded ? 'β–²' : 'β–Ό'}</span>
59
+ </button>
60
+ {expanded && (
61
+ <div className="px-14 pb-4 space-y-2">
62
+ {item.description && (
63
+ <p className="text-xs text-muted leading-relaxed">{item.description}</p>
64
+ )}
65
+ {item.files_changed && item.files_changed.length > 0 && (
66
+ <div className="flex flex-wrap gap-1.5">
67
+ {item.files_changed.map((f) => (
68
+ <span
69
+ key={f}
70
+ className="font-mono text-[10px] bg-bg border border-border px-2 py-0.5 rounded"
71
+ >
72
+ {f}
73
+ </span>
74
+ ))}
75
+ </div>
76
+ )}
77
+ {item.sentinel_verdict && (
78
+ <p className="text-xs text-muted border-l-2 border-border pl-3 italic">
79
+ Sentinel: {item.sentinel_verdict}
80
+ </p>
81
+ )}
82
+ {item.council_synthesis && (
83
+ <p className="text-xs text-muted border-l-2 border-status-blue pl-3">
84
+ Council: {item.council_synthesis}
85
+ </p>
86
+ )}
87
+ </div>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function RdStatBar({ label, value, total, color }) {
94
+ const pct = total > 0 ? Math.round((value / total) * 100) : 0;
95
+ return (
96
+ <div className="space-y-1.5">
97
+ <div className="flex justify-between text-[11px]">
98
+ <span className="text-muted font-mono">{label}</span>
99
+ <span className="font-mono text-ink">
100
+ {value}
101
+ <span className="text-muted">/{total}</span>
102
+ </span>
103
+ </div>
104
+ <div className="h-1 bg-bg rounded-full overflow-hidden">
105
+ <div
106
+ className="h-full rounded-full transition-all duration-700"
107
+ style={{ width: `${pct}%`, backgroundColor: color }}
108
+ />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
113
 
114
  export default function Projects() {
115
+ const [data, setData] = useState(null);
116
+ const [loading, setLoading] = useState(true);
117
+ const [error, setError] = useState(null);
118
+ const [lastFetch, setLastFetch] = useState(null);
119
+ const [filter, setFilter] = useState('all');
120
+
121
+ const load = useCallback(async () => {
122
+ setLoading(true);
123
+ setError(null);
124
+ try {
125
+ const res = await api.rd.history(GHOST_UID, 50);
126
+ setData(res);
127
+ setLastFetch(new Date());
128
+ } catch (e) {
129
+ setError(e.message);
130
+ } finally {
131
+ setLoading(false);
132
+ }
133
+ }, []);
134
+
135
+ useEffect(() => {
136
+ load();
137
+ }, [load]);
138
+
139
+ const improvements = data?.improvements ?? [];
140
+ const rdState = data?.rd_state ?? null;
141
+ const total = improvements.length;
142
+ const implemented = improvements.filter((i) => i.status === 'implemented').length;
143
+ const proposed = improvements.filter((i) => i.status === 'proposed').length;
144
+ const rejected = improvements.filter((i) => i.status === 'rejected').length;
145
+
146
+ const filtered =
147
+ filter === 'all'
148
+ ? improvements
149
+ : improvements.filter((i) => (i.status || 'pending') === filter);
150
+
151
  return (
152
  <div className="space-y-6">
153
+ <div className="flex items-center justify-between">
154
+ <div>
155
+ <h1 className="text-2xl font-bold tracking-tight">Projects</h1>
156
+ <p className="text-sm text-muted mt-0.5">
157
+ {lastFetch ? `Refreshed ${relativeTime(lastFetch)}` : 'Loading R&D history...'}
158
+ </p>
159
+ </div>
160
+ <button
161
+ onClick={load}
162
+ disabled={loading}
163
+ className="text-xs font-mono text-muted hover:text-ink border border-border px-3 py-1.5 rounded transition-colors disabled:opacity-40"
164
+ >
165
+ {loading ? '...' : '↻ Refresh'}
166
+ </button>
167
  </div>
168
 
169
+ {error && (
170
+ <div className="border border-red-200 bg-red-50 text-status-red text-xs px-4 py-3 rounded-lg font-mono">
171
+ βœ— {error} β€” RDLoop requires Redis + Brain running.
 
 
 
 
 
 
172
  </div>
173
+ )}
174
 
175
+ <div className="grid grid-cols-3 gap-4">
176
+ <div className="bg-surface border border-border rounded-lg p-5">
177
+ <p className="text-[11px] text-muted font-mono uppercase tracking-widest mb-3">Progress</p>
178
+ <div className="space-y-3">
179
+ <RdStatBar label="implemented" value={implemented} total={total || 1} color="#16A34A" />
180
+ <RdStatBar label="proposed" value={proposed} total={total || 1} color="#CA8A04" />
181
+ <RdStatBar label="rejected" value={rejected} total={total || 1} color="#DC2626" />
182
+ </div>
183
  </div>
184
+ <div className="bg-surface border border-border rounded-lg p-5 col-span-2">
185
+ <p className="text-[11px] text-muted font-mono uppercase tracking-widest mb-3">R&D State</p>
186
+ {rdState ? (
187
+ <div className="grid grid-cols-2 gap-x-8 gap-y-2">
188
+ {[
189
+ ['Loop status', rdState.status || 'idle'],
190
+ ['Proposals total', rdState.total_proposals ?? 'β€”'],
191
+ ['Accept rate', rdState.accept_rate ? `${Math.round(rdState.accept_rate * 100)}%` : 'β€”'],
192
+ ['Last run', rdState.last_run_at ? relativeTime(rdState.last_run_at) : 'never'],
193
+ ['Active goal', rdState.active_goal || 'none'],
194
+ ['Foresight signals', rdState.foresight_signal_count ?? 'β€”'],
195
+ ].map(([k, v]) => (
196
+ <div key={k} className="flex gap-3">
197
+ <span className="text-[11px] font-mono text-muted w-32 flex-shrink-0">{k}</span>
198
+ <span className="text-[11px] font-mono text-ink truncate">{String(v)}</span>
199
+ </div>
200
+ ))}
201
+ </div>
202
+ ) : (
203
+ <p className="text-xs text-muted font-mono">
204
+ {loading ? 'Loading...' : 'No RD state β€” RDLoop requires Redis.'}
205
+ </p>
206
+ )}
207
+ </div>
208
+ </div>
209
+
210
+ <div className="flex gap-2">
211
+ {['all', 'implemented', 'proposed', 'rejected'].map((f) => (
212
+ <button
213
+ key={f}
214
+ onClick={() => setFilter(f)}
215
+ className={`text-xs font-mono px-3 py-1.5 rounded border transition-colors ${
216
+ filter === f
217
+ ? 'bg-ink text-white border-ink'
218
+ : 'border-border text-muted hover:text-ink hover:border-ink/30'
219
+ }`}
220
+ >
221
+ {f}
222
+ <span className="ml-1.5 opacity-60">
223
+ {f === 'all' ? total : f === 'implemented' ? implemented : f === 'proposed' ? proposed : rejected}
224
+ </span>
225
+ </button>
226
+ ))}
227
+ </div>
228
+
229
+ <Card title="Autonomous R&D Log">
230
+ {loading ? (
231
+ <div className="px-5 py-10 text-center">
232
+ <p className="text-xs text-muted font-mono">Loading improvements...</p>
233
+ </div>
234
+ ) : filtered.length === 0 ? (
235
+ <div className="px-5 py-10 text-center space-y-2">
236
+ <p className="text-3xl text-border">β—·</p>
237
+ <p className="text-sm font-medium text-ink">
238
+ {filter === 'all' ? 'No improvements yet' : `No ${filter} improvements`}
239
+ </p>
240
+ <p className="text-xs text-muted max-w-sm mx-auto leading-relaxed">
241
+ {filter === 'all'
242
+ ? 'Give Ultron a task. After completion the R&D loop proposes autonomous improvements β€” Sentinel ranks, Council debates, best ones get implemented.'
243
+ : 'Switch to "all" to see everything.'}
244
+ </p>
245
+ </div>
246
+ ) : (
247
+ <div>
248
+ {filtered.map((item, i) => (
249
+ <ImprovementRow key={item.id || i} item={item} />
250
+ ))}
251
+ </div>
252
+ )}
253
  </Card>
254
  </div>
255
  );
website/src/pages/Sentinel.jsx CHANGED
@@ -1,13 +1,95 @@
1
- import { useState } from 'react';
 
 
 
 
2
  import { api } from '../lib/api';
 
3
  import Card from '../components/Card';
4
  import Badge from '../components/Badge';
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  export default function Sentinel() {
 
 
 
 
7
  const [triggering, setTriggering] = useState(false);
8
  const [triggered, setTriggered] = useState(false);
9
  const [triggerError, setTriggerError] = useState(null);
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  const triggerAudit = async () => {
12
  setTriggering(true);
13
  setTriggerError(null);
@@ -22,52 +104,119 @@ export default function Sentinel() {
22
  }
23
  };
24
 
 
 
 
 
 
 
 
25
  return (
26
  <div className="space-y-6">
27
  <div className="flex items-center justify-between">
28
  <div>
29
  <h1 className="text-2xl font-bold tracking-tight">Sentinel</h1>
30
- <p className="text-sm text-muted mt-0.5">God Layer. Gemini 2.5 Pro. Dedicated key. Watches everything.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  </div>
32
- <button
33
- onClick={triggerAudit}
34
- disabled={triggering}
35
- className="text-sm font-mono px-4 py-2 border border-border rounded hover:bg-bg transition-colors disabled:opacity-40"
36
- >
37
- {triggering ? '...' : triggered ? 'βœ“ Triggered' : 'Trigger Audit'}
38
- </button>
39
  </div>
40
 
 
 
 
 
 
41
  {triggerError && (
42
  <div className="text-xs font-mono text-status-red border border-red-200 bg-red-50 px-4 py-3 rounded-lg">
43
- βœ— {triggerError}
44
  </div>
45
  )}
46
 
47
  <div className="grid grid-cols-3 gap-4">
48
  {[
49
- { label: 'Authority', value: 'God Layer', sub: 'Above all modules. Final word.' },
50
- { label: 'Model', value: 'Gemini 2.5 Pro', sub: 'Dedicated. Never shared with pool.' },
51
- { label: 'Schedule', value: 'Sunday 23:59', sub: 'Weekly audit + incident detection' },
52
- ].map(({ label, value, sub }) => (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  <div key={label} className="bg-surface border border-border rounded-lg p-5">
54
  <p className="text-[11px] text-muted font-mono uppercase tracking-widest mb-2">{label}</p>
55
- <p className="text-base font-bold text-ink">{value}</p>
 
 
 
56
  <p className="text-xs text-muted mt-1">{sub}</p>
57
  </div>
58
  ))}
59
  </div>
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  <Card title="Trigger Schedule">
62
  <div className="px-5 py-5 space-y-0">
63
  {[
64
- { when: 'Every request', what: 'Reads KV routing table. Validates Space health. <50ms.' },
65
- { when: 'Every failure', what: 'Instant incident analysis β†’ Notion page β†’ KV rerouting β†’ Discord DM to Ghost.' },
66
- { when: 'Every Sunday 23:59', what: '1M context window reads full week logs β†’ structured report β†’ Notion + Discord.' },
67
  { when: 'Project start', what: 'Reads brief β†’ writes operational plan: DevOps, memory arch, MOA config, tool assignments.' },
68
  ].map(({ when, what }) => (
69
- <div key={when} className="flex gap-5 py-3 border-b border-border last:border-0">
70
- <div className="w-40 text-xs font-mono text-ink font-medium flex-shrink-0 pt-0.5">{when}</div>
71
  <div className="text-xs text-muted leading-relaxed">{what}</div>
72
  </div>
73
  ))}
@@ -78,6 +227,7 @@ export default function Sentinel() {
78
  <Card title="Incident Log">
79
  <div className="px-5 py-6 text-center">
80
  <p className="text-xs text-muted font-mono">No incidents. System stable.</p>
 
81
  </div>
82
  </Card>
83
  <Card title="Weekly Reports">
 
1
+ // website/src/pages/Sentinel.jsx
2
+ // God Layer dashboard: SpacePromoter live topology + audit trigger
3
+ // Data: GET /health (promoter field, refreshed every 30s)
4
+
5
+ import { useState, useEffect, useCallback } from 'react';
6
  import { api } from '../lib/api';
7
+ import { relativeTime } from '../lib/utils';
8
  import Card from '../components/Card';
9
  import Badge from '../components/Badge';
10
 
11
+ function NodeCard({ name, role, url, isAlive, lastStatus, consecutiveFailures, consecutiveSuccesses, lastCheckAt }) {
12
+ const ok = lastStatus === 'ok';
13
+ const unknown = lastStatus === 'unknown';
14
+ return (
15
+ <div className="bg-surface border border-border rounded-lg p-5">
16
+ <div className="flex items-center justify-between mb-3">
17
+ <div className="flex items-center gap-2">
18
+ <span
19
+ className={`w-2 h-2 rounded-full flex-shrink-0 ${
20
+ unknown ? 'bg-border' : ok ? 'bg-status-green' : 'bg-status-red'
21
+ }`}
22
+ />
23
+ <span className="text-xs font-mono font-medium text-ink">{name}</span>
24
+ </div>
25
+ <Badge variant={role === 'primary' ? 'green' : role === 'backup' ? 'yellow' : 'gray'}>
26
+ {role}
27
+ </Badge>
28
+ </div>
29
+ <p className="text-[11px] font-mono text-muted truncate mb-3" title={url}>
30
+ {url || 'β€”'}
31
+ </p>
32
+ <div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
33
+ <div>
34
+ <p className="text-[10px] text-muted font-mono">STATUS</p>
35
+ <p className={`text-xs font-mono font-medium ${
36
+ ok ? 'text-status-green' : unknown ? 'text-muted' : 'text-status-red'
37
+ }`}>
38
+ {lastStatus}
39
+ </p>
40
+ </div>
41
+ <div>
42
+ <p className="text-[10px] text-muted font-mono">ALIVE</p>
43
+ <p className={`text-xs font-mono font-medium ${isAlive ? 'text-status-green' : 'text-status-red'}`}>
44
+ {isAlive ? 'yes' : 'no'}
45
+ </p>
46
+ </div>
47
+ <div>
48
+ <p className="text-[10px] text-muted font-mono">FAILS</p>
49
+ <p className={`text-xs font-mono font-medium ${consecutiveFailures > 0 ? 'text-status-red' : 'text-ink'}`}>
50
+ {consecutiveFailures}
51
+ </p>
52
+ </div>
53
+ <div>
54
+ <p className="text-[10px] text-muted font-mono">OK STREAK</p>
55
+ <p className="text-xs font-mono font-medium text-ink">{consecutiveSuccesses}</p>
56
+ </div>
57
+ </div>
58
+ {lastCheckAt && (
59
+ <p className="text-[10px] text-muted font-mono mt-3">Checked {relativeTime(lastCheckAt)}</p>
60
+ )}
61
+ </div>
62
+ );
63
+ }
64
+
65
  export default function Sentinel() {
66
+ const [health, setHealth] = useState(null);
67
+ const [loading, setLoading] = useState(true);
68
+ const [error, setError] = useState(null);
69
+ const [lastFetch, setLastFetch] = useState(null);
70
  const [triggering, setTriggering] = useState(false);
71
  const [triggered, setTriggered] = useState(false);
72
  const [triggerError, setTriggerError] = useState(null);
73
 
74
+ const fetchHealth = useCallback(async () => {
75
+ try {
76
+ const data = await api.health();
77
+ setHealth(data);
78
+ setLastFetch(new Date());
79
+ setError(null);
80
+ } catch (e) {
81
+ setError(e.message);
82
+ } finally {
83
+ setLoading(false);
84
+ }
85
+ }, []);
86
+
87
+ useEffect(() => {
88
+ fetchHealth();
89
+ const iv = setInterval(fetchHealth, 30_000);
90
+ return () => clearInterval(iv);
91
+ }, [fetchHealth]);
92
+
93
  const triggerAudit = async () => {
94
  setTriggering(true);
95
  setTriggerError(null);
 
104
  }
105
  };
106
 
107
+ const promoter = health?.promoter ?? null;
108
+ const promoterActive = health?.promoter_active ?? false;
109
+ const sentinelActive = health?.sentinel_active ?? false;
110
+ const nodes = promoter?.nodes ? Object.values(promoter.nodes) : [];
111
+ const failoverCount = promoter?.failover_count ?? 0;
112
+ const inDegraded = promoter?.in_degraded_state ?? false;
113
+
114
  return (
115
  <div className="space-y-6">
116
  <div className="flex items-center justify-between">
117
  <div>
118
  <h1 className="text-2xl font-bold tracking-tight">Sentinel</h1>
119
+ <p className="text-sm text-muted mt-0.5">
120
+ {lastFetch ? `Refreshed ${relativeTime(lastFetch)}` : 'Connecting...'}
121
+ </p>
122
+ </div>
123
+ <div className="flex items-center gap-2">
124
+ <button
125
+ onClick={fetchHealth}
126
+ disabled={loading}
127
+ className="text-xs font-mono text-muted hover:text-ink border border-border px-3 py-1.5 rounded transition-colors disabled:opacity-40"
128
+ >
129
+ {loading ? '...' : '↻'}
130
+ </button>
131
+ <button
132
+ onClick={triggerAudit}
133
+ disabled={triggering}
134
+ className="text-sm font-mono px-4 py-2 border border-border rounded hover:bg-bg transition-colors disabled:opacity-40"
135
+ >
136
+ {triggering ? '...' : triggered ? 'βœ“ Triggered' : 'Trigger Audit'}
137
+ </button>
138
  </div>
 
 
 
 
 
 
 
139
  </div>
140
 
141
+ {error && (
142
+ <div className="text-xs font-mono text-status-red border border-red-200 bg-red-50 px-4 py-3 rounded-lg">
143
+ βœ— {error}
144
+ </div>
145
+ )}
146
  {triggerError && (
147
  <div className="text-xs font-mono text-status-red border border-red-200 bg-red-50 px-4 py-3 rounded-lg">
148
+ βœ— Trigger failed: {triggerError}
149
  </div>
150
  )}
151
 
152
  <div className="grid grid-cols-3 gap-4">
153
  {[
154
+ {
155
+ label: 'God Layer',
156
+ value: sentinelActive ? 'Active' : 'Inactive',
157
+ sub: sentinelActive ? 'Gemini 2.5 Pro Β· dedicated key' : 'Set GEMINI_SENTINEL_KEY in HF Space',
158
+ ok: sentinelActive,
159
+ },
160
+ {
161
+ label: 'SpacePromoter',
162
+ value: promoterActive ? (inDegraded ? 'Degraded' : `${nodes.length} node${nodes.length !== 1 ? 's' : ''}`) : 'Inactive',
163
+ sub: promoterActive
164
+ ? `${failoverCount} failover${failoverCount !== 1 ? 's' : ''} Β· 30s check interval`
165
+ : 'No Redis or init failed',
166
+ ok: promoterActive && !inDegraded,
167
+ },
168
+ {
169
+ label: 'Weekly Audit',
170
+ value: 'Sunday 23:59',
171
+ sub: 'GitHub Actions cron β†’ /sentinel/event',
172
+ ok: true,
173
+ },
174
+ ].map(({ label, value, sub, ok }) => (
175
  <div key={label} className="bg-surface border border-border rounded-lg p-5">
176
  <p className="text-[11px] text-muted font-mono uppercase tracking-widest mb-2">{label}</p>
177
+ <div className="flex items-center gap-2">
178
+ <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${ok ? 'bg-status-green' : 'bg-status-red'}`} />
179
+ <p className="text-base font-bold text-ink">{value}</p>
180
+ </div>
181
  <p className="text-xs text-muted mt-1">{sub}</p>
182
  </div>
183
  ))}
184
  </div>
185
 
186
+ {promoterActive && nodes.length > 0 && (
187
+ <div>
188
+ <h2 className="text-xs font-mono text-muted uppercase tracking-widest mb-3">Live Space Topology</h2>
189
+ <div className={`grid gap-4 ${nodes.length === 1 ? 'grid-cols-1' : nodes.length === 2 ? 'grid-cols-2' : 'grid-cols-3'}`}>
190
+ {nodes.map((node) => (
191
+ <NodeCard key={node.name} {...node} />
192
+ ))}
193
+ </div>
194
+ {inDegraded && (
195
+ <div className="mt-3 text-xs font-mono text-status-red border border-red-200 bg-red-50 px-4 py-3 rounded-lg">
196
+ 🚨 DEGRADED β€” max failovers reached or all nodes down. Manual intervention required.
197
+ </div>
198
+ )}
199
+ </div>
200
+ )}
201
+
202
+ {promoterActive && nodes.length === 0 && (
203
+ <Card title="Space Topology">
204
+ <div className="px-5 py-6 text-center">
205
+ <p className="text-xs text-muted font-mono">No nodes. Set BRAIN_PRIMARY_URL env var.</p>
206
+ </div>
207
+ </Card>
208
+ )}
209
+
210
  <Card title="Trigger Schedule">
211
  <div className="px-5 py-5 space-y-0">
212
  {[
213
+ { when: 'Every 30s', what: 'SpacePromoter pings all Space /health endpoints. 3 consecutive failures β†’ backup promoted β†’ CF KV updated.' },
214
+ { when: 'Every failure', what: 'Sentinel instant analysis β†’ Notion incident page β†’ KV rerouting β†’ Discord DM.' },
215
+ { when: 'Sunday 23:59', what: '1M context window reads full week logs β†’ structured report β†’ Notion + Discord. Triggered by GitHub Actions.' },
216
  { when: 'Project start', what: 'Reads brief β†’ writes operational plan: DevOps, memory arch, MOA config, tool assignments.' },
217
  ].map(({ when, what }) => (
218
+ <div key={when} className="flex gap-5 py-3.5 border-b border-border last:border-0">
219
+ <div className="w-36 text-xs font-mono text-ink font-medium flex-shrink-0 pt-0.5">{when}</div>
220
  <div className="text-xs text-muted leading-relaxed">{what}</div>
221
  </div>
222
  ))}
 
227
  <Card title="Incident Log">
228
  <div className="px-5 py-6 text-center">
229
  <p className="text-xs text-muted font-mono">No incidents. System stable.</p>
230
+ <p className="text-xs text-muted mt-1">SpacePromoter events appear here in real time.</p>
231
  </div>
232
  </Card>
233
  <Card title="Weekly Reports">