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.
- package/extension/background.js +99 -0
- package/extension/content-script.css +16 -0
- package/extension/content-script.js +134 -0
- package/extension/icons/README.md +27 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/lib/brain-client.js +47 -0
- package/extension/manifest.json +39 -0
- package/extension/options/options.css +218 -0
- package/extension/options/options.html +72 -0
- package/extension/options/options.js +99 -0
- package/extension/popup/popup.css +221 -0
- package/extension/popup/popup.html +44 -0
- package/extension/popup/popup.js +107 -0
- package/extension/sidepanel/sidepanel.css +340 -0
- package/extension/sidepanel/sidepanel.html +38 -0
- package/extension/sidepanel/sidepanel.js +229 -0
- package/frontend/README.md +73 -0
- package/frontend/index.html +17 -0
- package/frontend/package.json +31 -0
- package/frontend/src/App.css +2 -0
- package/frontend/src/App.tsx +390 -0
- package/frontend/src/api.ts +688 -0
- package/frontend/src/assets/hero.png +0 -0
- package/frontend/src/assets/react.svg +1 -0
- package/frontend/src/assets/vite.svg +1 -0
- package/frontend/src/components/BrainSelector.tsx +272 -0
- package/frontend/src/components/DaemonLogsTab.tsx +54 -0
- package/frontend/src/components/Graph3D.tsx +850 -0
- package/frontend/src/components/MarkdownUtils.tsx +99 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +707 -0
- package/frontend/src/components/TaskQueueTab.tsx +185 -0
- package/frontend/src/index.css +1589 -0
- package/frontend/src/main.tsx +10 -0
- package/frontend/src/pages/ApiKeysPage.tsx +288 -0
- package/frontend/src/pages/AutomationsPage.tsx +354 -0
- package/frontend/src/pages/ChatPage.tsx +831 -0
- package/frontend/src/pages/DeploymentsPage.tsx +275 -0
- package/frontend/src/pages/FilesPage.tsx +384 -0
- package/frontend/src/pages/HealthPage.tsx +91 -0
- package/frontend/src/pages/IntegrationsPage.tsx +382 -0
- package/frontend/src/pages/LoginPage.tsx +237 -0
- package/frontend/src/pages/MemoryPage.tsx +1192 -0
- package/frontend/src/pages/SandboxPage.tsx +113 -0
- package/frontend/src/pages/SettingsPage.tsx +662 -0
- package/frontend/src/pages/SkillsPage.tsx +618 -0
- package/frontend/src/pages/TasksPage.tsx +392 -0
- package/frontend/src/pages/UsagePage.tsx +260 -0
- package/frontend/src/types.ts +163 -0
- package/frontend/tsconfig.app.json +25 -0
- package/frontend/tsconfig.json +7 -0
- package/frontend/tsconfig.node.json +24 -0
- package/frontend/vite.config.ts +24 -0
- package/package.json +8 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/concepts/cli-help-reference.md +35 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/invariants/operating-instructions.md +13 -8
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/always-websearch-gap.md +28 -1
- package/scaffold/.agent/skills/total-recall/memory-vault/preferences/topic-research-sop.md +28 -1
- package/scaffold/.agent/skills/total-recall/skills/ssss/references/ssss-spec.md +830 -81
- package/src/cli/backup.mjs +172 -1
- package/src/cli/setup.mjs +24 -0
- 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
|
+
}
|