total-recall-brain 3.21.2 → 3.22.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.
Files changed (81) hide show
  1. package/frontend/dist/assets/index-C7_Xpk27.css +1 -0
  2. package/frontend/dist/assets/{index-D0XeeXhG.js → index-DJjuZ3t3.js} +106 -106
  3. package/frontend/dist/index.html +2 -2
  4. package/frontend/src/App.spec.tsx +1 -1
  5. package/frontend/src/api/_base.ts +3 -3
  6. package/frontend/src/api/embeddings.ts +46 -0
  7. package/frontend/src/api/mesh.ts +47 -0
  8. package/frontend/src/api/network.spec.ts +9 -9
  9. package/frontend/src/api/network.ts +4 -4
  10. package/frontend/src/api/notifications.ts +5 -4
  11. package/frontend/src/api/webhooks.ts +1 -1
  12. package/frontend/src/components/BrainSelector.spec.tsx +4 -4
  13. package/frontend/src/components/BrainSelector.tsx +1 -1
  14. package/frontend/src/components/ContextualHelp.spec.tsx +14 -14
  15. package/frontend/src/components/ContextualHelp.tsx +3 -1
  16. package/frontend/src/components/DocumentTable.spec.tsx +1 -1
  17. package/frontend/src/components/EmbeddingProviderPanel.spec.tsx +127 -0
  18. package/frontend/src/components/EmbeddingProviderPanel.tsx +177 -0
  19. package/frontend/src/components/Graph3D.spec.tsx +4 -4
  20. package/frontend/src/components/Graph3D.tsx +3 -3
  21. package/frontend/src/components/MobilePairing.spec.tsx +1 -1
  22. package/frontend/src/components/MobilePairing.tsx +1 -1
  23. package/frontend/src/components/UsageChart.spec.tsx +4 -4
  24. package/frontend/src/components/UsageChart.tsx +1 -1
  25. package/frontend/src/components/VoiceInput.spec.tsx +6 -6
  26. package/frontend/src/components/VoiceInput.tsx +19 -4
  27. package/frontend/src/components/brand/BrandMark.spec.tsx +3 -4
  28. package/frontend/src/index.css +6 -0
  29. package/frontend/src/pages/AutomationsPage.spec.tsx +4 -4
  30. package/frontend/src/pages/ChatPage.spec.tsx +3 -3
  31. package/frontend/src/pages/CollabPage.spec.tsx +1 -2
  32. package/frontend/src/pages/DesignDocsPage.spec.tsx +1 -1
  33. package/frontend/src/pages/FilesPage.spec.tsx +2 -2
  34. package/frontend/src/pages/GraphPage.spec.tsx +1 -1
  35. package/frontend/src/pages/HealthPage.spec.tsx +2 -2
  36. package/frontend/src/pages/HelpPage.spec.tsx +5 -5
  37. package/frontend/src/pages/InboxPage.spec.tsx +4 -4
  38. package/frontend/src/pages/InstructionsPage.spec.tsx +5 -5
  39. package/frontend/src/pages/IntegrationsPage.spec.tsx +7 -7
  40. package/frontend/src/pages/MemoryPage.spec.tsx +2 -2
  41. package/frontend/src/pages/MeshPage.spec.tsx +111 -2
  42. package/frontend/src/pages/MeshPage.tsx +95 -11
  43. package/frontend/src/pages/NetworkPage.spec.tsx +7 -8
  44. package/frontend/src/pages/NetworkPage.tsx +12 -12
  45. package/frontend/src/pages/NotificationsPage.tsx +9 -9
  46. package/frontend/src/pages/OnboardingPage.spec.tsx +7 -7
  47. package/frontend/src/pages/OpenWikiPage.tsx +3 -3
  48. package/frontend/src/pages/RulesPage.tsx +3 -3
  49. package/frontend/src/pages/SecretsPage.spec.tsx +5 -5
  50. package/frontend/src/pages/SecretsPage.tsx +13 -14
  51. package/frontend/src/pages/SettingsPage.spec.tsx +6 -6
  52. package/frontend/src/pages/SettingsPage.tsx +2 -2
  53. package/frontend/src/pages/SkillsPage.spec.tsx +1 -2
  54. package/frontend/src/pages/SkillsPage.tsx +3 -3
  55. package/frontend/src/pages/TasksPage.spec.tsx +3 -3
  56. package/frontend/src/pages/UsagePage.spec.tsx +2 -2
  57. package/frontend/src/pages/UsagePage.tsx +1 -1
  58. package/frontend/src/pages/WebhooksPage.spec.tsx +1 -1
  59. package/frontend/src/pages/WebhooksPage.tsx +19 -19
  60. package/frontend/src/setupTests.ts +4 -4
  61. package/package.json +1 -1
  62. package/scaffold/.agent/skills/total-recall/SKILL.md +92 -0
  63. package/scaffold/.agent/skills/total-recall/modules/agents/agents.yml +15 -13
  64. package/scaffold/.agent/skills/total-recall/research-queue.jsonl +0 -1
  65. package/src/cli/secret.mjs +93 -12
  66. package/src/core/browser-session.mjs +185 -0
  67. package/src/core/daemon-loop.mjs +28 -1
  68. package/src/core/embeddings.mjs +52 -10
  69. package/src/core/headscale-client.mjs +227 -0
  70. package/src/core/mesh-enroll.mjs +439 -0
  71. package/src/core/ollama-embeddings.mjs +336 -0
  72. package/src/core/provider-catalog.mjs +175 -6
  73. package/src/core/provider-rotation-recipes.mjs +299 -0
  74. package/src/core/rotation-capability.mjs +189 -0
  75. package/src/core/secrets-rotate.mjs +192 -0
  76. package/src/server/rest.mjs +2 -0
  77. package/src/server/route-manifest.json +20 -0
  78. package/src/server/routes/embeddings.mjs +111 -0
  79. package/src/server/routes/headscale.mjs +21 -81
  80. package/src/server/routes/mesh.mjs +41 -0
  81. package/frontend/dist/assets/index-B4RASWbV.css +0 -1
