total-recall-brain 3.16.1 → 3.17.0
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/frontend/dist/sw.js +7 -1
- package/frontend/src/App.tsx +34 -23
- package/frontend/src/api/_base.ts +3 -3
- package/frontend/src/api/chat.ts +1 -1
- package/frontend/src/api/headscale.ts +71 -0
- package/frontend/src/api/index.ts +1 -0
- package/frontend/src/api/keys.ts +1 -0
- package/frontend/src/api/network.spec.ts +6 -7
- package/frontend/src/api/network.ts +35 -42
- package/frontend/src/api/notifications.ts +41 -0
- package/frontend/src/api/secrets.spec.ts +62 -0
- package/frontend/src/api/secrets.ts +53 -0
- package/frontend/src/api/webhooks.ts +4 -0
- package/frontend/src/components/VoiceInput.tsx +13 -2
- package/frontend/src/index.css +150 -0
- package/frontend/src/pages/MeshPage.css +11 -0
- package/frontend/src/pages/MeshPage.spec.tsx +83 -3
- package/frontend/src/pages/MeshPage.tsx +312 -57
- package/frontend/src/pages/NetworkPage.css +263 -19
- package/frontend/src/pages/NetworkPage.spec.tsx +34 -17
- package/frontend/src/pages/NetworkPage.tsx +320 -124
- package/frontend/src/pages/NotificationsPage.css +219 -0
- package/frontend/src/pages/NotificationsPage.spec.tsx +62 -0
- package/frontend/src/pages/NotificationsPage.tsx +318 -0
- package/frontend/src/pages/SecretsPage.css +141 -0
- package/frontend/src/pages/{ApiKeysPage.spec.tsx → SecretsPage.spec.tsx} +4 -3
- package/frontend/src/pages/{ApiKeysPage.tsx → SecretsPage.tsx} +351 -19
- package/frontend/src/pages/TasksPage.spec.tsx +1 -1
- package/frontend/src/pages/WebhooksPage.css +138 -0
- package/frontend/src/pages/WebhooksPage.spec.tsx +93 -5
- package/frontend/src/pages/WebhooksPage.tsx +232 -45
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/modules/agents/agents.yml +1 -1
- package/src/core/project-brain.mjs +1 -0
- package/src/core/provider-catalog.mjs +13 -0
- package/src/core/runtime.mjs +1 -1
- package/src/core/secrets-store.mjs +2 -0
- package/src/server/index.mjs +1 -1
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +56 -0
- package/src/server/routes/headscale.mjs +121 -0
- package/src/server/routes/mesh.mjs +3 -3
- package/src/server/routes/secrets.mjs +156 -2
- package/src/server/routes/system.mjs +3 -0
- package/src/server/routes/webhooks.mjs +136 -13
- package/frontend/src/pages/ModelsPage.spec.tsx +0 -65
- package/frontend/src/pages/ModelsPage.tsx +0 -596
- package/frontend/src/pages/VaultPage.spec.tsx +0 -27
- package/frontend/src/pages/VaultPage.tsx +0 -223
package/frontend/dist/sw.js
CHANGED
|
@@ -105,7 +105,13 @@ async function staleWhileRevalidate(request) {
|
|
|
105
105
|
}
|
|
106
106
|
return response;
|
|
107
107
|
})
|
|
108
|
-
.catch(() =>
|
|
108
|
+
.catch((err) => {
|
|
109
|
+
if (cached) return cached;
|
|
110
|
+
return new Response(JSON.stringify({ error: 'Network error and no cache available' }), {
|
|
111
|
+
status: 503,
|
|
112
|
+
headers: { 'Content-Type': 'application/json' }
|
|
113
|
+
});
|
|
114
|
+
});
|
|
109
115
|
|
|
110
116
|
return cached || fetchPromise;
|
|
111
117
|
}
|
package/frontend/src/App.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import MemoryPage from './pages/MemoryPage'
|
|
|
9
9
|
import SettingsPage from './pages/SettingsPage'
|
|
10
10
|
import NetworkPage from './pages/NetworkPage'
|
|
11
11
|
import TasksPage from './pages/TasksPage'
|
|
12
|
-
import
|
|
12
|
+
import SecretsPage from './pages/SecretsPage'
|
|
13
13
|
import IntegrationsPage from './pages/IntegrationsPage'
|
|
14
14
|
import { MeshPage } from './pages/MeshPage'
|
|
15
15
|
import { WebhooksPage } from './pages/WebhooksPage'
|
|
@@ -20,6 +20,7 @@ import HelpPage from './pages/HelpPage'
|
|
|
20
20
|
import GraphPage from './pages/GraphPage'
|
|
21
21
|
import OpenWikiPage from './pages/OpenWikiPage'
|
|
22
22
|
import OnboardingPage from './pages/OnboardingPage'
|
|
23
|
+
import { NotificationsPage } from './pages/NotificationsPage'
|
|
23
24
|
import { isOnboardingComplete } from './utils/onboarding'
|
|
24
25
|
import BrandMark from './components/brand/BrandMark'
|
|
25
26
|
import ContextualHelp from './components/ContextualHelp'
|
|
@@ -56,6 +57,8 @@ interface SidebarProps {
|
|
|
56
57
|
onBrainChange: (id: string) => void
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const getNavLinkClass = ({ isActive }: { isActive: boolean }) => `nav-link ${isActive ? 'active' : ''}`;
|
|
61
|
+
|
|
59
62
|
function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProps) {
|
|
60
63
|
return (
|
|
61
64
|
<aside className="sidebar">
|
|
@@ -68,40 +71,40 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
68
71
|
<nav className="sidebar-nav">
|
|
69
72
|
<div className="nav-section-label">Memory</div>
|
|
70
73
|
{!isOnboardingComplete() && (
|
|
71
|
-
<NavLink to="/onboarding" className={
|
|
74
|
+
<NavLink to="/onboarding" className={getNavLinkClass} id="nav-onboarding">
|
|
72
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">
|
|
73
76
|
<path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5" /><path d="M2 12l10 5 10-5" />
|
|
74
77
|
</svg>
|
|
75
78
|
Setup
|
|
76
79
|
</NavLink>
|
|
77
80
|
)}
|
|
78
|
-
<NavLink to="/" end className={
|
|
81
|
+
<NavLink to="/" end className={getNavLinkClass} id="nav-chat">
|
|
79
82
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
80
83
|
<path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z" />
|
|
81
84
|
</svg>
|
|
82
85
|
Chat
|
|
83
86
|
</NavLink>
|
|
84
|
-
<NavLink to="/memory" className={
|
|
87
|
+
<NavLink to="/memory" className={getNavLinkClass} id="nav-memory">
|
|
85
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">
|
|
86
89
|
<circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" />
|
|
87
90
|
</svg>
|
|
88
91
|
Vault
|
|
89
92
|
</NavLink>
|
|
90
|
-
<NavLink to="/rules" className={
|
|
93
|
+
<NavLink to="/rules" className={getNavLinkClass} id="nav-rules">
|
|
91
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">
|
|
92
95
|
<path d="M14 2H6a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V8l-6-6z" />
|
|
93
96
|
<path d="M14 3v5h5M16 13H8M16 17H8M10 9H8" />
|
|
94
97
|
</svg>
|
|
95
98
|
Rules
|
|
96
99
|
</NavLink>
|
|
97
|
-
<NavLink to="/graph" className={
|
|
100
|
+
<NavLink to="/graph" className={getNavLinkClass} id="nav-graph">
|
|
98
101
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
99
102
|
<circle cx="5" cy="6" r="2" /><circle cx="19" cy="6" r="2" /><circle cx="12" cy="18" r="2" />
|
|
100
103
|
<path d="M7 7l4 9M17 7l-4 9" />
|
|
101
104
|
</svg>
|
|
102
105
|
Graph
|
|
103
106
|
</NavLink>
|
|
104
|
-
<NavLink to="/tasks" className={
|
|
107
|
+
<NavLink to="/tasks" className={getNavLinkClass} id="nav-tasks">
|
|
105
108
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
106
109
|
<path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" />
|
|
107
110
|
</svg>
|
|
@@ -109,20 +112,20 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
109
112
|
</NavLink>
|
|
110
113
|
|
|
111
114
|
<div className="nav-section-label">Connect</div>
|
|
112
|
-
<NavLink to="/integrations" className={
|
|
115
|
+
<NavLink to="/integrations" className={getNavLinkClass} id="nav-integrations">
|
|
113
116
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
114
117
|
<path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71" />
|
|
115
118
|
<path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71" />
|
|
116
119
|
</svg>
|
|
117
120
|
IDEs
|
|
118
121
|
</NavLink>
|
|
119
|
-
<NavLink to="/skills" className={
|
|
122
|
+
<NavLink to="/skills" className={getNavLinkClass} id="nav-skills">
|
|
120
123
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
121
124
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
122
125
|
</svg>
|
|
123
126
|
Skills
|
|
124
127
|
</NavLink>
|
|
125
|
-
<NavLink to="/openwiki" className={
|
|
128
|
+
<NavLink to="/openwiki" className={getNavLinkClass} id="nav-openwiki">
|
|
126
129
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
127
130
|
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z" />
|
|
128
131
|
<path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z" />
|
|
@@ -131,13 +134,7 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
131
134
|
</NavLink>
|
|
132
135
|
|
|
133
136
|
<div className="nav-section-label">Account</div>
|
|
134
|
-
<NavLink to="/
|
|
135
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
136
|
-
<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" />
|
|
137
|
-
</svg>
|
|
138
|
-
Keys & Usage
|
|
139
|
-
</NavLink>
|
|
140
|
-
<NavLink to="/network" className={({ isActive }) => `nav-link ${isActive ? 'active' : ''}`} id="nav-network">
|
|
137
|
+
<NavLink to="/network" className={getNavLinkClass} id="nav-network">
|
|
141
138
|
<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
139
|
<rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
|
|
143
140
|
<rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
|
|
@@ -146,25 +143,37 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
146
143
|
</svg>
|
|
147
144
|
Network
|
|
148
145
|
</NavLink>
|
|
149
|
-
<NavLink to="/mesh" className={
|
|
146
|
+
<NavLink to="/mesh" className={getNavLinkClass} id="nav-mesh">
|
|
150
147
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
151
148
|
<path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5" /><path d="M2 12l10 5 10-5" />
|
|
152
149
|
</svg>
|
|
153
150
|
Mesh
|
|
154
151
|
</NavLink>
|
|
155
|
-
<NavLink to="/webhooks" className={
|
|
152
|
+
<NavLink to="/webhooks" className={getNavLinkClass} id="nav-webhooks">
|
|
156
153
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
157
154
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
|
158
155
|
</svg>
|
|
159
156
|
Webhooks
|
|
160
157
|
</NavLink>
|
|
161
|
-
<NavLink to="/
|
|
158
|
+
<NavLink to="/secrets" className={getNavLinkClass} id="nav-secrets">
|
|
159
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
160
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0110 0v4" />
|
|
161
|
+
</svg>
|
|
162
|
+
Secrets & Keys
|
|
163
|
+
</NavLink>
|
|
164
|
+
<NavLink to="/notifications" className={getNavLinkClass} id="nav-notifications">
|
|
165
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
166
|
+
<path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9" /><path d="M13.73 21a2 2 0 01-3.46 0" />
|
|
167
|
+
</svg>
|
|
168
|
+
Notifications
|
|
169
|
+
</NavLink>
|
|
170
|
+
<NavLink to="/settings" className={getNavLinkClass} id="nav-settings">
|
|
162
171
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
163
172
|
<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" />
|
|
164
173
|
</svg>
|
|
165
174
|
Settings
|
|
166
175
|
</NavLink>
|
|
167
|
-
<NavLink to="/help" className={
|
|
176
|
+
<NavLink to="/help" className={getNavLinkClass} id="nav-help">
|
|
168
177
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
169
178
|
<circle cx="12" cy="12" r="10" />
|
|
170
179
|
<path d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" />
|
|
@@ -173,7 +182,7 @@ function Sidebar({ onLogout, health, activeBrainId, onBrainChange }: SidebarProp
|
|
|
173
182
|
Help
|
|
174
183
|
</NavLink>
|
|
175
184
|
{isOnboardingComplete() && (
|
|
176
|
-
<NavLink to="/onboarding" className={
|
|
185
|
+
<NavLink to="/onboarding" className={getNavLinkClass} id="nav-onboarding-replay" style={{ opacity: 0.75 }}>
|
|
177
186
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
178
187
|
<path d="M12 2L2 7l10 5 10-5-10-5z" /><path d="M2 17l10 5 10-5" /><path d="M2 12l10 5 10-5" />
|
|
179
188
|
</svg>
|
|
@@ -338,11 +347,13 @@ function MainContent({ activeBrainId, onBrainChange }: { activeBrainId: string;
|
|
|
338
347
|
<Route path="/skills" element={<SkillsPage activeBrainId={activeBrainId} />} />
|
|
339
348
|
<Route path="/openwiki" element={<OpenWikiPage activeBrainId={activeBrainId} />} />
|
|
340
349
|
<Route path="/integrations" element={<IntegrationsPage activeBrainId={activeBrainId} />} />
|
|
341
|
-
<Route path="/keys" element={<
|
|
350
|
+
<Route path="/keys" element={<Navigate to="/secrets" replace />} />
|
|
342
351
|
<Route path="/settings" element={<SettingsPage activeBrainId={activeBrainId} />} />
|
|
343
352
|
<Route path="/network" element={<NetworkPage />} />
|
|
344
353
|
<Route path="/mesh" element={<MeshPage />} />
|
|
345
354
|
<Route path="/webhooks" element={<WebhooksPage />} />
|
|
355
|
+
<Route path="/secrets" element={<SecretsPage />} />
|
|
356
|
+
<Route path="/notifications" element={<NotificationsPage />} />
|
|
346
357
|
<Route path="/help" element={<HelpPage />} />
|
|
347
358
|
{/* Legacy OS-control-plane routes → core product */}
|
|
348
359
|
<Route path="/vault" element={<Navigate to="/memory" replace />} />
|
|
@@ -34,13 +34,13 @@ export async function apiFetch(url: string, options: RequestInit = {}): Promise<
|
|
|
34
34
|
return res
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function get(path: string) {
|
|
37
|
+
export async function get<T = any>(path: string): Promise<T> {
|
|
38
38
|
const res = await apiFetch(API_BASE + path);
|
|
39
39
|
if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`);
|
|
40
40
|
return res.json();
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export async function post(path: string, body: any) {
|
|
43
|
+
export async function post<T = any>(path: string, body: any): Promise<T> {
|
|
44
44
|
const res = await apiFetch(API_BASE + path, {
|
|
45
45
|
method: 'POST',
|
|
46
46
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -50,7 +50,7 @@ export async function post(path: string, body: any) {
|
|
|
50
50
|
return res.json();
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export async function del(path: string) {
|
|
53
|
+
export async function del<T = any>(path: string): Promise<T> {
|
|
54
54
|
const res = await apiFetch(API_BASE + path, {
|
|
55
55
|
method: 'DELETE'
|
|
56
56
|
});
|
package/frontend/src/api/chat.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function sendChat(
|
|
|
37
37
|
})
|
|
38
38
|
if (!res.ok) throw new Error(`Chat API error: ${res.status}`)
|
|
39
39
|
const data = await res.json()
|
|
40
|
-
return data.choices?.[0]?.message?.content
|
|
40
|
+
return data.choices?.[0]?.message?.content || '(empty response)'
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export async function fetchChatHistory(sessionId?: string): Promise<{ role: string; content: string }[]> {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { get, post, del } from './_base';
|
|
2
|
+
|
|
3
|
+
export interface HeadscaleNode {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
user?: string | { name: string; id?: string };
|
|
7
|
+
ipAddresses: string[];
|
|
8
|
+
createdAt: string;
|
|
9
|
+
lastSeen: string;
|
|
10
|
+
online: boolean;
|
|
11
|
+
clientVersion?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PreAuthKey {
|
|
15
|
+
id: string;
|
|
16
|
+
key: string;
|
|
17
|
+
user: string;
|
|
18
|
+
reusable: boolean;
|
|
19
|
+
ephemeral?: boolean;
|
|
20
|
+
expiration: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
used: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface HeadscaleUser {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface HeadscaleNodesResponse {
|
|
32
|
+
nodes?: HeadscaleNode[];
|
|
33
|
+
machines?: HeadscaleNode[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PreAuthKeysResponse {
|
|
37
|
+
preAuthKeys?: PreAuthKey[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UsersResponse {
|
|
41
|
+
users?: HeadscaleUser[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function fetchHeadscaleNodes(): Promise<HeadscaleNode[]> {
|
|
45
|
+
const data = await get<HeadscaleNodesResponse>('/api/headscale/node');
|
|
46
|
+
return data.nodes || data.machines || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function deleteHeadscaleNode(id: string): Promise<void> {
|
|
50
|
+
return del(`/api/headscale/node/${encodeURIComponent(id)}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function fetchPreAuthKeys(user?: string): Promise<PreAuthKey[]> {
|
|
54
|
+
const query = user ? `?user=${encodeURIComponent(user)}` : '';
|
|
55
|
+
const data = await get<PreAuthKeysResponse>(`/api/headscale/preauthkey${query}`);
|
|
56
|
+
return data.preAuthKeys || [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function createPreAuthKey(params: {
|
|
60
|
+
user: string;
|
|
61
|
+
reusable: boolean;
|
|
62
|
+
expiration: string;
|
|
63
|
+
ephemeral?: boolean;
|
|
64
|
+
}): Promise<PreAuthKey> {
|
|
65
|
+
return post<PreAuthKey>('/api/headscale/preauthkey', params);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function fetchHeadscaleUsers(): Promise<HeadscaleUser[]> {
|
|
69
|
+
const data = await get<UsersResponse>('/api/headscale/user');
|
|
70
|
+
return data.users || [];
|
|
71
|
+
}
|
package/frontend/src/api/keys.ts
CHANGED
|
@@ -143,6 +143,7 @@ export interface SecretCatalogKey {
|
|
|
143
143
|
created_at: string | null
|
|
144
144
|
updated_at: string | null
|
|
145
145
|
rotated_at: string | null
|
|
146
|
+
headscale_url?: string | null
|
|
146
147
|
usage_30d: { events: number; cost_usd: number; input_tokens: number; output_tokens: number }
|
|
147
148
|
tiers: { id: string; label: string; monthly_usd?: number | null }[]
|
|
148
149
|
}
|
|
@@ -13,7 +13,7 @@ describe('networkApi', () => {
|
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const result = await networkApi.getStats();
|
|
16
|
-
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/stats');
|
|
16
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/stats', expect.any(Object));
|
|
17
17
|
expect(result.stats.total).toBe(10);
|
|
18
18
|
});
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ describe('networkApi', () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
const result = await networkApi.getPolicy();
|
|
27
|
-
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/policy');
|
|
27
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/policy', expect.any(Object));
|
|
28
28
|
expect(result.id).toBe('network-policy');
|
|
29
29
|
});
|
|
30
30
|
|
|
@@ -35,11 +35,10 @@ describe('networkApi', () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
await networkApi.blockDomain('evil.com');
|
|
38
|
-
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block', {
|
|
38
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block', expect.objectContaining({
|
|
39
39
|
method: 'POST',
|
|
40
|
-
headers: { 'Content-Type': 'application/json' },
|
|
41
40
|
body: JSON.stringify({ domain: 'evil.com' })
|
|
42
|
-
});
|
|
41
|
+
}));
|
|
43
42
|
});
|
|
44
43
|
|
|
45
44
|
it('unblockDomain DELETEs to /api/network/block/:domain', async () => {
|
|
@@ -49,8 +48,8 @@ describe('networkApi', () => {
|
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
await networkApi.unblockDomain('evil.com');
|
|
52
|
-
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block/evil.com', {
|
|
51
|
+
expect(globalThis.fetch).toHaveBeenCalledWith('/api/network/block/evil.com', expect.objectContaining({
|
|
53
52
|
method: 'DELETE'
|
|
54
|
-
});
|
|
53
|
+
}));
|
|
55
54
|
});
|
|
56
55
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { get, post, del, apiFetch, API_BASE } from './_base';
|
|
2
|
+
|
|
1
3
|
export interface NetworkStats {
|
|
2
4
|
total: number;
|
|
3
5
|
blocked: number;
|
|
@@ -41,51 +43,42 @@ export interface AuditLogEntry {
|
|
|
41
43
|
error?: string;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
export async function getNetworkStats(): Promise<{ stats: NetworkStats, audit_count: number }> {
|
|
47
|
+
return get('/api/network/stats');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getNetworkPolicy(): Promise<NetworkPolicy> {
|
|
51
|
+
return get('/api/network/policy');
|
|
52
|
+
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
export async function updateNetworkPolicy(patch: Partial<NetworkPolicy>): Promise<any> {
|
|
55
|
+
const res = await apiFetch(API_BASE + '/api/network/policy', {
|
|
56
|
+
method: 'PUT',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify(patch),
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) throw new Error(`PUT /api/network/policy failed: ${res.status}`);
|
|
61
|
+
return res.json();
|
|
62
|
+
}
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
headers: { 'Content-Type': 'application/json' },
|
|
61
|
-
body: JSON.stringify(patch)
|
|
62
|
-
});
|
|
63
|
-
if (!res.ok) throw new Error('Failed to update network policy');
|
|
64
|
-
return res.json();
|
|
65
|
-
},
|
|
64
|
+
export async function blockDomain(domain: string): Promise<any> {
|
|
65
|
+
return post('/api/network/block', { domain });
|
|
66
|
+
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
headers: { 'Content-Type': 'application/json' },
|
|
71
|
-
body: JSON.stringify({ domain })
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok) throw new Error('Failed to block domain');
|
|
74
|
-
return res.json();
|
|
75
|
-
},
|
|
68
|
+
export async function unblockDomain(domain: string): Promise<any> {
|
|
69
|
+
return del(`/api/network/block/${encodeURIComponent(domain)}`);
|
|
70
|
+
}
|
|
76
71
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (!res.ok) throw new Error('Failed to unblock domain');
|
|
82
|
-
return res.json();
|
|
83
|
-
},
|
|
72
|
+
export async function getAuditLog(params?: { domain?: string; status?: string; since?: string }): Promise<{ audit: AuditLogEntry[] }> {
|
|
73
|
+
const query = params ? '?' + new URLSearchParams(params as any).toString() : '';
|
|
74
|
+
return get(`/api/network/audit${query}`);
|
|
75
|
+
}
|
|
84
76
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
export const networkApi = {
|
|
78
|
+
getStats: getNetworkStats,
|
|
79
|
+
getPolicy: getNetworkPolicy,
|
|
80
|
+
updateNetworkPolicy,
|
|
81
|
+
blockDomain,
|
|
82
|
+
unblockDomain,
|
|
83
|
+
getAuditLog,
|
|
91
84
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { get, post, del } from './_base';
|
|
2
|
+
|
|
3
|
+
export interface NotificationRule {
|
|
4
|
+
id: string;
|
|
5
|
+
event: string;
|
|
6
|
+
channel: 'desktop' | 'webhook' | 'email';
|
|
7
|
+
priority: 'critical' | 'high' | 'low';
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
quietHours: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NotificationEntry {
|
|
13
|
+
id: string;
|
|
14
|
+
title: string;
|
|
15
|
+
message: string;
|
|
16
|
+
channel: string;
|
|
17
|
+
status: 'delivered' | 'failed';
|
|
18
|
+
timestamp: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function listNotificationRules(): Promise<NotificationRule[]> {
|
|
22
|
+
return get('/api/notifications/rules');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function createNotificationRule(
|
|
26
|
+
rule: Omit<NotificationRule, 'id'>,
|
|
27
|
+
): Promise<NotificationRule> {
|
|
28
|
+
return post('/api/notifications/rules', rule);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function deleteNotificationRule(id: string): Promise<void> {
|
|
32
|
+
return del(`/api/notifications/rules/${encodeURIComponent(id)}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getNotificationHistory(): Promise<NotificationEntry[]> {
|
|
36
|
+
return get('/api/notifications/history');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function sendTestNotification(): Promise<void> {
|
|
40
|
+
return post('/api/notifications/test', {});
|
|
41
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { listSecrets, addSecret, triggerSync, getSyncStatus } from './secrets';
|
|
4
|
+
|
|
5
|
+
const mockFetch = vi.fn();
|
|
6
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
7
|
+
|
|
8
|
+
describe('Secrets API client', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.clearAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('listSecrets fetches secrets list metadata', async () => {
|
|
14
|
+
mockFetch.mockResolvedValue({
|
|
15
|
+
ok: true,
|
|
16
|
+
json: async () => ({ keys: [{ key: 'KEY_1', provider: 'openai' }] })
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const res = await listSecrets();
|
|
20
|
+
expect(res).toEqual([{ key: 'KEY_1', provider: 'openai' }]);
|
|
21
|
+
expect(mockFetch).toHaveBeenCalledWith('/api/secrets/list', expect.any(Object));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('addSecret sends POST to /api/secrets', async () => {
|
|
25
|
+
mockFetch.mockResolvedValue({
|
|
26
|
+
ok: true,
|
|
27
|
+
json: async () => ({ key: 'NEW_KEY', provider: 'stripe' })
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const res = await addSecret('NEW_KEY', 'val_123', { provider: 'stripe' });
|
|
31
|
+
expect(res).toEqual({ key: 'NEW_KEY', provider: 'stripe' });
|
|
32
|
+
expect(mockFetch).toHaveBeenCalledWith('/api/secrets', expect.objectContaining({
|
|
33
|
+
method: 'POST',
|
|
34
|
+
body: JSON.stringify({ key: 'NEW_KEY', value: 'val_123', provider: 'stripe' })
|
|
35
|
+
}));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('triggerSync sends POST to /api/secrets/sync/trigger', async () => {
|
|
39
|
+
mockFetch.mockResolvedValue({
|
|
40
|
+
ok: true,
|
|
41
|
+
json: async () => ({ success: true, results: [] })
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const res = await triggerSync();
|
|
45
|
+
expect(res).toEqual({ success: true, results: [] });
|
|
46
|
+
expect(mockFetch).toHaveBeenCalledWith('/api/secrets/sync/trigger', expect.objectContaining({
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: '{}'
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('getSyncStatus sends GET to /api/secrets/sync/status', async () => {
|
|
53
|
+
mockFetch.mockResolvedValue({
|
|
54
|
+
ok: true,
|
|
55
|
+
json: async () => ({ localChecksum: 'h1', nodes: [] })
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const res = await getSyncStatus();
|
|
59
|
+
expect(res).toEqual({ localChecksum: 'h1', nodes: [] });
|
|
60
|
+
expect(mockFetch).toHaveBeenCalledWith('/api/secrets/sync/status', expect.any(Object));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { get, post } from './_base';
|
|
2
|
+
import type { SecretCatalogKey } from './keys';
|
|
3
|
+
|
|
4
|
+
export interface SyncNodeStatus {
|
|
5
|
+
hostname: string;
|
|
6
|
+
ip: string;
|
|
7
|
+
status: 'synced' | 'out_of_sync' | 'unreachable';
|
|
8
|
+
checksum: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SyncStatusResponse {
|
|
12
|
+
localChecksum: string;
|
|
13
|
+
nodes: SyncNodeStatus[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TriggerSyncResponse {
|
|
17
|
+
success: boolean;
|
|
18
|
+
results: {
|
|
19
|
+
hostname: string;
|
|
20
|
+
ip: string;
|
|
21
|
+
success: boolean;
|
|
22
|
+
error?: string;
|
|
23
|
+
}[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SecretEntry = SecretCatalogKey;
|
|
27
|
+
|
|
28
|
+
export async function listSecrets(): Promise<SecretEntry[]> {
|
|
29
|
+
const data = (await get('/api/secrets/list')) as { keys: SecretEntry[] };
|
|
30
|
+
return data.keys;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function addSecret(key: string, value: string, payload: Record<string, unknown>): Promise<SecretEntry> {
|
|
34
|
+
return (await post('/api/secrets', { key, value, ...payload })) as SecretEntry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function editSecret(key: string, patch: Record<string, unknown>): Promise<SecretEntry> {
|
|
38
|
+
const { updateSecretMeta } = await import('./keys');
|
|
39
|
+
return updateSecretMeta(key, patch);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function deleteSecret(key: string): Promise<void> {
|
|
43
|
+
const { deleteProviderSecret } = await import('./keys');
|
|
44
|
+
return deleteProviderSecret(key);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function triggerSync(): Promise<TriggerSyncResponse> {
|
|
48
|
+
return (await post('/api/secrets/sync/trigger', {})) as TriggerSyncResponse;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getSyncStatus(): Promise<SyncStatusResponse> {
|
|
52
|
+
return (await get('/api/secrets/sync/status')) as SyncStatusResponse;
|
|
53
|
+
}
|
|
@@ -3,6 +3,9 @@ import { get, post, del } from './_base';
|
|
|
3
3
|
export interface WebhookConfig {
|
|
4
4
|
provider: string;
|
|
5
5
|
status: 'active' | 'inactive';
|
|
6
|
+
secret?: string;
|
|
7
|
+
endpoint_url?: string;
|
|
8
|
+
events?: string[];
|
|
6
9
|
lastReceived?: string;
|
|
7
10
|
totalCount?: number;
|
|
8
11
|
}
|
|
@@ -12,6 +15,7 @@ export interface WebhookEvent {
|
|
|
12
15
|
provider: string;
|
|
13
16
|
event_type: string;
|
|
14
17
|
received_at: string;
|
|
18
|
+
payload?: any;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export async function fetchWebhookConfigs(): Promise<WebhookConfig[]> {
|