total-recall-brain 3.1.1 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/extension/background.js +99 -0
  2. package/extension/content-script.css +16 -0
  3. package/extension/content-script.js +134 -0
  4. package/extension/icons/README.md +27 -0
  5. package/extension/icons/icon-128.png +0 -0
  6. package/extension/icons/icon-16.png +0 -0
  7. package/extension/icons/icon-48.png +0 -0
  8. package/extension/lib/brain-client.js +47 -0
  9. package/extension/manifest.json +39 -0
  10. package/extension/options/options.css +218 -0
  11. package/extension/options/options.html +72 -0
  12. package/extension/options/options.js +99 -0
  13. package/extension/popup/popup.css +221 -0
  14. package/extension/popup/popup.html +44 -0
  15. package/extension/popup/popup.js +107 -0
  16. package/extension/sidepanel/sidepanel.css +340 -0
  17. package/extension/sidepanel/sidepanel.html +38 -0
  18. package/extension/sidepanel/sidepanel.js +229 -0
  19. package/frontend/README.md +73 -0
  20. package/frontend/index.html +17 -0
  21. package/frontend/package.json +31 -0
  22. package/frontend/src/App.css +2 -0
  23. package/frontend/src/App.tsx +390 -0
  24. package/frontend/src/api.ts +688 -0
  25. package/frontend/src/assets/hero.png +0 -0
  26. package/frontend/src/assets/react.svg +1 -0
  27. package/frontend/src/assets/vite.svg +1 -0
  28. package/frontend/src/components/BrainSelector.tsx +272 -0
  29. package/frontend/src/components/DaemonLogsTab.tsx +54 -0
  30. package/frontend/src/components/Graph3D.tsx +850 -0
  31. package/frontend/src/components/MarkdownUtils.tsx +99 -0
  32. package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
  33. package/frontend/src/components/TaskQueueTab.tsx +185 -0
  34. package/frontend/src/index.css +1589 -0
  35. package/frontend/src/main.tsx +10 -0
  36. package/frontend/src/pages/ApiKeysPage.tsx +288 -0
  37. package/frontend/src/pages/AutomationsPage.tsx +354 -0
  38. package/frontend/src/pages/ChatPage.tsx +806 -0
  39. package/frontend/src/pages/DeploymentsPage.tsx +275 -0
  40. package/frontend/src/pages/FilesPage.tsx +384 -0
  41. package/frontend/src/pages/HealthPage.tsx +91 -0
  42. package/frontend/src/pages/IntegrationsPage.tsx +382 -0
  43. package/frontend/src/pages/LoginPage.tsx +237 -0
  44. package/frontend/src/pages/MemoryPage.tsx +1192 -0
  45. package/frontend/src/pages/SandboxPage.tsx +113 -0
  46. package/frontend/src/pages/SettingsPage.tsx +662 -0
  47. package/frontend/src/pages/SkillsPage.tsx +618 -0
  48. package/frontend/src/pages/TasksPage.tsx +392 -0
  49. package/frontend/src/pages/UsagePage.tsx +260 -0
  50. package/frontend/src/types.ts +163 -0
  51. package/frontend/tsconfig.app.json +25 -0
  52. package/frontend/tsconfig.json +7 -0
  53. package/frontend/tsconfig.node.json +24 -0
  54. package/frontend/vite.config.ts +24 -0
  55. package/package.json +8 -1
  56. package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
  57. package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
  58. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
  59. package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
  60. package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
  61. package/src/cli/remember.mjs +1 -1
  62. package/src/core/research-queue.mjs +5 -1
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "react": "^19.2.6",
14
+ "react-dom": "^19.2.6",
15
+ "react-router-dom": "^7.15.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^10.0.1",
19
+ "@types/node": "^24.12.3",
20
+ "@types/react": "^19.2.14",
21
+ "@types/react-dom": "^19.2.3",
22
+ "@vitejs/plugin-react": "^6.0.1",
23
+ "eslint": "^10.3.0",
24
+ "eslint-plugin-react-hooks": "^7.1.1",
25
+ "eslint-plugin-react-refresh": "^0.5.2",
26
+ "globals": "^17.6.0",
27
+ "typescript": "~6.0.2",
28
+ "typescript-eslint": "^8.59.2",
29
+ "vite": "^8.0.12"
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ /* ─── App.css — page-specific overrides ──────────────────────────────────────── */
2
+ /* Intentionally minimal. Design tokens live in index.css. */
@@ -0,0 +1,390 @@
1
+ import { useState, useEffect } from 'react'
2
+ import { BrowserRouter, Routes, Route, NavLink, useLocation } from 'react-router-dom'
3
+ import './App.css'
4
+ import { getApiBase, setApiBase, checkSession, logout, registerUnauthedCallback, fetchHealth } from './api'
5
+ import type { HealthData } from './types'
6
+ import LoginPage from './pages/LoginPage'
7
+ import ChatPage from './pages/ChatPage'
8
+ import MemoryPage from './pages/MemoryPage'
9
+ import HealthPage from './pages/HealthPage'
10
+ import SandboxPage from './pages/SandboxPage'
11
+ import SettingsPage from './pages/SettingsPage'
12
+ import TasksPage from './pages/TasksPage'
13
+ import FilesPage from './pages/FilesPage'
14
+ import ApiKeysPage from './pages/ApiKeysPage'
15
+ import IntegrationsPage from './pages/IntegrationsPage'
16
+ import BrainSelector from './components/BrainSelector'
17
+ import AutomationsPage from './pages/AutomationsPage'
18
+ import DeploymentsPage from './pages/DeploymentsPage'
19
+ import SkillsPage from './pages/SkillsPage'
20
+ import UsagePage from './pages/UsagePage'
21
+
22
+ // ─── Auth state type ──────────────────────────────────────────────────────────
23
+ type AuthState = 'loading' | 'authed' | 'unauthed'
24
+
25
+ // ─── Sidebar ─────────────────────────────────────────────────────────────────
26
+
27
+ interface DotProps {
28
+ ok: boolean
29
+ label: string
30
+ }
31
+
32
+ function StatusDot({ ok, label }: DotProps) {
33
+ return (
34
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-tertiary)' }}>
35
+ <span style={{
36
+ width: 7, height: 7, borderRadius: '50%',
37
+ background: ok ? '#22c55e' : '#ef4444',
38
+ boxShadow: ok ? '0 0 6px rgba(34,197,94,0.5)' : '0 0 6px rgba(239,68,68,0.5)',
39
+ display: 'inline-block',
40
+ animation: ok ? undefined : 'blink 1s ease-in-out infinite',
41
+ }} />
42
+ {label}
43
+ </div>
44
+ )
45
+ }
46
+
47
+ interface SidebarProps {
48
+ onLogout: () => void
49
+ health: HealthData | null
50
+ activeBrainId: string
51
+ onBrainChange: (id: string) => void
52
+ }
53
+
54
+ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProps) {
55
+ return (
56
+ <aside className="sidebar">
57
+ <div className="sidebar-brand">
58
+ <h1>Total Recall</h1>
59
+ <span>Sovereign Brain v3.0</span>
60
+ </div>
61
+ <nav className="sidebar-nav">
62
+ <NavLink to="/" end className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-chat">
63
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
64
+ <path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
65
+ </svg>
66
+ Chat
67
+ </NavLink>
68
+ <NavLink to="/memory" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-memory">
69
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
70
+ <circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
71
+ </svg>
72
+ Memory
73
+ </NavLink>
74
+ <NavLink to="/tasks" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-tasks">
75
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
76
+ <path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
77
+ </svg>
78
+ Tasks
79
+ </NavLink>
80
+ <NavLink to="/automations" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-automations">
81
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
82
+ <circle cx="12" cy="12" r="10" />
83
+ <polyline points="12 6 12 12 16 14" />
84
+ </svg>
85
+ Automations
86
+ </NavLink>
87
+ <NavLink to="/files" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-files">
88
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
89
+ <path d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
90
+ </svg>
91
+ Files
92
+ </NavLink>
93
+ <NavLink to="/sandbox" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-sandbox">
94
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
95
+ <polyline points="16 18 22 12 16 6" /><polyline points="8 6 2 12 8 18" />
96
+ </svg>
97
+ Sandbox
98
+ </NavLink>
99
+ <NavLink to="/deployments" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-deployments">
100
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
101
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
102
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
103
+ <line x1="6" y1="6" x2="6.01" y2="6" />
104
+ <line x1="6" y1="18" x2="6.01" y2="18" />
105
+ </svg>
106
+ Deployments
107
+ </NavLink>
108
+ <NavLink to="/health" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-health">
109
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
110
+ <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
111
+ </svg>
112
+ Health
113
+ </NavLink>
114
+ <NavLink to="/usage" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-usage">
115
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
116
+ <line x1="12" y1="1" x2="12" y2="23"></line>
117
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
118
+ </svg>
119
+ Usage & Costs
120
+ </NavLink>
121
+ <NavLink to="/settings" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-settings">
122
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
123
+ <circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
124
+ </svg>
125
+ Settings
126
+ </NavLink>
127
+ <NavLink to="/keys" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-keys">
128
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
129
+ <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
130
+ </svg>
131
+ API Keys
132
+ </NavLink>
133
+ <NavLink to="/integrations" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-integrations">
134
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
135
+ <path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
136
+ <path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
137
+ </svg>
138
+ Integrations
139
+ </NavLink>
140
+ <NavLink to="/skills" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-skills">
141
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
142
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
143
+ </svg>
144
+ Skills Manager
145
+ </NavLink>
146
+ </nav>
147
+ <div className="sidebar-footer">
148
+ <div style={{ marginBottom: 8 }}>
149
+ <BrainSelector activeBrainId={activeBrainId} onBrainChange={onBrainChange} />
150
+ </div>
151
+ <div style={{ marginBottom: 12 }}>
152
+ <select
153
+ value={getApiBase()}
154
+ onChange={e => setApiBase(e.target.value)}
155
+ style={{
156
+ width: '100%',
157
+ background: 'var(--bg-secondary)',
158
+ color: 'var(--text-secondary)',
159
+ border: '1px solid var(--border)',
160
+ borderRadius: 6,
161
+ padding: '4px 8px',
162
+ fontSize: 12,
163
+ outline: 'none'
164
+ }}
165
+ >
166
+ <option value="">Localhost (Proxy)</option>
167
+ {import.meta.env.VITE_BRAIN_URL && (
168
+ <option value={import.meta.env.VITE_BRAIN_URL}>{new URL(import.meta.env.VITE_BRAIN_URL).host}</option>
169
+ )}
170
+ </select>
171
+ </div>
172
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
173
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11 }}>
174
+ {(() => {
175
+ const daemonOk = health?.daemon === 'running'
176
+ const serverOk = health && health.status !== 'unreachable'
177
+ const allGood = daemonOk && serverOk
178
+
179
+ if (!health) return <div style={{ color: 'var(--text-tertiary)' }}>Checking…</div>
180
+
181
+ return (
182
+ <>
183
+ <style>{`@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }`}</style>
184
+ {allGood ? (
185
+ <div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-tertiary)' }}>
186
+ <span className="pulse" /> All Systems Go
187
+ </div>
188
+ ) : (
189
+ <>
190
+ <StatusDot ok={!!serverOk} label={serverOk ? 'Server online' : 'Server offline'} />
191
+ <StatusDot ok={daemonOk} label={daemonOk ? 'Daemon' : 'Daemon ' + (health?.daemon ?? 'unknown')} />
192
+ </>
193
+ )}
194
+ </>
195
+ )
196
+ })()}
197
+ </div>
198
+ {/* Logout button */}
199
+ <button
200
+ id="btn-logout"
201
+ onClick={onLogout}
202
+ title="Sign out"
203
+ style={{
204
+ background: 'none',
205
+ border: '1px solid var(--border)',
206
+ borderRadius: 6,
207
+ cursor: 'pointer',
208
+ color: 'var(--text-tertiary)',
209
+ display: 'flex',
210
+ alignItems: 'center',
211
+ gap: 5,
212
+ padding: '4px 8px',
213
+ fontSize: 11,
214
+ transition: 'color 0.15s, border-color 0.15s',
215
+ }}
216
+ onMouseEnter={e => { (e.currentTarget as HTMLElement).style.color = '#ef4444'; (e.currentTarget as HTMLElement).style.borderColor = '#ef4444' }}
217
+ onMouseLeave={e => { (e.currentTarget as HTMLElement).style.color = 'var(--text-tertiary)'; (e.currentTarget as HTMLElement).style.borderColor = 'var(--border)' }}
218
+ >
219
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
220
+ <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/>
221
+ <polyline points="16 17 21 12 16 7"/>
222
+ <line x1="21" y1="12" x2="9" y2="12"/>
223
+ </svg>
224
+ Sign out
225
+ </button>
226
+ </div>
227
+ </div>
228
+ </aside>
229
+ )
230
+ }
231
+
232
+ // ─── Main content (routes) ────────────────────────────────────────────────────
233
+
234
+ function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string; onBrainChange: (id: string) => void }) {
235
+ const location = useLocation();
236
+ const isChat = location.pathname === '/';
237
+ const [floatingChat, setFloatingChat] = useState(false);
238
+
239
+ useEffect(() => {
240
+ const handleToggle = () => setFloatingChat(v => !v);
241
+ window.addEventListener('toggle-floating-chat', handleToggle);
242
+ return () => window.removeEventListener('toggle-floating-chat', handleToggle);
243
+ }, []);
244
+
245
+ const showChat = isChat || floatingChat;
246
+
247
+ const chatStyle: React.CSSProperties = floatingChat ? {
248
+ display: 'flex',
249
+ position: 'fixed',
250
+ bottom: 20,
251
+ right: 20,
252
+ width: 400,
253
+ height: 600,
254
+ zIndex: 9999,
255
+ backgroundColor: 'var(--bg-primary)',
256
+ border: '1px solid var(--border)',
257
+ borderRadius: 12,
258
+ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
259
+ overflow: 'hidden',
260
+ flexDirection: 'column'
261
+ } : {
262
+ display: showChat ? 'flex' : 'none',
263
+ flex: 1,
264
+ flexDirection: 'column',
265
+ height: '100%',
266
+ overflow: 'hidden'
267
+ };
268
+
269
+ return (
270
+ <main className="main-content" style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden', position: 'relative' }}>
271
+ <div style={chatStyle}>
272
+ {floatingChat && (
273
+ <div style={{ padding: '8px', background: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
274
+ <span style={{ fontSize: 12, fontWeight: 600 }}>Total Recall Agent</span>
275
+ <button onClick={() => setFloatingChat(false)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-tertiary)' }}>✖</button>
276
+ </div>
277
+ )}
278
+ <ChatPage activeBrainId={activeBrainId} onBrainChange={onBrainChange} />
279
+ </div>
280
+ {(!isChat || floatingChat) && (
281
+ <div style={{ flex: 1, overflow: 'auto' }}>
282
+ <Routes>
283
+ <Route path="/" element={<div/>} />
284
+ <Route path="/memory" element={<MemoryPage activeBrainId={activeBrainId} />} />
285
+ <Route path="/tasks" element={<TasksPage />} />
286
+ <Route path="/automations" element={<AutomationsPage />} />
287
+ <Route path="/files" element={<FilesPage />} />
288
+ <Route path="/sandbox" element={<SandboxPage />} />
289
+ <Route path="/deployments" element={<DeploymentsPage />} />
290
+ <Route path="/health" element={<HealthPage />} />
291
+ <Route path="/usage" element={<UsagePage />} />
292
+ <Route path="/settings" element={<SettingsPage />} />
293
+ <Route path="/keys" element={<ApiKeysPage />} />
294
+ <Route path="/integrations" element={<IntegrationsPage />} />
295
+ <Route path="/skills" element={<SkillsPage />} />
296
+ </Routes>
297
+ </div>
298
+ )}
299
+ </main>
300
+ );
301
+ }
302
+
303
+
304
+ // ─── Root App with auth gate ──────────────────────────────────────────────────
305
+
306
+ const BRAIN_STORAGE_KEY = 'total-recall-active-brain'
307
+
308
+ function App() {
309
+ const [authState, setAuthState] = useState<AuthState>('loading')
310
+ const [health, setHealth] = useState<HealthData | null>(null)
311
+ const [activeBrainId, setActiveBrainId] = useState(
312
+ () => localStorage.getItem(BRAIN_STORAGE_KEY) || 'global'
313
+ )
314
+
315
+ // Persist activeBrainId to localStorage on change
316
+ useEffect(() => {
317
+ localStorage.setItem(BRAIN_STORAGE_KEY, activeBrainId)
318
+ }, [activeBrainId])
319
+
320
+ // Register 401 interceptor so any API call that gets a 401 flips us to unauthed
321
+ useEffect(() => {
322
+ registerUnauthedCallback(() => setAuthState('unauthed'))
323
+ return () => {}
324
+ }, [])
325
+
326
+ // Session check on mount — probe /auth/me before rendering anything
327
+ useEffect(() => {
328
+ checkSession().then(ok => setAuthState(ok ? 'authed' : 'unauthed'))
329
+ }, [])
330
+
331
+ // Health polling — shared by EmergencyAlertBanner and Sidebar
332
+ useEffect(() => {
333
+ let active = true
334
+ const poll = async () => {
335
+ try {
336
+ const data = await fetchHealth()
337
+ if (active) setHealth(data)
338
+ } catch {
339
+ if (active) setHealth({ status: 'unreachable', version: '', uptime_seconds: 0, timestamp: '' })
340
+ }
341
+ }
342
+ poll()
343
+ const interval = setInterval(poll, 10000)
344
+ return () => { active = false; clearInterval(interval) }
345
+ }, [])
346
+
347
+ async function handleLogout() {
348
+ await logout()
349
+ setAuthState('unauthed')
350
+ }
351
+
352
+ function handleAuthenticated() {
353
+ setAuthState('authed')
354
+ }
355
+
356
+ // Loading state — blank dark screen, no flash of unprotected content
357
+ if (authState === 'loading') {
358
+ return (
359
+ <div style={{
360
+ minHeight: '100vh',
361
+ display: 'flex',
362
+ alignItems: 'center',
363
+ justifyContent: 'center',
364
+ background: 'var(--bg-primary)',
365
+ }}>
366
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-tertiary)" strokeWidth="2.5" strokeLinecap="round" style={{ animation: 'spin 0.8s linear infinite' }}>
367
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
368
+ </svg>
369
+ <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
370
+ </div>
371
+ )
372
+ }
373
+
374
+ // Unauthed — show login screen only
375
+ if (authState === 'unauthed') {
376
+ return <LoginPage onAuthenticated={handleAuthenticated} />
377
+ }
378
+
379
+ // Authed — render full dashboard
380
+ return (
381
+ <BrowserRouter>
382
+ <div className="app-layout">
383
+ <Sidebar onLogout={handleLogout} health={health} activeBrainId={activeBrainId} onBrainChange={setActiveBrainId} />
384
+ <MainContent activeBrainId={activeBrainId} onBrainChange={setActiveBrainId} />
385
+ </div>
386
+ </BrowserRouter>
387
+ )
388
+ }
389
+
390
+ export default App