@@ -2,7 +2,13 @@ import { render, screen, waitFor } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { describe, it, expect, vi, beforeEach } from 'vitest';
4
4
  import { MeshPage } from './MeshPage';
5
- import { fetchLeader, fetchNodes as fetchMeshNodes, refreshElection } from '../api/mesh';
5
+ import {
6
+ fetchLeader,
7
+ fetchNodes as fetchMeshNodes,
8
+ refreshElection,
9
+ fetchEnrollmentStatus,
10
+ enrollThisNode,
11
+ } from '../api/mesh';
6
12
  import { fetchHeadscaleNodes, deleteHeadscaleNode, fetchPreAuthKeys, createPreAuthKey, fetchHeadscaleUsers } from '../api/headscale';
7
13
 
8
14
  vi.mock('../api/mesh', () => ({
@@ -47,6 +53,21 @@ vi.mock('../api/mesh', () => ({
47
53
  }),
48
54
  fetchElectionHistory: vi.fn().mockResolvedValue([]),
49
55
  logElectionObservation: vi.fn().mockResolvedValue({ success: true, recorded: true }),
56
+ fetchEnrollmentStatus: vi.fn().mockResolvedValue({
57
+ state: 'enrolled',
58
+ enrolled: true,
59
+ backend_state: 'Running',
60
+ auth_url: null,
61
+ ips: ['100.64.0.1'],
62
+ hostname: 'node-a',
63
+ login_server: 'https://control.example.org',
64
+ can_auto_enroll: true,
65
+ auto_enroll_blocked_reason: null,
66
+ auto_enroll_enabled: true,
67
+ client_available: true,
68
+ checked_at: new Date().toISOString(),
69
+ }),
70
+ enrollThisNode: vi.fn(),
50
71
  }));
51
72
 
52
73
  vi.mock('../api/headscale', () => ({
@@ -135,7 +156,7 @@ describe('MeshPage', () => {
135
156
  vi.mocked(fetchPreAuthKeys).mockResolvedValue([
136
157
  { id: 'key1', key: 'hspkey_xyz', user: 'default', reusable: true, expiration: '2099-12-31T23:59:59Z', createdAt: '2024-01-01', used: false }
137
158
  ]);
138
- vi.mocked(createPreAuthKey).mockResolvedValue({} as any);
159
+ vi.mocked(createPreAuthKey).mockResolvedValue({} as never);
139
160
 
140
161
  render(<MeshPage />);
141
162
 
@@ -174,4 +195,92 @@ describe('MeshPage', () => {
174
195
  expect(screen.getByText('alice')).toBeInTheDocument();
175
196
  });
176
197
  });
198
+
199
+ describe('enrollment banner', () => {
200
+ const notEnrolled = {
201
+ state: 'needs_login' as const,
202
+ enrolled: false,
203
+ backend_state: 'NeedsLogin',
204
+ auth_url: 'https://control.example.org/register/abc123',
205
+ ips: [],
206
+ hostname: 'node-a',
207
+ login_server: 'https://control.example.org',
208
+ can_auto_enroll: true,
209
+ auto_enroll_blocked_reason: null,
210
+ auto_enroll_enabled: true,
211
+ client_available: true,
212
+ checked_at: new Date().toISOString(),
213
+ };
214
+
215
+ beforeEach(() => {
216
+ vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'node-a.mesh', ip: '100.64.0.1' });
217
+ vi.mocked(fetchMeshNodes).mockResolvedValue([]);
218
+ });
219
+
220
+ it('stays hidden when this node is already on the mesh', async () => {
221
+ vi.mocked(fetchEnrollmentStatus).mockResolvedValue({ ...notEnrolled, state: 'enrolled', enrolled: true });
222
+ render(<MeshPage />);
223
+ await waitFor(() => expect(screen.getByText('Mesh Operations Center')).toBeInTheDocument());
224
+ expect(screen.queryByTestId('enrollment-banner')).not.toBeInTheDocument();
225
+ });
226
+
227
+ it('prompts to enroll and shows the pending registration link', async () => {
228
+ vi.mocked(fetchEnrollmentStatus).mockResolvedValue(notEnrolled);
229
+ render(<MeshPage />);
230
+
231
+ const banner = await screen.findByTestId('enrollment-banner');
232
+ expect(banner).toHaveTextContent('This node is not on the mesh');
233
+ expect(screen.getByRole('link', { name: /approve this node/i })).toHaveAttribute(
234
+ 'href',
235
+ 'https://control.example.org/register/abc123',
236
+ );
237
+ expect(screen.getByRole('button', { name: /enroll this node/i })).toBeInTheDocument();
238
+ });
239
+
240
+ it('enrolls on click and re-syncs enrollment state from the server', async () => {
241
+ const enrolled = { ...notEnrolled, state: 'enrolled' as const, enrolled: true, ips: ['100.64.0.7'], auth_url: null };
242
+ // The page must trust the server, not its own optimistic state: the
243
+ // status call flips to `enrolled` only once the node is really up.
244
+ vi.mocked(fetchEnrollmentStatus).mockResolvedValueOnce(notEnrolled).mockResolvedValue(enrolled);
245
+ vi.mocked(enrollThisNode).mockResolvedValue({
246
+ ok: true,
247
+ changed: true,
248
+ state: 'enrolled',
249
+ method: 'preauth-key',
250
+ status: enrolled,
251
+ });
252
+
253
+ render(<MeshPage />);
254
+ await userEvent.click(await screen.findByRole('button', { name: /enroll this node/i }));
255
+
256
+ await waitFor(() => expect(enrollThisNode).toHaveBeenCalled());
257
+ await waitFor(() => expect(screen.queryByTestId('enrollment-banner')).not.toBeInTheDocument());
258
+ });
259
+
260
+ it('explains what to configure when no automatic path exists', async () => {
261
+ vi.mocked(fetchEnrollmentStatus).mockResolvedValue({
262
+ ...notEnrolled,
263
+ can_auto_enroll: false,
264
+ auto_enroll_blocked_reason: 'no-headscale-api-key',
265
+ login_server: null,
266
+ });
267
+ render(<MeshPage />);
268
+ const banner = await screen.findByTestId('enrollment-banner');
269
+ expect(banner).toHaveTextContent(/No control server configured/i);
270
+ });
271
+
272
+ it('hides the enroll button when no tailscale client is installed', async () => {
273
+ vi.mocked(fetchEnrollmentStatus).mockResolvedValue({
274
+ ...notEnrolled,
275
+ state: 'client_unavailable',
276
+ client_available: false,
277
+ auth_url: null,
278
+ can_auto_enroll: false,
279
+ });
280
+ render(<MeshPage />);
281
+ const banner = await screen.findByTestId('enrollment-banner');
282
+ expect(banner).toHaveTextContent(/No Tailscale client detected/i);
283
+ expect(screen.queryByRole('button', { name: /enroll this node/i })).not.toBeInTheDocument();
284
+ });
285
+ });
177
286
  });
