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
|
@@ -50,7 +50,12 @@ export function VoiceInput({ onTranscript, isRecording = false }: VoiceInputProp
|
|
|
50
50
|
|
|
51
51
|
if (!supported) {
|
|
52
52
|
return (
|
|
53
|
-
<button
|
|
53
|
+
<button
|
|
54
|
+
disabled
|
|
55
|
+
className="chat-send-btn"
|
|
56
|
+
style={{ background: 'var(--bg-tertiary)', border: '1px solid var(--border)', color: 'var(--text-tertiary)', opacity: 0.5, cursor: 'not-allowed' }}
|
|
57
|
+
title="Voice input not supported"
|
|
58
|
+
>
|
|
54
59
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
|
55
60
|
</button>
|
|
56
61
|
);
|
|
@@ -59,7 +64,13 @@ export function VoiceInput({ onTranscript, isRecording = false }: VoiceInputProp
|
|
|
59
64
|
return (
|
|
60
65
|
<button
|
|
61
66
|
onClick={toggleRecording}
|
|
62
|
-
className=
|
|
67
|
+
className="chat-send-btn"
|
|
68
|
+
style={{
|
|
69
|
+
background: recording ? 'var(--error)' : 'var(--bg-tertiary)',
|
|
70
|
+
border: '1px solid var(--border)',
|
|
71
|
+
color: recording ? '#fff' : 'var(--text-secondary)',
|
|
72
|
+
transition: 'all var(--transition-fast)'
|
|
73
|
+
}}
|
|
63
74
|
title={recording ? "Stop recording" : "Start voice input"}
|
|
64
75
|
>
|
|
65
76
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
package/frontend/src/index.css
CHANGED
|
@@ -355,6 +355,156 @@ code, pre { font-family: var(--font-mono); }
|
|
|
355
355
|
|
|
356
356
|
.btn-sm { padding: 6px 11px; font-size: 12px; }
|
|
357
357
|
|
|
358
|
+
/* ─── Alerts ─────────────────────────────────────────────────────────────────── */
|
|
359
|
+
|
|
360
|
+
.alert {
|
|
361
|
+
padding: 12px 16px;
|
|
362
|
+
border-radius: var(--radius-sm);
|
|
363
|
+
font-size: 13px;
|
|
364
|
+
font-weight: 500;
|
|
365
|
+
border: 1px solid transparent;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.alert-error {
|
|
369
|
+
background: var(--error-muted);
|
|
370
|
+
border-color: rgba(248, 113, 113, 0.25);
|
|
371
|
+
color: var(--error);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.alert-success {
|
|
375
|
+
background: var(--success-muted);
|
|
376
|
+
border-color: rgba(52, 211, 153, 0.25);
|
|
377
|
+
color: var(--success);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ─── Form Controls ──────────────────────────────────────────────────────────── */
|
|
381
|
+
|
|
382
|
+
.input, .select {
|
|
383
|
+
background: var(--bg-secondary);
|
|
384
|
+
border: 1px solid var(--border);
|
|
385
|
+
border-radius: var(--radius-sm);
|
|
386
|
+
padding: 9px 12px;
|
|
387
|
+
color: var(--text-primary);
|
|
388
|
+
font-family: var(--font-sans);
|
|
389
|
+
font-size: 13px;
|
|
390
|
+
outline: none;
|
|
391
|
+
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.input:focus, .select:focus {
|
|
395
|
+
border-color: var(--accent);
|
|
396
|
+
box-shadow: 0 0 0 3px var(--accent-muted);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.input::placeholder {
|
|
400
|
+
color: var(--text-tertiary);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.select {
|
|
404
|
+
cursor: pointer;
|
|
405
|
+
appearance: none;
|
|
406
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
|
|
407
|
+
background-repeat: no-repeat;
|
|
408
|
+
background-position: right 10px center;
|
|
409
|
+
padding-right: 32px;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ─── Data Tables ────────────────────────────────────────────────────────────── */
|
|
413
|
+
|
|
414
|
+
.data-table {
|
|
415
|
+
width: 100%;
|
|
416
|
+
border-collapse: collapse;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.data-table th {
|
|
420
|
+
text-align: left;
|
|
421
|
+
padding: 12px 16px;
|
|
422
|
+
color: var(--text-secondary);
|
|
423
|
+
font-weight: 500;
|
|
424
|
+
font-size: 12px;
|
|
425
|
+
text-transform: uppercase;
|
|
426
|
+
letter-spacing: 0.04em;
|
|
427
|
+
border-bottom: 1px solid var(--border);
|
|
428
|
+
background: var(--bg-secondary);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.data-table td {
|
|
432
|
+
padding: 14px 16px;
|
|
433
|
+
border-bottom: 1px solid var(--border);
|
|
434
|
+
color: var(--text-primary);
|
|
435
|
+
font-size: 13px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.data-table tbody tr {
|
|
439
|
+
transition: background var(--transition-fast);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.data-table tbody tr:hover {
|
|
443
|
+
background: var(--bg-hover);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.data-table tr.is-leader td {
|
|
447
|
+
background: var(--accent-soft);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* ─── Page Layout ────────────────────────────────────────────────────────────── */
|
|
451
|
+
|
|
452
|
+
.page-container {
|
|
453
|
+
padding: 32px;
|
|
454
|
+
max-width: 1200px;
|
|
455
|
+
margin: 0 auto;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.page-header {
|
|
459
|
+
display: flex;
|
|
460
|
+
align-items: center;
|
|
461
|
+
justify-content: space-between;
|
|
462
|
+
margin-bottom: 32px;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.page-header h1 {
|
|
466
|
+
font-size: 24px;
|
|
467
|
+
font-weight: 600;
|
|
468
|
+
color: var(--text-primary);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ─── Badges ─────────────────────────────────────────────────────────────────── */
|
|
472
|
+
|
|
473
|
+
.badge {
|
|
474
|
+
display: inline-flex;
|
|
475
|
+
align-items: center;
|
|
476
|
+
padding: 3px 8px;
|
|
477
|
+
border-radius: 4px;
|
|
478
|
+
font-size: 11px;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
letter-spacing: 0.03em;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.badge-online, .badge-active {
|
|
484
|
+
background: var(--success-muted);
|
|
485
|
+
color: var(--success);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.badge-offline, .badge-inactive {
|
|
489
|
+
background: var(--error-muted);
|
|
490
|
+
color: var(--error);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.badge-leader {
|
|
494
|
+
background: rgba(59, 130, 246, 0.15);
|
|
495
|
+
color: var(--accent-hover);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.badge-follower {
|
|
499
|
+
background: var(--bg-hover);
|
|
500
|
+
color: var(--text-secondary);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.badge-warning {
|
|
504
|
+
background: var(--warning-muted);
|
|
505
|
+
color: var(--warning);
|
|
506
|
+
}
|
|
507
|
+
|
|
358
508
|
/* Premium surfaces */
|
|
359
509
|
.surface-premium {
|
|
360
510
|
background: linear-gradient(165deg, rgba(23, 32, 51, 0.92), rgba(12, 18, 32, 0.96));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* ─── MeshPage-specific styles ───────────────────────────────────────────────── */
|
|
2
|
+
/* Global classes (card, data-table, badge, page-container, page-header) live in index.css */
|
|
3
|
+
|
|
4
|
+
.mesh-page {
|
|
5
|
+
animation: mesh-fade-in 0.35s ease-out;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@keyframes mesh-fade-in {
|
|
9
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
10
|
+
to { opacity: 1; transform: translateY(0); }
|
|
11
|
+
}
|
|
@@ -2,7 +2,8 @@ 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, forceReElection } from '../api/mesh';
|
|
5
|
+
import { fetchLeader, fetchNodes as fetchMeshNodes, forceReElection } from '../api/mesh';
|
|
6
|
+
import { fetchHeadscaleNodes, deleteHeadscaleNode, fetchPreAuthKeys, createPreAuthKey, fetchHeadscaleUsers } from '../api/headscale';
|
|
6
7
|
|
|
7
8
|
vi.mock('../api/mesh', () => ({
|
|
8
9
|
fetchLeader: vi.fn(),
|
|
@@ -10,14 +11,23 @@ vi.mock('../api/mesh', () => ({
|
|
|
10
11
|
forceReElection: vi.fn(),
|
|
11
12
|
}));
|
|
12
13
|
|
|
14
|
+
vi.mock('../api/headscale', () => ({
|
|
15
|
+
fetchHeadscaleNodes: vi.fn(),
|
|
16
|
+
deleteHeadscaleNode: vi.fn(),
|
|
17
|
+
fetchPreAuthKeys: vi.fn(),
|
|
18
|
+
createPreAuthKey: vi.fn(),
|
|
19
|
+
fetchHeadscaleUsers: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
13
22
|
describe('MeshPage', () => {
|
|
14
23
|
beforeEach(() => {
|
|
15
24
|
vi.clearAllMocks();
|
|
25
|
+
window.confirm = vi.fn(() => true);
|
|
16
26
|
});
|
|
17
27
|
|
|
18
28
|
it('renders mesh nodes and leader', async () => {
|
|
19
29
|
vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'macmini.mesh', ip: '1.2.3.4' });
|
|
20
|
-
vi.mocked(
|
|
30
|
+
vi.mocked(fetchMeshNodes).mockResolvedValue([
|
|
21
31
|
{ hostname: 'macmini.mesh', ip: '1.2.3.4', status: 'online', role: 'leader', lastHeartbeat: '2024-01-01T00:00:00Z' },
|
|
22
32
|
{ hostname: 'laptop.mesh', ip: '1.2.3.5', status: 'offline', role: 'follower', lastHeartbeat: '2024-01-01T00:00:00Z' },
|
|
23
33
|
]);
|
|
@@ -32,7 +42,7 @@ describe('MeshPage', () => {
|
|
|
32
42
|
|
|
33
43
|
it('handles force re-election', async () => {
|
|
34
44
|
vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'macmini.mesh', ip: '1.2.3.4' });
|
|
35
|
-
vi.mocked(
|
|
45
|
+
vi.mocked(fetchMeshNodes).mockResolvedValue([]);
|
|
36
46
|
vi.mocked(forceReElection).mockResolvedValue(undefined);
|
|
37
47
|
|
|
38
48
|
render(<MeshPage />);
|
|
@@ -44,4 +54,74 @@ describe('MeshPage', () => {
|
|
|
44
54
|
await userEvent.click(screen.getByText('Force Re-Election'));
|
|
45
55
|
expect(forceReElection).toHaveBeenCalled();
|
|
46
56
|
});
|
|
57
|
+
|
|
58
|
+
it('renders Headscale Nodes tab and allows deletion', async () => {
|
|
59
|
+
vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'macmini.mesh', ip: '1.2.3.4' });
|
|
60
|
+
vi.mocked(fetchMeshNodes).mockResolvedValue([]);
|
|
61
|
+
vi.mocked(fetchHeadscaleNodes).mockResolvedValue([
|
|
62
|
+
{ id: '1', name: 'node-one', user: 'testuser', ipAddresses: ['100.64.0.1'], online: true, createdAt: '2024-01-01', lastSeen: '2024-01-01T00:00:00Z' }
|
|
63
|
+
]);
|
|
64
|
+
vi.mocked(deleteHeadscaleNode).mockResolvedValue(undefined);
|
|
65
|
+
|
|
66
|
+
render(<MeshPage />);
|
|
67
|
+
|
|
68
|
+
const tabBtn = screen.getByText('Headscale Nodes');
|
|
69
|
+
await userEvent.click(tabBtn);
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(screen.getByText('node-one')).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByText('100.64.0.1')).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const deleteBtn = screen.getByText('Delete');
|
|
77
|
+
await userEvent.click(deleteBtn);
|
|
78
|
+
expect(window.confirm).toHaveBeenCalled();
|
|
79
|
+
expect(deleteHeadscaleNode).toHaveBeenCalledWith('1');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders Pre-Auth Keys tab and supports key generation', async () => {
|
|
83
|
+
vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'macmini.mesh', ip: '1.2.3.4' });
|
|
84
|
+
vi.mocked(fetchMeshNodes).mockResolvedValue([]);
|
|
85
|
+
vi.mocked(fetchPreAuthKeys).mockResolvedValue([
|
|
86
|
+
{ id: 'key1', key: 'hspkey_xyz', user: 'default', reusable: true, expiration: '2099-12-31T23:59:59Z', createdAt: '2024-01-01', used: false }
|
|
87
|
+
]);
|
|
88
|
+
vi.mocked(createPreAuthKey).mockResolvedValue({} as any);
|
|
89
|
+
|
|
90
|
+
render(<MeshPage />);
|
|
91
|
+
|
|
92
|
+
const tabBtn = screen.getByText('Pre-Auth Keys');
|
|
93
|
+
await userEvent.click(tabBtn);
|
|
94
|
+
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(screen.getByText('hspkey_xyz')).toBeInTheDocument();
|
|
97
|
+
expect(screen.getByText('Generate Key')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const generateBtn = screen.getByText('Generate Key');
|
|
101
|
+
await userEvent.click(generateBtn);
|
|
102
|
+
|
|
103
|
+
expect(createPreAuthKey).toHaveBeenCalledWith({
|
|
104
|
+
user: 'default',
|
|
105
|
+
reusable: false,
|
|
106
|
+
ephemeral: false,
|
|
107
|
+
expiration: '2099-12-31T23:59:59Z'
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renders Users tab', async () => {
|
|
112
|
+
vi.mocked(fetchLeader).mockResolvedValue({ hostname: 'macmini.mesh', ip: '1.2.3.4' });
|
|
113
|
+
vi.mocked(fetchMeshNodes).mockResolvedValue([]);
|
|
114
|
+
vi.mocked(fetchHeadscaleUsers).mockResolvedValue([
|
|
115
|
+
{ id: 'u1', name: 'alice', createdAt: '2024-01-01T00:00:00Z' }
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
render(<MeshPage />);
|
|
119
|
+
|
|
120
|
+
const tabBtn = screen.getByText('Users');
|
|
121
|
+
await userEvent.click(tabBtn);
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByText('alice')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
47
127
|
});
|