@auxiora/dashboard 1.0.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 (97) hide show
  1. package/LICENSE +191 -0
  2. package/dist/auth.d.ts +13 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/cloud-types.d.ts +71 -0
  7. package/dist/cloud-types.d.ts.map +1 -0
  8. package/dist/cloud-types.js +2 -0
  9. package/dist/cloud-types.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/router.d.ts +13 -0
  15. package/dist/router.d.ts.map +1 -0
  16. package/dist/router.js +2250 -0
  17. package/dist/router.js.map +1 -0
  18. package/dist/types.d.ts +314 -0
  19. package/dist/types.d.ts.map +1 -0
  20. package/dist/types.js +7 -0
  21. package/dist/types.js.map +1 -0
  22. package/dist-ui/assets/index-BfY0i5jw.css +1 -0
  23. package/dist-ui/assets/index-CXpk9mvw.js +60 -0
  24. package/dist-ui/icon.svg +59 -0
  25. package/dist-ui/index.html +20 -0
  26. package/package.json +32 -0
  27. package/src/auth.ts +83 -0
  28. package/src/cloud-types.ts +63 -0
  29. package/src/index.ts +5 -0
  30. package/src/router.ts +2494 -0
  31. package/src/types.ts +269 -0
  32. package/tests/auth.test.ts +51 -0
  33. package/tests/cloud-router.test.ts +249 -0
  34. package/tests/desktop-router.test.ts +151 -0
  35. package/tests/router.test.ts +388 -0
  36. package/tests/trust-router.test.ts +170 -0
  37. package/tsconfig.json +12 -0
  38. package/tsconfig.tsbuildinfo +1 -0
  39. package/ui/index.html +19 -0
  40. package/ui/node_modules/.bin/browserslist +17 -0
  41. package/ui/node_modules/.bin/tsc +17 -0
  42. package/ui/node_modules/.bin/tsserver +17 -0
  43. package/ui/node_modules/.bin/vite +17 -0
  44. package/ui/package.json +23 -0
  45. package/ui/public/icon.svg +59 -0
  46. package/ui/src/App.tsx +63 -0
  47. package/ui/src/api.ts +238 -0
  48. package/ui/src/components/ActivityFeed.tsx +123 -0
  49. package/ui/src/components/BehaviorHealth.tsx +105 -0
  50. package/ui/src/components/DataTable.tsx +39 -0
  51. package/ui/src/components/Layout.tsx +160 -0
  52. package/ui/src/components/PasswordStrength.tsx +31 -0
  53. package/ui/src/components/SetupProgress.tsx +26 -0
  54. package/ui/src/components/StatusBadge.tsx +12 -0
  55. package/ui/src/components/ThemeSelector.tsx +39 -0
  56. package/ui/src/contexts/ThemeContext.tsx +58 -0
  57. package/ui/src/hooks/useApi.ts +19 -0
  58. package/ui/src/hooks/usePolling.ts +8 -0
  59. package/ui/src/main.tsx +16 -0
  60. package/ui/src/pages/AuditLog.tsx +36 -0
  61. package/ui/src/pages/Behaviors.tsx +426 -0
  62. package/ui/src/pages/Chat.tsx +688 -0
  63. package/ui/src/pages/Login.tsx +64 -0
  64. package/ui/src/pages/Overview.tsx +56 -0
  65. package/ui/src/pages/Sessions.tsx +26 -0
  66. package/ui/src/pages/SettingsAmbient.tsx +185 -0
  67. package/ui/src/pages/SettingsConnections.tsx +201 -0
  68. package/ui/src/pages/SettingsNotifications.tsx +241 -0
  69. package/ui/src/pages/SetupAppearance.tsx +45 -0
  70. package/ui/src/pages/SetupChannels.tsx +143 -0
  71. package/ui/src/pages/SetupComplete.tsx +31 -0
  72. package/ui/src/pages/SetupConnections.tsx +80 -0
  73. package/ui/src/pages/SetupDashboardPassword.tsx +50 -0
  74. package/ui/src/pages/SetupIdentity.tsx +68 -0
  75. package/ui/src/pages/SetupPersonality.tsx +78 -0
  76. package/ui/src/pages/SetupProvider.tsx +65 -0
  77. package/ui/src/pages/SetupVault.tsx +50 -0
  78. package/ui/src/pages/SetupWelcome.tsx +19 -0
  79. package/ui/src/pages/UnlockVault.tsx +56 -0
  80. package/ui/src/pages/Webhooks.tsx +158 -0
  81. package/ui/src/pages/settings/Appearance.tsx +63 -0
  82. package/ui/src/pages/settings/Channels.tsx +138 -0
  83. package/ui/src/pages/settings/Identity.tsx +61 -0
  84. package/ui/src/pages/settings/Personality.tsx +54 -0
  85. package/ui/src/pages/settings/PersonalityEditor.tsx +577 -0
  86. package/ui/src/pages/settings/Provider.tsx +537 -0
  87. package/ui/src/pages/settings/Security.tsx +111 -0
  88. package/ui/src/styles/global.css +2308 -0
  89. package/ui/src/styles/themes/index.css +7 -0
  90. package/ui/src/styles/themes/monolith.css +125 -0
  91. package/ui/src/styles/themes/nebula.css +90 -0
  92. package/ui/src/styles/themes/neon.css +149 -0
  93. package/ui/src/styles/themes/polar.css +151 -0
  94. package/ui/src/styles/themes/signal.css +163 -0
  95. package/ui/src/styles/themes/terra.css +146 -0
  96. package/ui/tsconfig.json +14 -0
  97. package/ui/vite.config.ts +20 -0