@@ -10,11 +10,21 @@ import {
10
10
  registerLanMeshNodes,
11
11
  fetchElectionHistory,
12
12
  logElectionObservation,
13
+ fetchEnrollmentStatus,
14
+ enrollThisNode,
13
15
  } from '../api/mesh';
14
16
  import { fetchHeadscaleNodes, fetchPreAuthKeys, fetchHeadscaleUsers, createPreAuthKey, deleteHeadscaleNode } from '../api/headscale';
15
- import type { MeshNode, LeaderInfo, LanHost, MeshInterfaceSummary, DeviceIoProfile } from '../api/mesh';
17
+ import type {
18
+ MeshNode,
19
+ LeaderInfo,
20
+ LanHost,
21
+ MeshInterfaceSummary,
22
+ DeviceIoProfile,
23
+ EnrollmentStatus,
24
+ } from '../api/mesh';
16
25
  import type { HeadscaleNode, PreAuthKey as HeadscalePreAuthKey, HeadscaleUser } from '../api/headscale';
17
26
  import { MeshTopology } from '../components/MeshTopology';
27
+ import { EmbeddingProviderPanel } from '../components/EmbeddingProviderPanel';
18
28
  import { LatencySparkline } from '../components/LatencySparkline';
19
29
  import './MeshPage.css';
