total-recall-brain 3.1.2 → 3.2.2

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 (63) 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 +831 -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/backup.mjs +172 -1
  62. package/src/cli/setup.mjs +24 -0
  63. package/src/server/rest.mjs +56 -7
@@ -0,0 +1,662 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { fetchConfigJson, saveConfigJson, fetchConfig, saveConfig } from '../api';
3
+ import type { ConfigJson } from '../types';
4
+
5
+ export default function SettingsPage() {
6
+ const [viewMode, setViewMode] = useState<'visual' | 'yaml'>('visual');
7
+ const [activeYamlTab, setActiveYamlTab] = useState<'frontier.yml' | 'security.yml' | 'budget.yml'>('security.yml');
8
+
9
+ // Visual Form State
10
+ const [configData, setConfigData] = useState<ConfigJson | null>(null);
11
+
12
+ // YAML Text Area State
13
+ const [yamlContent, setYamlContent] = useState('');
14
+
15
+ // UI States
16
+ const [loading, setLoading] = useState(true);
17
+ const [saving, setSaving] = useState(false);
18
+ const [error, setError] = useState<string | null>(null);
19
+ const [success, setSuccess] = useState(false);
20
+
21
+ // Load JSON for Visual Form
22
+ const loadVisualConfig = async () => {
23
+ setLoading(true);
24
+ setError(null);
25
+ try {
26
+ const data = await fetchConfigJson();
27
+ // Enforce default structures if missing
28
+ if (!data.security) data.security = {};
29
+ if (!data.security.privacy) data.security.privacy = { enforce_local_only: true, allow_frontier_export: 'ask_per_skill' };
30
+ if (!data.security.dashboard) data.security.dashboard = { session_ttl_hours: 24 };
31
+ if (!data.security.bind) data.security.bind = { host: '127.0.0.1', port: 3000, allow_public_bind: false };
32
+ if (!data.security.network) data.security.network = { require_https: true, public_health: false, allowed_origins: [] };
33
+ if (!data.security.rate_limits) data.security.rate_limits = { api_requests_per_minute: 60, sandbox_requests_per_minute: 10, ingest_requests_per_minute: 120 };
34
+ if (!data.budget) data.budget = {};
35
+ if (!data.budget.budget) data.budget.budget = { daily_cap_usd: 5.0, weekly_cap_usd: 25.0, enabled: true };
36
+ if (!data.brain) data.brain = {};
37
+ if (!data.brain.preferred_agent) data.brain.preferred_agent = 'auto';
38
+
39
+ setConfigData(data);
40
+ } catch (err: unknown) {
41
+ setError((err as Error).message || 'Failed to load configuration.');
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ };
46
+
47
+ // Load YAML string for Raw Editor
48
+ const loadYamlConfig = async (name: string) => {
49
+ setLoading(true);
50
+ setError(null);
51
+ try {
52
+ const data = await fetchConfig(name);
53
+ setYamlContent(data);
54
+ } catch (err: unknown) {
55
+ setError((err as Error).message || `Failed to load ${name}.`);
56
+ } finally {
57
+ setLoading(false);
58
+ }
59
+ };
60
+
61
+ useEffect(() => {
62
+ if (viewMode === 'visual') {
63
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- legitimate config loader on visual active
64
+ void loadVisualConfig();
65
+ } else {
66
+
67
+ void loadYamlConfig(activeYamlTab);
68
+ }
69
+ }, [viewMode, activeYamlTab]);
70
+
71
+ // Save Visual Form JSON
72
+ const handleSaveVisual = async () => {
73
+ if (!configData) return;
74
+ setSaving(true);
75
+ setError(null);
76
+ setSuccess(false);
77
+ try {
78
+ await saveConfigJson(configData);
79
+ setSuccess(true);
80
+ setTimeout(() => setSuccess(false), 4000);
81
+ } catch (err: unknown) {
82
+ setError((err as Error).message || 'Failed to save configuration.');
83
+ } finally {
84
+ setSaving(false);
85
+ }
86
+ };
87
+
88
+ // Save YAML Text
89
+ const handleSaveYaml = async () => {
90
+ setSaving(true);
91
+ setError(null);
92
+ setSuccess(false);
93
+ try {
94
+ await saveConfig(activeYamlTab, yamlContent);
95
+ setSuccess(true);
96
+ setTimeout(() => setSuccess(false), 4000);
97
+ } catch (err: unknown) {
98
+ setError((err as Error).message || `Failed to save ${activeYamlTab}.`);
99
+ } finally {
100
+ setSaving(false);
101
+ }
102
+ };
103
+
104
+ // Safe mutators for deeply nested properties
105
+ const updateSecurityProp = (section: string, prop: string, value: unknown) => {
106
+ if (!configData) return;
107
+ setConfigData({
108
+ ...configData,
109
+ security: {
110
+ ...configData.security,
111
+ [section]: {
112
+ ...(configData.security[section] as Record<string, unknown>),
113
+ [prop]: value
114
+ }
115
+ }
116
+ });
117
+ };
118
+
119
+ const updateRootSecurityProp = (prop: string, value: unknown) => {
120
+ if (!configData) return;
121
+ setConfigData({
122
+ ...configData,
123
+ security: {
124
+ ...configData.security,
125
+ [prop]: value
126
+ }
127
+ });
128
+ };
129
+
130
+ const updateBudgetProp = (prop: string, value: unknown) => {
131
+ if (!configData) return;
132
+ setConfigData({
133
+ ...configData,
134
+ budget: {
135
+ ...configData.budget,
136
+ budget: {
137
+ ...configData.budget.budget,
138
+ [prop]: value
139
+ }
140
+ }
141
+ });
142
+ };
143
+
144
+ const updateBrainProp = (prop: string, value: unknown) => {
145
+ if (!configData) return;
146
+ setConfigData({
147
+ ...configData,
148
+ brain: {
149
+ ...configData.brain,
150
+ [prop]: value as string
151
+ }
152
+ });
153
+ };
154
+
155
+ return (
156
+ <div className="page" style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
157
+ <div className="page-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 16 }}>
158
+ <div>
159
+ <h1>System Settings</h1>
160
+ <p>Read, write, and toggle UI and dashboard settings directly from the terminal or control panel</p>
161
+ </div>
162
+
163
+ {/* Toggle Mode Button */}
164
+ <div style={{ display: 'flex', gap: 8, background: 'var(--bg-secondary)', padding: 4, borderRadius: 8, border: '1px solid var(--border)' }}>
165
+ <button
166
+ onClick={() => setViewMode('visual')}
167
+ className="btn btn-sm"
168
+ style={{
169
+ background: viewMode === 'visual' ? 'var(--accent)' : 'transparent',
170
+ color: viewMode === 'visual' ? '#fff' : 'var(--text-secondary)',
171
+ borderRadius: 6,
172
+ fontSize: 12
173
+ }}
174
+ >
175
+ 🎛️ Control Panel
176
+ </button>
177
+ <button
178
+ onClick={() => setViewMode('yaml')}
179
+ className="btn btn-sm"
180
+ style={{
181
+ background: viewMode === 'yaml' ? 'var(--accent)' : 'transparent',
182
+ color: viewMode === 'yaml' ? '#fff' : 'var(--text-secondary)',
183
+ borderRadius: 6,
184
+ fontSize: 12
185
+ }}
186
+ >
187
+ 📝 Raw YAML Editor
188
+ </button>
189
+ </div>
190
+ </div>
191
+
192
+ {error && (
193
+ <div className="badge badge-error" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #ef4444', color: '#f87171' }}>
194
+ ⚠️ {error}
195
+ </div>
196
+ )}
197
+ {success && (
198
+ <div className="badge badge-success" style={{ marginBottom: 20, display: 'inline-flex', padding: '6px 12px', background: 'rgba(16, 185, 129, 0.1)', border: '1px solid #10b981', color: '#34d399' }}>
199
+ ✓ Saved successfully! The memory kernel will hot-reload configuration settings instantly.
200
+ </div>
201
+ )}
202
+
203
+ {loading ? (
204
+ <div style={{ padding: 40, textAlign: 'center', color: 'var(--text-secondary)' }}>
205
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" style={{ animation: 'spin 1s linear infinite', marginBottom: 12 }}>
206
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
207
+ </svg>
208
+ <div>Retrieving brain configuration state...</div>
209
+ </div>
210
+ ) : viewMode === 'visual' && configData ? (
211
+ /* ==================== VISUAL CONTROL PANEL UI ==================== */
212
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(360px, 1fr))', gap: 24, flex: 1, minHeight: 0, overflowY: 'auto', paddingBottom: 24 }}>
213
+
214
+ {/* Card 1: AI Reasoning & Autonomy */}
215
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
216
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
217
+ <span style={{ fontSize: 20 }}>🧠</span>
218
+ <div>
219
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>Reasoning & Autonomy</h3>
220
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Control AI routing agent behaviors and local boundaries</p>
221
+ </div>
222
+ </div>
223
+
224
+ {/* YOLO Mode Switch */}
225
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
226
+ <div>
227
+ <label htmlFor="yolo_mode" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>YOLO Mode (Fully Autonomous)</label>
228
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Execute workflows without prompt confirmation gates</span>
229
+ </div>
230
+ <input
231
+ id="yolo_mode"
232
+ type="checkbox"
233
+ checked={configData.security.yolo_mode || false}
234
+ onChange={(e) => updateRootSecurityProp('yolo_mode', e.target.checked)}
235
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
236
+ />
237
+ </div>
238
+
239
+ {/* Local Privacy Switch */}
240
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
241
+ <div>
242
+ <label htmlFor="enforce_local_only" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>Local Privacy Isolation</label>
243
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Enforce local execution; never stream to cloud APIs</span>
244
+ </div>
245
+ <input
246
+ id="enforce_local_only"
247
+ type="checkbox"
248
+ checked={configData.security.privacy?.enforce_local_only || false}
249
+ onChange={(e) => updateSecurityProp('privacy', 'enforce_local_only', e.target.checked)}
250
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
251
+ />
252
+ </div>
253
+
254
+ {/* Export policy */}
255
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
256
+ <label htmlFor="allow_frontier_export" style={{ fontSize: 13, fontWeight: 500 }}>Frontier API Export Policy</label>
257
+ <select
258
+ id="allow_frontier_export"
259
+ value={configData.security.privacy?.allow_frontier_export || 'ask_per_skill'}
260
+ onChange={(e) => updateSecurityProp('privacy', 'allow_frontier_export', e.target.value)}
261
+ style={{
262
+ background: 'var(--bg-tertiary)',
263
+ color: 'var(--text-primary)',
264
+ border: '1px solid var(--border)',
265
+ padding: '8px 12px',
266
+ borderRadius: 6,
267
+ outline: 'none',
268
+ fontSize: 13
269
+ }}
270
+ >
271
+ <option value="always">Always Route (YOLO Mode)</option>
272
+ <option value="ask_per_skill">Ask Per Skill Trigger</option>
273
+ <option value="never">Never Route (Fully Air-Gapped)</option>
274
+ </select>
275
+ </div>
276
+
277
+ {/* Preferred CLI Agent Selector */}
278
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
279
+ <label htmlFor="preferred_agent" style={{ fontSize: 13, fontWeight: 500 }}>Preferred CLI Agent</label>
280
+ <select
281
+ id="preferred_agent"
282
+ value={configData.brain?.preferred_agent || 'auto'}
283
+ onChange={(e) => updateBrainProp('preferred_agent', e.target.value)}
284
+ style={{
285
+ background: 'var(--bg-tertiary)',
286
+ color: 'var(--text-primary)',
287
+ border: '1px solid var(--border)',
288
+ padding: '8px 12px',
289
+ borderRadius: 6,
290
+ outline: 'none',
291
+ fontSize: 13
292
+ }}
293
+ >
294
+ <option value="auto">Auto (Smart Routing / System Default)</option>
295
+ <option value="claude">Claude Code (Anthropic CLI Agent)</option>
296
+ <option value="gemini">Gemini CLI (Google Developer Agent)</option>
297
+ <option value="codex">OpenAI Codex (OpenAI Workspace Suite)</option>
298
+ <option value="antigravity">Antigravity (Local Sovereign Agent)</option>
299
+ </select>
300
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Elevates selected agent as the active primary for all dispatches</span>
301
+ </div>
302
+ </div>
303
+
304
+ {/* Card 2: Cost Control & Budget */}
305
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
306
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
307
+ <span style={{ fontSize: 20 }}>🪙</span>
308
+ <div>
309
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>Sovereign Cost Supervisor</h3>
310
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Track dollar cost consumption limits dynamically</p>
311
+ </div>
312
+ </div>
313
+
314
+ {/* Budget enabled Switch */}
315
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
316
+ <div>
317
+ <label htmlFor="budget_enabled" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>Supervision Active</label>
318
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Block LLM requests if cap limits are exceeded</span>
319
+ </div>
320
+ <input
321
+ id="budget_enabled"
322
+ type="checkbox"
323
+ checked={configData.budget.budget?.enabled !== false}
324
+ onChange={(e) => updateBudgetProp('enabled', e.target.checked)}
325
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
326
+ />
327
+ </div>
328
+
329
+ {/* Daily limit */}
330
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
331
+ <label htmlFor="daily_cap" style={{ fontSize: 13, fontWeight: 500 }}>Daily Capital Cap ($ USD)</label>
332
+ <input
333
+ id="daily_cap"
334
+ type="number"
335
+ step="0.01"
336
+ min="0"
337
+ value={configData.budget.budget?.daily_cap_usd || ''}
338
+ onChange={(e) => updateBudgetProp('daily_cap_usd', Number(e.target.value))}
339
+ style={{
340
+ background: 'var(--bg-tertiary)',
341
+ color: 'var(--text-primary)',
342
+ border: '1px solid var(--border)',
343
+ padding: '8px 12px',
344
+ borderRadius: 6,
345
+ outline: 'none',
346
+ fontSize: 13
347
+ }}
348
+ />
349
+ </div>
350
+
351
+ {/* Weekly limit */}
352
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
353
+ <label htmlFor="weekly_cap" style={{ fontSize: 13, fontWeight: 500 }}>Weekly Capital Cap ($ USD)</label>
354
+ <input
355
+ id="weekly_cap"
356
+ type="number"
357
+ step="0.01"
358
+ min="0"
359
+ value={configData.budget.budget?.weekly_cap_usd || ''}
360
+ onChange={(e) => updateBudgetProp('weekly_cap_usd', Number(e.target.value))}
361
+ style={{
362
+ background: 'var(--bg-tertiary)',
363
+ color: 'var(--text-primary)',
364
+ border: '1px solid var(--border)',
365
+ padding: '8px 12px',
366
+ borderRadius: 6,
367
+ outline: 'none',
368
+ fontSize: 13
369
+ }}
370
+ />
371
+ </div>
372
+ </div>
373
+
374
+ {/* Card 3: Dashboard & Sessions */}
375
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
376
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
377
+ <span style={{ fontSize: 20 }}>🔒</span>
378
+ <div>
379
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>Security & Tunnels</h3>
380
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Manage network exposure, binding, and session lifetimes</p>
381
+ </div>
382
+ </div>
383
+
384
+ {/* Force Reset */}
385
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
386
+ <div>
387
+ <label htmlFor="force_reset" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>Force Password Change</label>
388
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Prompt admin to reset credentials on next dashboard visit</span>
389
+ </div>
390
+ <input
391
+ id="force_reset"
392
+ type="checkbox"
393
+ checked={configData.security.dashboard?.force_password_reset || false}
394
+ onChange={(e) => updateSecurityProp('dashboard', 'force_password_reset', e.target.checked)}
395
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
396
+ />
397
+ </div>
398
+
399
+ {/* Enforce HTTPS */}
400
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
401
+ <div>
402
+ <label htmlFor="require_https" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>Require HTTPS Tunneling</label>
403
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Block non-localhost requests that arrive without SSL</span>
404
+ </div>
405
+ <input
406
+ id="require_https"
407
+ type="checkbox"
408
+ checked={configData.security.network?.require_https || false}
409
+ onChange={(e) => updateSecurityProp('network', 'require_https', e.target.checked)}
410
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
411
+ />
412
+ </div>
413
+
414
+ {/* Public Health */}
415
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
416
+ <div>
417
+ <label htmlFor="public_health" style={{ fontSize: 13, fontWeight: 500, display: 'block' }}>Public Health Route</label>
418
+ <span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>Keep /health status checker accessible to public probes</span>
419
+ </div>
420
+ <input
421
+ id="public_health"
422
+ type="checkbox"
423
+ checked={configData.security.network?.public_health || false}
424
+ onChange={(e) => updateSecurityProp('network', 'public_health', e.target.checked)}
425
+ style={{ width: 44, height: 22, cursor: 'pointer', accentColor: 'var(--accent)' }}
426
+ />
427
+ </div>
428
+
429
+ {/* Session TTL Slider */}
430
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
431
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
432
+ <label htmlFor="session_ttl" style={{ fontSize: 13, fontWeight: 500 }}>Session Lifetime (TTL)</label>
433
+ <span style={{ fontSize: 12, fontWeight: 600, color: 'var(--accent)' }}>{configData.security.dashboard?.session_ttl_hours || 24} hours</span>
434
+ </div>
435
+ <input
436
+ id="session_ttl"
437
+ type="range"
438
+ min="1"
439
+ max="168"
440
+ value={configData.security.dashboard?.session_ttl_hours || 24}
441
+ onChange={(e) => updateSecurityProp('dashboard', 'session_ttl_hours', Number(e.target.value))}
442
+ style={{ cursor: 'pointer', accentColor: 'var(--accent)' }}
443
+ />
444
+ </div>
445
+ </div>
446
+
447
+ {/* Card 4: Network & CORS Limits */}
448
+ <div className="card" style={{ padding: 24, display: 'flex', flexDirection: 'column', gap: 20, background: 'rgba(18, 18, 26, 0.6)', backdropFilter: 'blur(8px)', border: '1px solid var(--border)' }}>
449
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, borderBottom: '1px solid var(--border)', paddingBottom: 12 }}>
450
+ <span style={{ fontSize: 20 }}>🛡️</span>
451
+ <div>
452
+ <h3 style={{ fontSize: 15, fontWeight: 600 }}>CORS & Traffic Gateways</h3>
453
+ <p style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Adjust port bindings and enforce request quotas</p>
454
+ </div>
455
+ </div>
456
+
457
+ {/* Port input */}
458
+ <div style={{ display: 'flex', gap: 12 }}>
459
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 6 }}>
460
+ <label htmlFor="bind_host" style={{ fontSize: 12, fontWeight: 500 }}>Binding Host</label>
461
+ <input
462
+ id="bind_host"
463
+ type="text"
464
+ value={configData.security.bind?.host || ''}
465
+ onChange={(e) => updateSecurityProp('bind', 'host', e.target.value)}
466
+ style={{
467
+ background: 'var(--bg-tertiary)',
468
+ color: 'var(--text-primary)',
469
+ border: '1px solid var(--border)',
470
+ padding: '8px 12px',
471
+ borderRadius: 6,
472
+ outline: 'none',
473
+ fontSize: 13
474
+ }}
475
+ />
476
+ </div>
477
+ <div style={{ width: 100, display: 'flex', flexDirection: 'column', gap: 6 }}>
478
+ <label htmlFor="bind_port" style={{ fontSize: 12, fontWeight: 500 }}>Port</label>
479
+ <input
480
+ id="bind_port"
481
+ type="number"
482
+ value={configData.security.bind?.port || 3000}
483
+ onChange={(e) => updateSecurityProp('bind', 'port', Number(e.target.value))}
484
+ style={{
485
+ background: 'var(--bg-tertiary)',
486
+ color: 'var(--text-primary)',
487
+ border: '1px solid var(--border)',
488
+ padding: '8px 12px',
489
+ borderRadius: 6,
490
+ outline: 'none',
491
+ fontSize: 13
492
+ }}
493
+ />
494
+ </div>
495
+ </div>
496
+
497
+ {/* Allowed origins input */}
498
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
499
+ <label htmlFor="allowed_origins" style={{ fontSize: 13, fontWeight: 500 }}>CORS Allowed Origins</label>
500
+ <input
501
+ id="allowed_origins"
502
+ type="text"
503
+ placeholder="e.g. http://localhost:5173,http://localhost:3000"
504
+ value={Array.isArray(configData.security.network?.allowed_origins) ? configData.security.network.allowed_origins.join(',') : ''}
505
+ onChange={(e) => updateSecurityProp('network', 'allowed_origins', e.target.value.split(',').map(s => s.trim()).filter(Boolean))}
506
+ style={{
507
+ background: 'var(--bg-tertiary)',
508
+ color: 'var(--text-primary)',
509
+ border: '1px solid var(--border)',
510
+ padding: '8px 12px',
511
+ borderRadius: 6,
512
+ outline: 'none',
513
+ fontSize: 13
514
+ }}
515
+ />
516
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }}>Separate multiple domains with commas</span>
517
+ </div>
518
+
519
+ {/* API Requests rate limit */}
520
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
521
+ <label htmlFor="api_requests" style={{ fontSize: 13, fontWeight: 500 }}>API Requests Rate Limit (/min)</label>
522
+ <input
523
+ id="api_requests"
524
+ type="number"
525
+ value={configData.security.rate_limits?.api_requests_per_minute || ''}
526
+ onChange={(e) => updateSecurityProp('rate_limits', 'api_requests_per_minute', Number(e.target.value))}
527
+ style={{
528
+ background: 'var(--bg-tertiary)',
529
+ color: 'var(--text-primary)',
530
+ border: '1px solid var(--border)',
531
+ padding: '8px 12px',
532
+ borderRadius: 6,
533
+ outline: 'none',
534
+ fontSize: 13
535
+ }}
536
+ />
537
+ </div>
538
+ </div>
539
+
540
+ </div>
541
+ ) : (
542
+ /* ==================== RAW YAML EDITOR UI ==================== */
543
+ <div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
544
+
545
+ {/* Sub tabs for config selection */}
546
+ <div style={{ display: 'flex', gap: 8, marginBottom: 16, borderBottom: '1px solid var(--border)', paddingBottom: 12, flexShrink: 0 }}>
547
+ <button
548
+ className={`btn btn-sm ${activeYamlTab === 'frontier.yml' ? 'btn-primary' : 'btn-ghost'}`}
549
+ onClick={() => setActiveYamlTab('frontier.yml')}
550
+ style={{ fontSize: 12 }}
551
+ >
552
+ 🌐 Frontier (frontier.yml)
553
+ </button>
554
+ <button
555
+ className={`btn btn-sm ${activeYamlTab === 'security.yml' ? 'btn-primary' : 'btn-ghost'}`}
556
+ onClick={() => setActiveYamlTab('security.yml')}
557
+ style={{ fontSize: 12 }}
558
+ >
559
+ 🔒 Security (security.yml)
560
+ </button>
561
+ <button
562
+ className={`btn btn-sm ${activeYamlTab === 'budget.yml' ? 'btn-primary' : 'btn-ghost'}`}
563
+ onClick={() => setActiveYamlTab('budget.yml')}
564
+ style={{ fontSize: 12 }}
565
+ >
566
+ 🪙 Budget (budget.yml)
567
+ </button>
568
+ </div>
569
+
570
+ {/* Code text editor */}
571
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
572
+ <textarea
573
+ value={yamlContent}
574
+ onChange={(e) => setYamlContent(e.target.value)}
575
+ style={{
576
+ flex: 1,
577
+ width: '100%',
578
+ fontFamily: 'monospace',
579
+ fontSize: 14,
580
+ padding: 16,
581
+ background: 'var(--bg-secondary)',
582
+ color: 'var(--text-primary)',
583
+ border: '1px solid var(--border)',
584
+ borderRadius: 8,
585
+ resize: 'none',
586
+ outline: 'none',
587
+ lineHeight: 1.5,
588
+ whiteSpace: 'pre',
589
+ overflowWrap: 'normal',
590
+ overflowX: 'auto'
591
+ }}
592
+ spellCheck={false}
593
+ />
594
+ </div>
595
+ </div>
596
+ )}
597
+
598
+ {/* CHROME EXTENSION */}
599
+ <div style={{
600
+ marginTop: 24,
601
+ padding: 20,
602
+ background: 'linear-gradient(135deg, rgba(108, 92, 231, 0.08), rgba(99, 179, 237, 0.08))',
603
+ border: '1px solid rgba(108, 92, 231, 0.25)',
604
+ borderRadius: 12
605
+ }}>
606
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 12 }}>
607
+ <span style={{ fontSize: 24 }}>🧩</span>
608
+ <div>
609
+ <h3 style={{ margin: 0, fontSize: 15, fontWeight: 600, color: 'var(--text-primary)' }}>Chrome Extension</h3>
610
+ <p style={{ margin: '2px 0 0', fontSize: 12, color: 'var(--text-secondary)' }}>
611
+ Browse with your brain — contextual memory, quick capture, and research from any page
612
+ </p>
613
+ </div>
614
+ </div>
615
+ <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap' }}>
616
+ <a
617
+ href="/api/extension/download"
618
+ download
619
+ style={{
620
+ display: 'inline-flex',
621
+ alignItems: 'center',
622
+ gap: 8,
623
+ padding: '8px 16px',
624
+ background: 'linear-gradient(135deg, #6c5ce7, #a29bfe)',
625
+ color: '#fff',
626
+ borderRadius: 8,
627
+ fontWeight: 500,
628
+ fontSize: 13,
629
+ textDecoration: 'none',
630
+ transition: 'opacity 0.2s'
631
+ }}
632
+ onMouseEnter={e => (e.currentTarget.style.opacity = '0.85')}
633
+ onMouseLeave={e => (e.currentTarget.style.opacity = '1')}
634
+ >
635
+ ⬇ Download Extension
636
+ </a>
637
+ <span style={{ fontSize: 12, color: 'var(--text-tertiary)' }}>
638
+ Unzip → chrome://extensions → Developer mode → Load unpacked
639
+ </span>
640
+ </div>
641
+ </div>
642
+
643
+ {/* FOOTER ACTIONS */}
644
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 16, flexShrink: 0 }}>
645
+ <button
646
+ className="btn btn-primary"
647
+ onClick={viewMode === 'visual' ? handleSaveVisual : handleSaveYaml}
648
+ disabled={loading || saving}
649
+ style={{
650
+ width: 140,
651
+ background: 'linear-gradient(135deg, var(--accent), var(--accent-hover))',
652
+ color: '#fff',
653
+ fontWeight: 500,
654
+ borderRadius: 6
655
+ }}
656
+ >
657
+ {saving ? 'Saving...' : 'Save Settings'}
658
+ </button>
659
+ </div>
660
+ </div>
661
+ );
662
+ }