package/ui/src/api.ts ADDED
@@ -0,0 +1,238 @@
1
+ const BASE = '/api/v1/dashboard';
2
+
3
+ async function fetchApi<T>(path: string, options?: RequestInit): Promise<T> {
4
+ const res = await fetch(`${BASE}${path}`, {
5
+ credentials: 'include',
6
+ headers: { 'Content-Type': 'application/json' },
7
+ ...options,
8
+ });
9
+
10
+ if (res.status === 401) {
11
+ window.location.href = '/dashboard/login';
12
+ throw new Error('Unauthorized');
13
+ }
14
+
15
+ if (!res.ok) {
16
+ const body = await res.json().catch(() => ({}));
17
+ throw new Error(body.error || `HTTP ${res.status}`);
18
+ }
19
+
20
+ return res.json();
21
+ }
22
+
23
+ export const api = {
24
+ checkAuth: () => fetchApi<{ authenticated: boolean }>('/auth/check'),
25
+ login: (pw: string) =>
26
+ fetchApi<{ success: boolean }>('/auth/login', {
27
+ method: 'POST',
28
+ body: JSON.stringify({ password: pw }),
29
+ }),
30
+ logout: () => fetchApi<{ success: boolean }>('/auth/logout', { method: 'POST' }),
31
+ getBehaviors: () => fetchApi<{ data: any[] }>('/behaviors'),
32
+ patchBehavior: (id: string, updates: Record<string, unknown>) =>
33
+ fetchApi<{ data: any }>(`/behaviors/${id}`, {
34
+ method: 'PATCH',
35
+ body: JSON.stringify(updates),
36
+ }),
37
+ deleteBehavior: (id: string) =>
38
+ fetchApi<{ data: any }>(`/behaviors/${id}`, { method: 'DELETE' }),
39
+ createBehavior: (input: Record<string, unknown>) =>
40
+ fetchApi<{ data: any }>('/behaviors', {
41
+ method: 'POST',
42
+ body: JSON.stringify(input),
43
+ }),
44
+ getWebhooks: () => fetchApi<{ data: any[] }>('/webhooks'),
45
+ patchWebhook: (id: string, updates: Record<string, unknown>) =>
46
+ fetchApi<{ data: any }>(`/webhooks/${id}`, {
47
+ method: 'PATCH',
48
+ body: JSON.stringify(updates),
49
+ }),
50
+ deleteWebhook: (id: string) =>
51
+ fetchApi<{ data: any }>(`/webhooks/${id}`, { method: 'DELETE' }),
52
+ createWebhook: (input: Record<string, unknown>) =>
53
+ fetchApi<{ data: any }>('/webhooks', {
54
+ method: 'POST',
55
+ body: JSON.stringify(input),
56
+ }),
57
+ getSessions: () => fetchApi<{ data: any[] }>('/sessions'),
58
+ getAudit: (params?: { type?: string; limit?: number }) => {
59
+ const query = new URLSearchParams();
60
+ if (params?.type) query.set('type', params.type);
61
+ if (params?.limit) query.set('limit', String(params.limit));
62
+ const qs = query.toString();
63
+ return fetchApi<{ data: any[] }>(`/audit${qs ? `?${qs}` : ''}`);
64
+ },
65
+ getStatus: () => fetchApi<{ data: any }>('/status'),
66
+ getSetupStatus: () =>
67
+ fetchApi<{ needsSetup: boolean; completedSteps: string[]; vaultUnlocked: boolean; dashboardPasswordSet: boolean; agentName: string }>('/setup/status'),
68
+ setupVault: (password: string) =>
69
+ fetchApi<{ success: boolean }>('/setup/vault', { method: 'POST', body: JSON.stringify({ password }) }),
70
+ setupDashboardPassword: (password: string) =>
71
+ fetchApi<{ success: boolean }>('/setup/dashboard-password', { method: 'POST', body: JSON.stringify({ password }) }),
72
+ setupIdentity: (name: string, pronouns: string, vibe?: string) =>
73
+ fetchApi<{ success: boolean }>('/setup/identity', { method: 'POST', body: JSON.stringify({ name, pronouns, vibe }) }),
74
+ getSetupTemplates: () =>
75
+ fetchApi<{ data: Array<{ id: string; name: string; description: string; preview: string }> }>('/setup/templates'),
76
+ setupPersonality: (template: string) =>
77
+ fetchApi<{ success: boolean }>('/setup/personality', { method: 'POST', body: JSON.stringify({ template }) }),
78
+ setupProvider: (provider: string, apiKey?: string, endpoint?: string) =>
79
+ fetchApi<{ success: boolean }>('/setup/provider', { method: 'POST', body: JSON.stringify({ provider, apiKey, endpoint }) }),
80
+ setupChannels: (channels: Array<{ type: string; enabled: boolean; credentials?: Record<string, string> }>) =>
81
+ fetchApi<{ success: boolean }>('/setup/channels', { method: 'POST', body: JSON.stringify({ channels }) }),
82
+ completeSetup: () =>
83
+ fetchApi<{ success: boolean }>('/setup/complete', { method: 'POST' }),
84
+
85
+ // Settings API
86
+ getModels: () => fetchApi<{ providers: any[]; routing: any; cost: any }>('/models'),
87
+ getIdentity: () => fetchApi<{ data: { name: string; pronouns: string } }>('/identity'),
88
+ updateIdentity: (name: string, pronouns: string) =>
89
+ fetchApi<{ success: boolean }>('/identity', {
90
+ method: 'POST',
91
+ body: JSON.stringify({ name, pronouns }),
92
+ }),
93
+ getPersonality: () =>
94
+ fetchApi<{ data: { template: { id: string; name: string } | null } }>('/personality'),
95
+ getTemplates: () =>
96
+ fetchApi<{ data: Array<{ id: string; name: string; description: string; preview: string }> }>('/personality/templates'),
97
+ updatePersonality: (template: string) =>
98
+ fetchApi<{ success: boolean }>('/personality', {
99
+ method: 'POST',
100
+ body: JSON.stringify({ template }),
101
+ }),
102
+ getPersonalityFull: () =>
103
+ fetchApi<{ data: {
104
+ name: string; pronouns: string; avatar: string | null; vibe: string;
105
+ tone: { warmth: number; directness: number; humor: number; formality: number };
106
+ errorStyle: string; expertise: string[]; catchphrases: Record<string, string>;
107
+ boundaries: { neverJokeAbout: string[]; neverAdviseOn: string[] };
108
+ customInstructions: string; soulContent: string | null; activeTemplate: string | null;
109
+ } }>('/personality/full'),
110
+ updatePersonalityFull: (data: Record<string, unknown>) =>
111
+ fetchApi<{ success: boolean }>('/personality/full', {
112
+ method: 'PUT',
113
+ body: JSON.stringify(data),
114
+ }),
115
+ updateProvider: (provider: string, apiKey?: string, endpoint?: string) =>
116
+ fetchApi<{ success: boolean }>('/provider', {
117
+ method: 'POST',
118
+ body: JSON.stringify({ provider, apiKey, endpoint }),
119
+ }),
120
+ configureProvider: (provider: string, apiKey?: string, endpoint?: string) =>
121
+ fetchApi<{ success: boolean }>('/provider/configure', {
122
+ method: 'POST',
123
+ body: JSON.stringify({ provider, apiKey, endpoint }),
124
+ }),
125
+ // Claude OAuth
126
+ startClaudeOAuth: () =>
127
+ fetchApi<{ authUrl: string }>('/provider/claude-oauth/start', { method: 'POST' }),
128
+ completeClaudeOAuth: (code: string) =>
129
+ fetchApi<{ success: boolean }>('/provider/claude-oauth/callback', {
130
+ method: 'POST',
131
+ body: JSON.stringify({ code }),
132
+ }),
133
+ disconnectClaudeOAuth: () =>
134
+ fetchApi<{ success: boolean }>('/provider/claude-oauth/disconnect', { method: 'POST' }),
135
+ getClaudeOAuthStatus: () =>
136
+ fetchApi<{ connected: boolean }>('/provider/claude-oauth/status'),
137
+ updateRouting: (primary: string, fallback?: string) =>
138
+ fetchApi<{ success: boolean }>('/provider/routing', {
139
+ method: 'POST',
140
+ body: JSON.stringify({ primary, fallback }),
141
+ }),
142
+ setProviderModel: (provider: string, model: string) =>
143
+ fetchApi<{ success: boolean }>('/provider/model', {
144
+ method: 'POST',
145
+ body: JSON.stringify({ provider, model }),
146
+ }),
147
+ getSessionMessages: () =>
148
+ fetchApi<{ data: Array<{ id: string; role: string; content: string; timestamp: number }> }>('/session/messages'),
149
+ getChannels: () => fetchApi<{ data: { connected: string[]; configured: Array<{ type: string; enabled: boolean }> } }>('/channels'),
150
+ updateChannels: (channels: Array<{ type: string; enabled: boolean; credentials?: Record<string, string> }>) =>
151
+ fetchApi<{ success: boolean }>('/channels', {
152
+ method: 'POST',
153
+ body: JSON.stringify({ channels }),
154
+ }),
155
+ changeDashboardPassword: (oldPassword: string, newPassword: string) =>
156
+ fetchApi<{ success: boolean }>('/security/dashboard-password', {
157
+ method: 'POST',
158
+ body: JSON.stringify({ oldPassword, newPassword }),
159
+ }),
160
+ changeVaultPassword: (newPassword: string) =>
161
+ fetchApi<{ success: boolean }>('/security/vault-password', {
162
+ method: 'POST',
163
+ body: JSON.stringify({ newPassword }),
164
+ }),
165
+
166
+ // Connector OAuth API
167
+ getConnectorStatus: (connectorId: string) =>
168
+ fetchApi<{ data: { connectorId: string; hasCredentials: boolean; connected: boolean; expiresAt?: number } }>(
169
+ `/connectors/${connectorId}/status`,
170
+ ),
171
+ saveConnectorCredentials: (connectorId: string, clientId: string, clientSecret: string) =>
172
+ fetchApi<{ success: boolean; oauthUrl?: string }>(`/connectors/${connectorId}/credentials`, {
173
+ method: 'POST',
174
+ body: JSON.stringify({ clientId, clientSecret }),
175
+ }),
176
+ disconnectConnector: (connectorId: string) =>
177
+ fetchApi<{ success: boolean }>(`/connectors/${connectorId}/disconnect`, {
178
+ method: 'POST',
179
+ }),
180
+
181
+ // Ambient config
182
+ getAmbientConfig: () =>
183
+ fetchApi<{ data: any }>('/ambient/config'),
184
+ updateAmbientConfig: (config: Record<string, unknown>) =>
185
+ fetchApi<{ success: boolean }>('/ambient/config', {
186
+ method: 'POST',
187
+ body: JSON.stringify(config),
188
+ }),
189
+
190
+ // Appearance
191
+ getAppearance: () =>
192
+ fetchApi<{ data: { theme: string } }>('/appearance'),
193
+ updateAppearance: (theme: string) =>
194
+ fetchApi<{ success: boolean }>('/appearance', {
195
+ method: 'POST',
196
+ body: JSON.stringify({ theme }),
197
+ }),
198
+
199
+ // Chat management
200
+ getChats: (archived?: boolean) => {
201
+ const qs = archived ? '?archived=true' : '';
202
+ return fetchApi<{ data: any[]; total: number }>(`/chats${qs}`);
203
+ },
204
+ createNewChat: (title?: string) =>
205
+ fetchApi<{ data: any }>('/chats', {
206
+ method: 'POST',
207
+ body: JSON.stringify({ title }),
208
+ }),
209
+ getChatMessages: (chatId: string) =>
210
+ fetchApi<{ data: Array<{ id: string; role: string; content: string; timestamp: number }> }>(`/chats/${chatId}/messages`),
211
+ renameChat: (chatId: string, title: string) =>
212
+ fetchApi<{ data: any }>(`/chats/${chatId}`, {
213
+ method: 'PATCH',
214
+ body: JSON.stringify({ title }),
215
+ }),
216
+ archiveChat: (chatId: string) =>
217
+ fetchApi<{ data: any }>(`/chats/${chatId}`, {
218
+ method: 'PATCH',
219
+ body: JSON.stringify({ archived: true }),
220
+ }),
221
+ deleteChatThread: (chatId: string) =>
222
+ fetchApi<{ data: any }>(`/chats/${chatId}`, { method: 'DELETE' }),
223
+
224
+ // Notifications
225
+ getNotifications: () =>
226
+ fetchApi<{ data: any[] }>('/notifications'),
227
+ dismissNotification: (id: string) =>
228
+ fetchApi<{ data: { dismissed: boolean } }>(`/notifications/${id}/dismiss`, {
229
+ method: 'POST',
230
+ }),
231
+ getNotificationPreferences: () =>
232
+ fetchApi<{ data: any }>('/notifications/preferences'),
233
+ updateNotificationPreferences: (prefs: Record<string, unknown>) =>
234
+ fetchApi<{ success: boolean }>('/notifications/preferences', {
235
+ method: 'POST',
236
+ body: JSON.stringify(prefs),
237
+ }),
238
+ };
@@ -0,0 +1,123 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { api } from '../api';
3
+
4
+ interface ActivityEvent {
5
+ timestamp: string;
6
+ sequence: number;
7
+ event: string;
8
+ details: Record<string, unknown>;
9
+ }
10
+
11
+ const EVENT_LABELS: Record<string, (d: Record<string, unknown>) => string> = {
12
+ 'behavior.executed': (d) => d.success ? `Behavior ran successfully` : `Behavior failed: ${d.error ?? 'unknown'}`,
13
+ 'behavior.created': () => 'Behavior created',
14
+ 'behavior.updated': () => 'Behavior updated',
15
+ 'behavior.deleted': () => 'Behavior deleted',
16
+ 'behavior.paused': () => 'Behavior paused',
17
+ 'behavior.failed': (d) => `Behavior failed: ${d.error ?? 'unknown'}`,
18
+ 'message.received': (d) => `Message received on ${d.channelType ?? 'unknown'}`,
19
+ 'message.sent': (d) => `Message sent to ${d.channelType ?? 'unknown'}`,
20
+ 'message.filtered': () => 'Message filtered',
21
+ 'channel.connected': (d) => `${d.channelType ?? 'Channel'} connected`,
22
+ 'channel.disconnected': (d) => `${d.channelType ?? 'Channel'} disconnected`,
23
+ 'channel.error': (d) => `${d.channelType ?? 'Channel'} error`,
24
+ 'webhook.triggered': (d) => `Webhook triggered: ${d.path ?? ''}`,
25
+ 'webhook.received': () => 'Webhook received',
26
+ 'webhook.created': () => 'Webhook created',
27
+ 'webhook.deleted': () => 'Webhook deleted',
28
+ 'webhook.error': (d) => `Webhook error: ${d.error ?? 'unknown'}`,
29
+ 'system.startup': () => 'System started',
30
+ 'system.shutdown': () => 'System shut down',
31
+ 'system.error': (d) => `System error: ${d.error ?? 'unknown'}`,
32
+ 'auth.login': () => 'Login',
33
+ 'auth.logout': () => 'Logout',
34
+ };
35
+
36
+ const CATEGORY_COLORS: Record<string, string> = {
37
+ behavior: 'var(--accent)',
38
+ message: '#3b82f6',
39
+ channel: 'var(--success)',
40
+ webhook: '#f97316',
41
+ system: 'var(--text-secondary)',
42
+ auth: 'var(--danger)',
43
+ };
44
+
45
+ function getCategory(event: string): string {
46
+ return event.split('.')[0];
47
+ }
48
+
49
+ function getLabel(event: string, details: Record<string, unknown>): string {
50
+ const fn = EVENT_LABELS[event];
51
+ if (fn) return fn(details);
52
+ return event.replace(/\./g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
53
+ }
54
+
55
+ function timeAgo(iso: string): string {
56
+ const diff = (Date.now() - new Date(iso).getTime()) / 1000;
57
+ if (diff < 60) return 'just now';
58
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
59
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
60
+ return `${Math.floor(diff / 86400)}d ago`;
61
+ }
62
+
63
+ const MAX_EVENTS = 100;
64
+
65
+ export function ActivityFeed() {
66
+ const [events, setEvents] = useState<ActivityEvent[]>([]);
67
+ const wsRef = useRef<WebSocket | null>(null);
68
+
69
+ useEffect(() => {
70
+ api.getAudit({ limit: 50 }).then((res) => {
71
+ if (res.data) {
72
+ const PREFIXES = ['behavior.', 'message.', 'channel.', 'webhook.', 'system.', 'auth.login', 'auth.logout'];
73
+ const filtered = res.data
74
+ .filter((e: any) => PREFIXES.some((p) => e.event.startsWith(p)))
75
+ .reverse();
76
+ setEvents(filtered);
77
+ }
78
+ }).catch(() => {});
79
+ }, []);
80
+
81
+ useEffect(() => {
82
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
83
+ const host = import.meta.env.DEV ? 'localhost:18800' : window.location.host;
84
+ const ws = new WebSocket(`${protocol}//${host}/`);
85
+ wsRef.current = ws;
86
+
87
+ ws.onmessage = (evt) => {
88
+ try {
89
+ const msg = JSON.parse(evt.data);
90
+ if (msg.type === 'activity' && msg.payload) {
91
+ setEvents((prev) => {
92
+ const next = [msg.payload, ...prev];
93
+ return next.length > MAX_EVENTS ? next.slice(0, MAX_EVENTS) : next;
94
+ });
95
+ }
96
+ } catch { /* ignore non-JSON */ }
97
+ };
98
+
99
+ return () => { ws.close(); };
100
+ }, []);
101
+
102
+ return (
103
+ <div className="activity-feed">
104
+ <h3 className="mc-section-title">Live Activity</h3>
105
+ <div className="activity-feed-list">
106
+ {events.length === 0 && (
107
+ <div className="activity-empty">No recent activity</div>
108
+ )}
109
+ {events.map((e, i) => {
110
+ const cat = getCategory(e.event);
111
+ const color = CATEGORY_COLORS[cat] ?? 'var(--text-secondary)';
112
+ return (
113
+ <div key={`${e.sequence}-${i}`} className="activity-item">
114
+ <span className="activity-dot" style={{ background: color }} />
115
+ <span className="activity-label">{getLabel(e.event, e.details)}</span>
116
+ <span className="activity-time">{timeAgo(e.timestamp)}</span>
117
+ </div>
118
+ );
119
+ })}
120
+ </div>
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,105 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { api } from '../api';
3
+
4
+ interface Behavior {
5
+ id: string;
6
+ type: 'scheduled' | 'monitor' | 'one-shot';
7
+ status: 'active' | 'paused' | 'deleted' | 'missed';
8
+ action: string;
9
+ runCount: number;
10
+ failCount: number;
11
+ maxFailures: number;
12
+ lastRun?: string;
13
+ lastResult?: string;
14
+ }
15
+
16
+ function timeAgo(iso: string): string {
17
+ const diff = (Date.now() - new Date(iso).getTime()) / 1000;
18
+ if (diff < 60) return 'just now';
19
+ if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
20
+ if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
21
+ return `${Math.floor(diff / 86400)}d ago`;
22
+ }
23
+
24
+ function getHealthColor(b: Behavior): string {
25
+ if (b.status === 'paused' && b.failCount >= b.maxFailures) return 'var(--danger)';
26
+ if (b.status === 'paused') return 'var(--text-secondary)';
27
+ if (b.failCount > 0) return 'var(--warning)';
28
+ return 'var(--success)';
29
+ }
30
+
31
+ function getHealthLabel(b: Behavior): string {
32
+ if (b.status === 'paused' && b.failCount >= b.maxFailures) return 'Auto-paused';
33
+ if (b.status === 'paused') return 'Paused';
34
+ if (b.failCount > 0) return `${b.failCount} failures`;
35
+ return 'Healthy';
36
+ }
37
+
38
+ const TYPE_LABELS: Record<string, string> = {
39
+ scheduled: 'Sched',
40
+ monitor: 'Monitor',
41
+ 'one-shot': 'Once',
42
+ };
43
+
44
+ export function BehaviorHealth() {
45
+ const [behaviors, setBehaviors] = useState<Behavior[]>([]);
46
+ const wsRef = useRef<WebSocket | null>(null);
47
+
48
+ useEffect(() => {
49
+ api.getBehaviors().then((res) => {
50
+ if (res.data) {
51
+ setBehaviors(res.data.filter((b: any) => b.status !== 'deleted'));
52
+ }
53
+ }).catch(() => {});
54
+ }, []);
55
+
56
+ useEffect(() => {
57
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
58
+ const host = import.meta.env.DEV ? 'localhost:18800' : window.location.host;
59
+ const ws = new WebSocket(`${protocol}//${host}/`);
60
+ wsRef.current = ws;
61
+
62
+ ws.onmessage = (evt) => {
63
+ try {
64
+ const msg = JSON.parse(evt.data);
65
+ if (msg.type === 'activity' && msg.payload?.event?.startsWith('behavior.')) {
66
+ api.getBehaviors().then((res) => {
67
+ if (res.data) {
68
+ setBehaviors(res.data.filter((b: any) => b.status !== 'deleted'));
69
+ }
70
+ }).catch(() => {});
71
+ }
72
+ } catch { /* ignore */ }
73
+ };
74
+
75
+ return () => { ws.close(); };
76
+ }, []);
77
+
78
+ return (
79
+ <div className="behavior-health">
80
+ <h3 className="mc-section-title">Behavior Health</h3>
81
+ {behaviors.length === 0 && (
82
+ <div className="bh-empty">No behaviors configured</div>
83
+ )}
84
+ <div className="bh-list">
85
+ {behaviors.map((b) => (
86
+ <div key={b.id} className="bh-card">
87
+ <div className="bh-card-header">
88
+ <span className="bh-dot" style={{ background: getHealthColor(b) }} />
89
+ <span className="bh-action">{b.action.length > 60 ? b.action.slice(0, 57) + '...' : b.action}</span>
90
+ <span className="bh-type-badge">{TYPE_LABELS[b.type] ?? b.type}</span>
91
+ </div>
92
+ <div className="bh-card-meta">
93
+ <span className="bh-health-label" style={{ color: getHealthColor(b) }}>{getHealthLabel(b)}</span>
94
+ <span className="bh-stat">{b.runCount} runs</span>
95
+ {b.lastRun && <span className="bh-stat">{timeAgo(b.lastRun)}</span>}
96
+ </div>
97
+ {b.lastResult && (
98
+ <div className="bh-result">{b.lastResult.length > 120 ? b.lastResult.slice(0, 117) + '...' : b.lastResult}</div>
99
+ )}
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,39 @@
1
+ interface Column<T> {
2
+ key: string;
3
+ label: string;
4
+ render?: (row: T) => React.ReactNode;
5
+ }
6
+
7
+ interface DataTableProps<T> {
8
+ columns: Column<T>[];
9
+ rows: T[];
10
+ keyField: string;
11
+ actions?: (row: T) => React.ReactNode;
12
+ }
13
+
14
+ export function DataTable<T extends Record<string, any>>({ columns, rows, keyField, actions }: DataTableProps<T>) {
15
+ return (
16
+ <table className="data-table">
17
+ <thead>
18
+ <tr>
19
+ {columns.map((col) => <th key={col.key}>{col.label}</th>)}
20
+ {actions && <th>Actions</th>}
21
+ </tr>
22
+ </thead>
23
+ <tbody>
24
+ {rows.length === 0 ? (
25
+ <tr><td colSpan={columns.length + (actions ? 1 : 0)} className="empty-row">No data</td></tr>
26
+ ) : (
27
+ rows.map((row) => (
28
+ <tr key={row[keyField]}>
29
+ {columns.map((col) => (
30
+ <td key={col.key}>{col.render ? col.render(row) : String(row[col.key] ?? '')}</td>
31
+ ))}
32
+ {actions && <td className="actions-cell">{actions(row)}</td>}
33
+ </tr>
34
+ ))
35
+ )}
36
+ </tbody>
37
+ </table>
38
+ );
39
+ }
@@ -0,0 +1,160 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { NavLink, Outlet, useNavigate } from 'react-router-dom';
3
+ import { useApi } from '../hooks/useApi';
4
+ import { usePolling } from '../hooks/usePolling';
5
+ import { api } from '../api';
6
+
7
+ const CHANNEL_TYPES = ['webchat', 'discord', 'telegram', 'slack', 'twilio', 'matrix', 'signal', 'teams', 'whatsapp', 'email'] as const;
8
+
9
+ export function Layout() {
10
+ const [checking, setChecking] = useState(true);
11
+ const [ready, setReady] = useState(false);
12
+ const [agentName, setAgentName] = useState('Auxiora');
13
+ const navigate = useNavigate();
14
+
15
+ useEffect(() => {
16
+ api.getSetupStatus()
17
+ .then(status => {
18
+ if (status.agentName) setAgentName(status.agentName);
19
+ if (status.needsSetup) {
20
+ navigate('/setup', { replace: true });
21
+ } else if (!status.vaultUnlocked) {
22
+ navigate('/unlock', { replace: true });
23
+ } else {
24
+ setReady(true);
25
+ }
26
+ })
27
+ .catch(() => {})
28
+ .finally(() => setChecking(false));
29
+ }, []);
30
+
31
+ // Only make authenticated calls once vault/setup check passes
32
+ const { data: status, refresh } = useApi(() => ready ? api.getStatus() : Promise.resolve(null), [ready]);
33
+ const { data: sessions, refresh: refreshSessions } = useApi(() => ready ? api.getSessions() : Promise.resolve(null), [ready]);
34
+ usePolling(() => { if (ready) { refresh(); refreshSessions(); } });
35
+
36
+ if (checking) return null;
37
+
38
+ // Derive connected channel types from active sessions
39
+ const connectedChannels = new Set<string>();
40
+ if (sessions?.data) {
41
+ for (const s of sessions.data) {
42
+ if (s.channelType) connectedChannels.add(s.channelType);
43
+ }
44
+ }
45
+
46
+ // Only show channels that have connections or are commonly configured
47
+ const visibleChannels = CHANNEL_TYPES.filter(ch =>
48
+ connectedChannels.has(ch) || ch === 'webchat'
49
+ );
50
+
51
+ return (
52
+ <div className="layout">
53
+ <nav className="sidebar">
54
+ <div className="sidebar-header">
55
+ <h1>{agentName}</h1>
56
+ </div>
57
+ <ul className="nav-list">
58
+ {/* MAIN */}
59
+ <div className="nav-group">
60
+ <div className="nav-group-label">Main</div>
61
+ <li>
62
+ <NavLink to="/chat" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
63
+ Chat
64
+ </NavLink>
65
+ </li>
66
+ <li>
67
+ <NavLink to="/" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'} end>
68
+ Mission Control
69
+ </NavLink>
70
+ </li>
71
+ </div>
72
+
73
+ {/* CHANNELS */}
74
+ <div className="nav-group">
75
+ <div className="nav-group-label">Channels</div>
76
+ {visibleChannels.map(ch => (
77
+ <li key={ch}>
78
+ <NavLink to="/settings/channels" className="nav-link">
79
+ {ch.charAt(0).toUpperCase() + ch.slice(1)}
80
+ <span className={`channel-dot ${connectedChannels.has(ch) ? 'connected' : 'disconnected'}`} />
81
+ </NavLink>
82
+ </li>
83
+ ))}
84
+ </div>
85
+
86
+ {/* MANAGEMENT */}
87
+ <div className="nav-group">
88
+ <div className="nav-group-label">Management</div>
89
+ <li>
90
+ <NavLink to="/behaviors" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
91
+ Behaviors
92
+ </NavLink>
93
+ </li>
94
+ <li>
95
+ <NavLink to="/webhooks" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
96
+ Webhooks
97
+ </NavLink>
98
+ </li>
99
+ </div>
100
+
101
+ {/* SETTINGS */}
102
+ <div className="nav-group">
103
+ <div className="nav-group-label">Settings</div>
104
+ <li>
105
+ <NavLink to="/settings/personality" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
106
+ Personality
107
+ </NavLink>
108
+ </li>
109
+ <li>
110
+ <NavLink to="/settings/provider" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
111
+ Provider
112
+ </NavLink>
113
+ </li>
114
+ <li>
115
+ <NavLink to="/settings/channels" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
116
+ Channels
117
+ </NavLink>
118
+ </li>
119
+ <li>
120
+ <NavLink to="/settings/connections" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
121
+ Connections
122
+ </NavLink>
123
+ </li>
124
+ <li>
125
+ <NavLink to="/settings/ambient" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
126
+ Ambient
127
+ </NavLink>
128
+ </li>
129
+ <li>
130
+ <NavLink to="/settings/appearance" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
131
+ Appearance
132
+ </NavLink>
133
+ </li>
134
+ <li>
135
+ <NavLink to="/settings/notifications" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
136
+ Notifications
137
+ </NavLink>
138
+ </li>
139
+ <li>
140
+ <NavLink to="/settings/security" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
141
+ Security
142
+ </NavLink>
143
+ </li>
144
+ <li>
145
+ <NavLink to="/settings/audit" className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}>
146
+ Audit Log
147
+ </NavLink>
148
+ </li>
149
+ </div>
150
+ </ul>
151
+ <button className="logout-btn" onClick={() => api.logout().then(() => { window.location.href = '/dashboard/login'; })}>
152
+ Logout
153
+ </button>
154
+ </nav>
155
+ <main className="content">
156
+ <Outlet />
157
+ </main>
158
+ </div>
159
+ );
160
+ }