20
30
 
@@ -45,6 +55,9 @@ export function MeshPage() {
45
55
  const [lanTrCount, setLanTrCount] = useState(0);
46
56
  const [deviceIo, setDeviceIo] = useState<DeviceIoProfile | null>(null);
47
57
  const [uiHints, setUiHints] = useState<string[]>([]);
58
+ const [enrollment, setEnrollment] = useState<EnrollmentStatus | null>(null);
59
+ const [enrollBusy, setEnrollBusy] = useState(false);
60
+ const [enrollMsg, setEnrollMsg] = useState<string | null>(null);
48
61
  const [lanRegisterBusy, setLanRegisterBusy] = useState(false);
49
62
  const [lanRegisterMsg, setLanRegisterMsg] = useState<string | null>(null);
50
63
  const prevLeaderRef = useRef<string | null>(null);
@@ -145,6 +158,10 @@ export function MeshPage() {
145
158
  setUiHints(ioRes.ui_hints || ioRes.io?.ui_hints || []);
146
159
  })
147
160
  .catch(() => { /* optional */ });
161
+ // Enrollment state drives the join banner; never fail the page on it.
162
+ fetchEnrollmentStatus()
163
+ .then(setEnrollment)
164
+ .catch(() => { /* optional */ });
148
165
  pollMsRef.current = POLL_BASE_MS;
149
166
  } else if (activeTab === 'headscale-nodes') {
150
167
  const nodes = await fetchHeadscaleNodes();
@@ -156,8 +173,8 @@ export function MeshPage() {
156
173
  const users = await fetchHeadscaleUsers();
157
174
  setHsUsers(users);
158
175
  }
159
- } catch (err: any) {
160
- setError(err.message || `Failed to load ${activeTab} data`);
176
+ } catch (err) {
177
+ setError(err instanceof Error ? err.message : `Failed to load ${activeTab} data`);
161
178
  // Back off polling on errors to reduce long-task pressure.
162
179
  pollMsRef.current = Math.min(POLL_MAX_MS, pollMsRef.current * 1.5);
163
180
  } finally {
@@ -188,8 +205,8 @@ export function MeshPage() {
188
205
  const l = await refreshElection();
189
206
  recordLeaderChange(l, 'manual refresh');
190
207
  await loadData();
191
- } catch (err: any) {
192
- setError(err.message || 'Failed to refresh election state');
208
+ } catch (err) {
209
+ setError(err instanceof Error ? err.message : 'Failed to refresh election state');
193
210
  }
194
211
  }
195
212
 
@@ -202,13 +219,36 @@ export function MeshPage() {
202
219
  `Registered ${res.registration?.written_count ?? 0} of ${res.registration?.attempted ?? 0} TR-reachable LAN hosts as mesh_node entities.`,
203
220
  );
204
221
  await loadData();
205
- } catch (err: any) {
206
- setError(err.message || 'LAN register failed');
222
+ } catch (err) {
223
+ setError(err instanceof Error ? err.message : 'LAN register failed');
207
224
  } finally {
208
225
  setLanRegisterBusy(false);
209
226
  }
210
227
  }
211
228
 
229
+ async function handleEnroll() {
230
+ setEnrollBusy(true);
231
+ setEnrollMsg(null);
232
+ try {
233
+ const result = await enrollThisNode();
234
+ setEnrollment(result.status);
235
+ if (result.ok && result.changed) {
236
+ setEnrollMsg(`Enrolled on the mesh via ${result.method || 'control server'}.`);
237
+ } else if (result.ok) {
238
+ setEnrollMsg('This node is already enrolled.');
239
+ } else if (result.auth_url) {
240
+ setEnrollMsg('Approve the registration link below to finish joining.');
241
+ } else {
242
+ setEnrollMsg(result.hint || result.reason || 'Enrollment did not complete.');
243
+ }
244
+ await loadData();
245
+ } catch (err) {
246
+ setError(err instanceof Error ? err.message : 'Enrollment failed');
247
+ } finally {
248
+ setEnrollBusy(false);
249
+ }
250
+ }
251
+
212
252
  async function handleCreateKey(e: React.FormEvent) {
213
253
  e.preventDefault();
214
254
  try {
@@ -219,8 +259,8 @@ export function MeshPage() {
219
259
  expiration: newKeyExpiration
220
260
  });
221
261
  await loadData();
222
- } catch (err: any) {
223
- setError(err.message || 'Failed to create pre-auth key');
262
+ } catch (err) {
263
+ setError(err instanceof Error ? err.message : 'Failed to create pre-auth key');
224
264
  }
225
265
  }
226
266
 
@@ -229,8 +269,8 @@ export function MeshPage() {
229
269
  try {
230
270
  await deleteHeadscaleNode(id);
231
271
  await loadData();
232
- } catch (err: any) {
233
- setError(err.message || 'Failed to delete node');
272
+ } catch (err) {
273
+ setError(err instanceof Error ? err.message : 'Failed to delete node');
234
274
  }
235
275
  }
236
276
 
@@ -278,8 +318,52 @@ export function MeshPage() {
278
318
 
279
319
  {error && <div className="alert alert-error" style={{ marginBottom: '24px' }}>{error}</div>}
280
320
 
321
+ {enrollment && !enrollment.enrolled && (
322
+ <div className="alert alert-warning mesh-enrollment-banner" style={{ marginBottom: '24px' }} data-testid="enrollment-banner">
323
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 16, flexWrap: 'wrap' }}>
324
+ <div>
325
+ <strong>This node is not on the mesh</strong>
326
+ <div style={{ fontSize: 13, marginTop: 4 }}>
327
+ {enrollment.state === 'client_unavailable'
328
+ ? 'No Tailscale client detected. Install Tailscale (or set TR_TAILSCALE_BIN) to join the mesh.'
329
+ : enrollment.can_auto_enroll
330
+ ? `Ready to enroll automatically against ${enrollment.login_server}.`
331
+ : enrollment.login_server
332
+ ? `Control server ${enrollment.login_server} — add a Headscale API key to enroll without approving in a browser.`
333
+ : 'No control server configured. Add a Headscale API key with its URL, or set TR_HEADSCALE_URL.'}
334
+ </div>
335
+ {enrollment.auth_url && (
336
+ <div style={{ fontSize: 13, marginTop: 8 }}>
337
+ Pending registration:{' '}
338
+ <a href={enrollment.auth_url} target="_blank" rel="noreferrer noopener">
339
+ approve this node
340
+ </a>
341
+ </div>
342
+ )}
343
+ {enrollMsg && (
344
+ <div style={{ fontSize: 13, marginTop: 8, color: 'var(--accent-hover)' }}>{enrollMsg}</div>
345
+ )}
346
+ </div>
347
+ {enrollment.client_available && (
348
+ <button
349
+ type="button"
350
+ className="btn btn-primary"
351
+ onClick={handleEnroll}
352
+ disabled={enrollBusy}
353
+ >
354
+ {enrollBusy ? 'Enrolling…' : 'Enroll this node'}
355
+ </button>
356
+ )}
357
+ </div>
358
+ </div>
359
+ )}
360
+
281
361
  {activeTab === 'mesh' && (
282
362
  <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
363
+ {/* Which mesh node actually serves embeddings, and which model it uses. */}
364
+ <div className="card">
365
+ <EmbeddingProviderPanel />
366
+ </div>
283
367
  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
284
368
  <div className="card">
285
369
  <h3 style={{ marginTop: 0, marginBottom: 12 }}>Topology</h3>
@@ -33,14 +33,14 @@ vi.mock('../api/network', () => {
33
33
  describe('NetworkPage', () => {
34
34
  beforeEach(() => {
35
35
  vi.clearAllMocks();
36
- (getNetworkStats as any).mockResolvedValue({
37
- stats: { active: 2, queueLength: 0, errors: 0, timeouts: 0 }
38
- });
39
- (getNetworkPolicy as any).mockResolvedValue({
36
+ vi.mocked(getNetworkStats).mockResolvedValue({
37
+ stats: { active: 2, queueLength: 0, errors: 0, timeouts: 0 } as any
38
+ } as any);
39
+ vi.mocked(getNetworkPolicy).mockResolvedValue({
40
40
  blocked_domains: ['evil.com'],
41
41
  max_global_concurrency: 20
42
- });
43
- (getAuditLog as any).mockResolvedValue({
42
+ } as any);
43
+ vi.mocked(getAuditLog).mockResolvedValue({
44
44
  audit: []
45
45
  });
46
46
  });
@@ -54,10 +54,9 @@ describe('NetworkPage', () => {
54
54
  render(<NetworkPage />);
55
55
 
56
56
  await waitFor(() => {
57
- expect(screen.getAllByText(/Network Firewall/i)[0]).toBeInTheDocument();
57
+ expect(screen.getByText(/Active Connections/i)).toBeInTheDocument();
58
58
  });
59
59
 
60
- expect(screen.getByText(/Active Connections/i)).toBeInTheDocument();
61
60
  expect(screen.getByText('2')).toBeInTheDocument();
62
61
  expect(screen.getByText('evil.com')).toBeInTheDocument();
63
62
  });
@@ -53,7 +53,7 @@ export default function NetworkPage() {
53
53
  : undefined
54
54
  ),
55
55
  ]);
56
- setStats(statsRes.stats || statsRes as any);
56
+ setStats(statsRes.stats || statsRes as unknown as NetworkStats);
57
57
  if (policyRes) {
58
58
  setPolicy(policyRes);
59
59
  if (!limitsChanged) {
@@ -66,15 +66,15 @@ export default function NetworkPage() {
66
66
  }
67
67
  setAudit(auditRes.audit || []);
68
68
  setError('');
69
- } catch (err: any) {
70
- setError(err.message || 'Failed to load network data');
69
+ } catch (err) {
70
+ setError(err instanceof Error ? err.message : 'Failed to load network data');
71
71
  } finally {
72
72
  setLoading(false);
73
73
  }
74
74
  }, [filterDomain, filterStatus, limitsChanged]);
75
75
 
76
76
  useEffect(() => {
77
- loadData();
77
+ setTimeout(() => void loadData(), 0);
78
78
  }, [loadData]);
79
79
 
80
80
  useEffect(() => {
@@ -94,8 +94,8 @@ export default function NetworkPage() {
94
94
  setSuccess(`Blocked ${domain}`);
95
95
  setTimeout(() => setSuccess(''), 3000);
96
96
  await loadData();
97
- } catch (err: any) {
98
- setError(err.message);
97
+ } catch (err) {
98
+ setError(err instanceof Error ? err.message : String(err));
99
99
  }
100
100
  };
101
101
 
@@ -105,8 +105,8 @@ export default function NetworkPage() {
105
105
  setSuccess(`Unblocked ${domain}`);
106
106
  setTimeout(() => setSuccess(''), 3000);
107
107
  await loadData();
108
- } catch (err: any) {
109
- setError(err.message);
108
+ } catch (err) {
109
+ setError(err instanceof Error ? err.message : String(err));
110
110
  }
111
111
  };
112
112
 
@@ -117,8 +117,8 @@ export default function NetworkPage() {
117
117
  setSuccess('Limits updated');
118
118
  setTimeout(() => setSuccess(''), 3000);
119
119
  await loadData();
120
- } catch (err: any) {
121
- setError(err.message);
120
+ } catch (err) {
121
+ setError(err instanceof Error ? err.message : String(err));
122
122
  }
123
123
  };
124
124
 
@@ -129,8 +129,8 @@ export default function NetworkPage() {
129
129
  setSuccess(`Switched to ${!policy.whitelist_mode ? 'whitelist' : 'blocklist'} mode`);
130
130
  setTimeout(() => setSuccess(''), 3000);
131
131
  await loadData();
132
- } catch (err: any) {
133
- setError(err.message);
132
+ } catch (err) {
133
+ setError(err instanceof Error ? err.message : String(err));
134
134
  }
135
135
  };
136
136
 
@@ -41,15 +41,15 @@ export function NotificationsPage() {
41
41
  const [r, h] = await Promise.all([listNotificationRules(), getNotificationHistory()]);
42
42
  setRules(r);
43
43
  setHistory(h);
44
- } catch (err: any) {
45
- setError(err.message || 'Failed to load notifications data');
44
+ } catch (err) {
45
+ setError(err instanceof Error ? err.message : 'Failed to load notifications data');
46
46
  } finally {
47
47
  setLoading(false);
48
48
  }
49
49
  }
50
50
 
51
51
  useEffect(() => {
52
- loadData();
52
+ setTimeout(() => { void loadData() }, 0);
53
53
  const interval = setInterval(loadData, 15000);
54
54
  return () => clearInterval(interval);
55
55
  }, []);
@@ -70,8 +70,8 @@ export function NotificationsPage() {
70
70
  setNewPriority('high');
71
71
  setNewQuietHours(false);
72
72
  await loadData();
73
- } catch (err: any) {
74
- setError(err.message || 'Failed to create notification rule');
73
+ } catch (err) {
74
+ setError(err instanceof Error ? err.message : 'Failed to create notification rule');
75
75
  }
76
76
  }
77
77
 
@@ -80,8 +80,8 @@ export function NotificationsPage() {
80
80
  try {
81
81
  await deleteNotificationRule(id);
82
82
  await loadData();
83
- } catch (err: any) {
84
- setError(err.message || 'Failed to delete notification rule');
83
+ } catch (err) {
84
+ setError(err instanceof Error ? err.message : 'Failed to delete notification rule');
85
85
  }
86
86
  }
87
87
 
@@ -90,8 +90,8 @@ export function NotificationsPage() {
90
90
  await sendTestNotification();
91
91
  setSuccess('Test notification sent');
92
92
  setTimeout(() => setSuccess(null), 3000);
93
- } catch (err: any) {
94
- setError(err.message || 'Failed to send test notification');
93
+ } catch (err) {
94
+ setError(err instanceof Error ? err.message : 'Failed to send test notification');
95
95
  }
96
96
  }
97
97
 
@@ -1,4 +1,4 @@
1
- // @ts-nocheck
1
+
2
2
  import { describe, it, expect, vi, beforeEach } from 'vitest';
3
3
  import { render, screen, waitFor, fireEvent } from '@testing-library/react';
4
4
  import { BrowserRouter } from 'react-router-dom';
@@ -18,7 +18,7 @@ describe('OnboardingPage', () => {
18
18
  };
19
19
 
20
20
  it('renders welcome step first', async () => {
21
- vi.mocked(api.listMemory).mockResolvedValue([] as any);
21
+ vi.mocked(api.listMemory).mockResolvedValue([] as never);
22
22
 
23
23
  renderWithRouter(<OnboardingPage />);
24
24
 
@@ -28,8 +28,8 @@ describe('OnboardingPage', () => {
28
28
  });
29
29
 
30
30
  it('navigates to next step', async () => {
31
- vi.mocked(api.listMemory).mockResolvedValue([] as any);
32
- vi.mocked(api.scanEnvSecrets).mockResolvedValue({ candidates: [] } as any);
31
+ vi.mocked(api.listMemory).mockResolvedValue([] as never);
32
+ vi.mocked(api.scanEnvSecrets).mockResolvedValue({ candidates: [] } as never);
33
33
 
34
34
  renderWithRouter(<OnboardingPage />);
35
35
 
@@ -42,10 +42,10 @@ describe('OnboardingPage', () => {
42
42
  });
43
43
 
44
44
  it('handles scan import mode', async () => {
45
- vi.mocked(api.listMemory).mockResolvedValue([] as any);
45
+ vi.mocked(api.listMemory).mockResolvedValue([] as never);
46
46
  vi.mocked(api.scanEnvSecrets).mockResolvedValue({
47
- candidates: [{ key: 'TEST_KEY', provider: 'test', masked: 'sk-***', source_label: '.env' }]
48
- });
47
+ candidates: [{ key: 'TEST_KEY', provider: 'test', masked: 'sk-***', source_label: '.env' } as any]
48
+ } as any);
49
49
 
50
50
  renderWithRouter(<OnboardingPage />);
51
51
 
@@ -110,7 +110,7 @@ try {
110
110
  // Re-fetch nodes after background ingest completes
111
111
  void fetchNodes(activeBrainId || 'global');
112
112
  });
113
- } catch (e) {
113
+ } catch {
114
114
  // ignore
115
115
  }
116
116
 
@@ -118,8 +118,8 @@ try {
118
118
  };
119
119
 
120
120
  void initFetch();
121
- void fetchHealthData();
122
- const interval = setInterval(fetchHealthData, 15000);
121
+ setTimeout(() => { void fetchHealthData(); }, 0);
122
+ const interval = setInterval(() => { void fetchHealthData(); }, 15000);
123
123
  return () => clearInterval(interval);
124
124
  }, [activeBrainId]);
125
125
 
@@ -20,7 +20,7 @@ export default function RulesPage({ activeBrainId }: { activeBrainId: string })
20
20
  fetchRules();
21
21
  }, [activeBrainId]);
22
22
 
23
- const fetchRules = async () => {
23
+ async function fetchRules() {
24
24
  setLoading(true);
25
25
  setError(null);
26
26
  try {
@@ -51,7 +51,7 @@ export default function RulesPage({ activeBrainId }: { activeBrainId: string })
51
51
  headers: { 'Content-Type': 'application/json' },
52
52
  });
53
53
  fetchRules();
54
- } catch (err) {
54
+ } catch {
55
55
  alert('Failed to archive rule');
56
56
  }
57
57
  };
@@ -64,7 +64,7 @@ export default function RulesPage({ activeBrainId }: { activeBrainId: string })
64
64
  headers: { 'Content-Type': 'application/json' },
65
65
  });
66
66
  fetchRules();
67
- } catch (err) {
67
+ } catch {
68
68
  alert('Failed to restore rule');
69
69
  }
70
70
  };
@@ -12,15 +12,15 @@ describe('SecretsPage', () => {
12
12
  });
13
13
 
14
14
  it('renders without crashing', async () => {
15
- vi.mocked(api.fetchConfigJson).mockResolvedValue({} as any);
16
- vi.mocked(api.fetchSecretsCatalog).mockResolvedValue({ keys: [], by_provider: [], store: 'mock-store' } as any);
17
- vi.mocked(api.listApiKeys).mockResolvedValue({ keys: [] } as any);
18
- vi.mocked(api.fetchWebAuthnStatus).mockResolvedValue({} as any);
15
+ vi.mocked(api.fetchConfigJson).mockResolvedValue({} as never);
16
+ vi.mocked(api.fetchSecretsCatalog).mockResolvedValue({ keys: [], by_provider: [], store: 'mock-store' } as never);
17
+ vi.mocked(api.listApiKeys).mockResolvedValue({ keys: [] } as never);
18
+ vi.mocked(api.fetchWebAuthnStatus).mockResolvedValue({} as never);
19
19
  vi.mocked(api.fetchGeminiModels).mockResolvedValue([]);
20
20
  vi.mocked(api.fetchClaudeModels).mockResolvedValue([]);
21
21
  vi.mocked(api.fetchOpenaiModels).mockResolvedValue([]);
22
22
  vi.mocked(api.fetchOpenRouterModels).mockResolvedValue([]);
23
- vi.mocked(api.getSyncStatus).mockResolvedValue({ nodes: [], localChecksum: 'abc' } as any);
23
+ vi.mocked(api.getSyncStatus).mockResolvedValue({ nodes: [], localChecksum: 'abc' } as never);
24
24
 
25
25
  render(
26
26
  <BrowserRouter>
@@ -231,8 +231,8 @@ export default function SecretsPage() {
231
231
  const syncData = await getSyncStatus()
232
232
  setSyncNodes(syncData.nodes || [])
233
233
  setLocalChecksum(syncData.localChecksum)
234
- } catch (err: any) {
235
- setError(err.message || 'Failed to load sync status')
234
+ } catch (err) {
235
+ setError(err instanceof Error ? err.message : 'Failed to load sync status')
236
236
  }
237
237
  }, [])
238
238
 
@@ -249,10 +249,10 @@ export default function SecretsPage() {
249
249
  setSyncLogs(prev => [{ time: new Date().toLocaleTimeString(), event: msg, status: 'success' }, ...prev])
250
250
 
251
251
  await loadSyncData()
252
- } catch (err: any) {
253
- const msg = `Sync trigger failed: ${err.message || err}`
252
+ } catch (err) {
253
+ const msg = `Sync trigger failed: ${err instanceof Error ? err.message : String(err)}`
254
254
  setSyncLogs(prev => [{ time: new Date().toLocaleTimeString(), event: msg, status: 'error' }, ...prev])
255
- setError(err.message || 'Failed to trigger synchronization')
255
+ setError(err instanceof Error ? err.message : 'Failed to trigger synchronization')
256
256
  } finally {
257
257
  setSyncing(false)
258
258
  }
@@ -287,8 +287,8 @@ export default function SecretsPage() {
287
287
  setNewSecretKeyName('')
288
288
  setNewSecretVal('')
289
289
  await loadCatalog()
290
- } catch (err: any) {
291
- setError(err.message || 'Failed to create secret')
290
+ } catch (err) {
291
+ setError(err instanceof Error ? err.message : 'Failed to create secret')
292
292
  } finally {
293
293
  setSaving(false)
294
294
  }
@@ -296,7 +296,7 @@ export default function SecretsPage() {
296
296
 
297
297
  useEffect(() => {
298
298
  if (tab === 'sync') {
299
- void loadSyncData()
299
+ setTimeout(() => void loadSyncData(), 0)
300
300
  }
301
301
  }, [tab, loadSyncData])
302
302
 
@@ -360,12 +360,11 @@ export default function SecretsPage() {
360
360
  }, [])
361
361
 
362
362
  useEffect(() => {
363
- // eslint-disable-next-line
364
- void loadCatalog()
365
-
366
- void loadPats()
367
-
368
- void loadWebAuthn()
363
+ setTimeout(() => {
364
+ void loadCatalog()
365
+ void loadPats()
366
+ void loadWebAuthn()
367
+ }, 0)
369
368
  }, [loadCatalog, loadPats, loadWebAuthn])
370
369
 
371
370