Dashboard.svelte raw
1 <script>
2 import { onMount, onDestroy } from 'svelte';
3 import { userSigner, userPubkey, statusData, isLoading, error } from '../stores.js';
4 import { fetchStatus, restartServices, startServices, stopServices, startService, stopService, restartService, saveConfig } from '../api.js';
5 import ProcessCard from '../components/ProcessCard.svelte';
6
7 let refreshInterval;
8
9 onMount(async () => {
10 await loadStatus();
11 // Auto-refresh every 5 seconds
12 refreshInterval = setInterval(loadStatus, 5000);
13 });
14
15 onDestroy(() => {
16 if (refreshInterval) {
17 clearInterval(refreshInterval);
18 }
19 });
20
21 async function loadStatus() {
22 try {
23 $statusData = await fetchStatus($userSigner, $userPubkey);
24 $error = '';
25 } catch (e) {
26 $error = e.message;
27 }
28 }
29
30 async function handleRestart() {
31 if (!confirm('Are you sure you want to restart all services?')) {
32 return;
33 }
34
35 $isLoading = true;
36 try {
37 await restartServices($userSigner, $userPubkey);
38 // Wait a moment then refresh
39 setTimeout(loadStatus, 2000);
40 } catch (e) {
41 $error = e.message;
42 } finally {
43 $isLoading = false;
44 }
45 }
46
47 async function handleStart() {
48 $isLoading = true;
49 try {
50 await startServices($userSigner, $userPubkey);
51 // Wait a moment then refresh
52 setTimeout(loadStatus, 2000);
53 } catch (e) {
54 $error = e.message;
55 } finally {
56 $isLoading = false;
57 }
58 }
59
60 async function handleStop() {
61 if (!confirm('Are you sure you want to stop all services?')) {
62 return;
63 }
64
65 $isLoading = true;
66 try {
67 await stopServices($userSigner, $userPubkey);
68 // Wait a moment then refresh
69 setTimeout(loadStatus, 2000);
70 } catch (e) {
71 $error = e.message;
72 } finally {
73 $isLoading = false;
74 }
75 }
76
77 async function handleServiceStart(event) {
78 const { service } = event.detail;
79 $isLoading = true;
80 try {
81 await startService($userSigner, $userPubkey, service);
82 // Wait a moment then refresh
83 setTimeout(loadStatus, 2000);
84 } catch (e) {
85 $error = e.message;
86 } finally {
87 $isLoading = false;
88 }
89 }
90
91 async function handleServiceStop(event) {
92 const { service } = event.detail;
93 // Check if this is a critical service with dependents
94 const hasDependents = ['orly-db', 'orly-acl'].includes(service);
95 if (hasDependents) {
96 if (!confirm(`Stopping ${service} will also stop its dependent services. Continue?`)) {
97 return;
98 }
99 }
100
101 $isLoading = true;
102 try {
103 await stopService($userSigner, $userPubkey, service);
104 // Wait a moment then refresh
105 setTimeout(loadStatus, 2000);
106 } catch (e) {
107 $error = e.message;
108 } finally {
109 $isLoading = false;
110 }
111 }
112
113 async function handleServiceRestart(event) {
114 const { service } = event.detail;
115 // Check if this is a critical service with dependents
116 const hasDependents = ['orly-db', 'orly-acl'].includes(service);
117 if (hasDependents) {
118 if (!confirm(`Restarting ${service} will also restart its dependent services. Continue?`)) {
119 return;
120 }
121 }
122
123 $isLoading = true;
124 try {
125 await restartService($userSigner, $userPubkey, service);
126 // Wait a moment then refresh
127 setTimeout(loadStatus, 2000);
128 } catch (e) {
129 $error = e.message;
130 } finally {
131 $isLoading = false;
132 }
133 }
134
135 // Map service names to config properties
136 function getConfigForService(service, enabled) {
137 switch (service) {
138 // Database backends (mutually exclusive)
139 case 'orly-db-badger':
140 return enabled ? { db_backend: 'badger' } : null;
141 case 'orly-db-neo4j':
142 return enabled ? { db_backend: 'neo4j' } : null;
143
144 // ACL backends (mutually exclusive)
145 case 'orly-acl-follows':
146 return enabled ? { acl_enabled: true, acl_mode: 'follows' } : { acl_enabled: false };
147 case 'orly-acl-managed':
148 return enabled ? { acl_enabled: true, acl_mode: 'managed' } : { acl_enabled: false };
149 case 'orly-acl-curation':
150 return enabled ? { acl_enabled: true, acl_mode: 'curation' } : { acl_enabled: false };
151
152 // Sync services (independent)
153 case 'orly-sync-distributed':
154 return { distributed_sync_enabled: enabled };
155 case 'orly-sync-cluster':
156 return { cluster_sync_enabled: enabled };
157 case 'orly-sync-relaygroup':
158 return { relay_group_enabled: enabled };
159 case 'orly-sync-negentropy':
160 return { negentropy_enabled: enabled };
161
162 // Certificate service
163 case 'orly-certs':
164 return { certs_enabled: enabled };
165
166 default:
167 return null;
168 }
169 }
170
171 async function handleToggleEnabled(event) {
172 const { service, enabled, category, isExclusive } = event.detail;
173
174 // For exclusive categories, warn if enabling will disable others
175 if (enabled && isExclusive) {
176 const currentlyEnabled = $statusData.processes
177 .filter(p => p.category === category && p.enabled && p.name !== service)
178 .map(p => p.name);
179
180 if (currentlyEnabled.length > 0) {
181 if (!confirm(`Enabling ${service} will disable ${currentlyEnabled.join(', ')}. Continue?`)) {
182 // Refresh to reset the checkbox
183 await loadStatus();
184 return;
185 }
186 }
187 }
188
189 const configUpdate = getConfigForService(service, enabled);
190 if (!configUpdate) {
191 $error = `Unknown service: ${service}`;
192 return;
193 }
194
195 $isLoading = true;
196 try {
197 await saveConfig($userSigner, $userPubkey, configUpdate);
198 // Refresh status after config change
199 setTimeout(loadStatus, 1000);
200 } catch (e) {
201 $error = e.message;
202 // Refresh to reset the checkbox
203 setTimeout(loadStatus, 500);
204 } finally {
205 $isLoading = false;
206 }
207 }
208 </script>
209
210 <div class="dashboard">
211 <div class="page-header">
212 <h2>Dashboard</h2>
213 <div class="actions">
214 <button class="refresh-btn" on:click={loadStatus} disabled={$isLoading}>
215 Refresh
216 </button>
217 {#if $statusData?.services_running}
218 <button class="stop-btn" on:click={handleStop} disabled={$isLoading}>
219 Stop Services
220 </button>
221 <button class="restart-btn" on:click={handleRestart} disabled={$isLoading}>
222 Restart All
223 </button>
224 {:else}
225 <button class="start-btn" on:click={handleStart} disabled={$isLoading}>
226 Start Services
227 </button>
228 {/if}
229 </div>
230 </div>
231
232 {#if $error}
233 <div class="error-banner">{$error}</div>
234 {/if}
235
236 {#if $statusData}
237 <div class="status-summary">
238 <div class="summary-card">
239 <span class="label">Status</span>
240 <span class="value status-indicator" class:running={$statusData.services_running} class:stopped={!$statusData.services_running}>
241 {$statusData.services_running ? 'Running' : 'Stopped'}
242 </span>
243 </div>
244 <div class="summary-card">
245 <span class="label">Version</span>
246 <span class="value">{$statusData.version || 'unknown'}</span>
247 </div>
248 <div class="summary-card">
249 <span class="label">Uptime</span>
250 <span class="value">{$statusData.uptime}</span>
251 </div>
252 <div class="summary-card">
253 <span class="label">Running</span>
254 <span class="value">{$statusData.processes?.filter(p => p.status === 'running').length || 0} / {$statusData.processes?.filter(p => p.enabled).length || 0}</span>
255 </div>
256 </div>
257
258 <h3>Available Modules</h3>
259 <div class="processes-grid">
260 {#each $statusData.processes || [] as process}
261 <ProcessCard
262 {process}
263 isLoading={$isLoading}
264 on:start={handleServiceStart}
265 on:stop={handleServiceStop}
266 on:restart={handleServiceRestart}
267 on:toggle-enabled={handleToggleEnabled}
268 />
269 {/each}
270 </div>
271 {:else if !$error}
272 <div class="loading">Loading status...</div>
273 {/if}
274 </div>
275
276 <style>
277 .dashboard {
278 padding: 20px 0;
279 }
280
281 .page-header {
282 display: flex;
283 justify-content: space-between;
284 align-items: center;
285 margin-bottom: 24px;
286 }
287
288 .page-header h2 {
289 font-size: 1.5rem;
290 color: var(--text-color);
291 }
292
293 .actions {
294 display: flex;
295 gap: 8px;
296 }
297
298 .refresh-btn,
299 .restart-btn,
300 .start-btn,
301 .stop-btn {
302 padding: 8px 16px;
303 border-radius: 4px;
304 cursor: pointer;
305 font-size: 0.9rem;
306 }
307
308 .refresh-btn {
309 background: var(--card-bg);
310 border: 1px solid var(--border-color);
311 color: var(--text-color);
312 }
313
314 .refresh-btn:hover:not(:disabled) {
315 background: var(--border-color);
316 }
317
318 .restart-btn {
319 background: var(--warning);
320 border: none;
321 color: white;
322 }
323
324 .restart-btn:hover:not(:disabled) {
325 opacity: 0.9;
326 }
327
328 .start-btn {
329 background: var(--success, #4caf50);
330 border: none;
331 color: white;
332 }
333
334 .start-btn:hover:not(:disabled) {
335 opacity: 0.9;
336 }
337
338 .stop-btn {
339 background: var(--error, #f44336);
340 border: none;
341 color: white;
342 }
343
344 .stop-btn:hover:not(:disabled) {
345 opacity: 0.9;
346 }
347
348 .restart-btn:disabled,
349 .refresh-btn:disabled,
350 .start-btn:disabled,
351 .stop-btn:disabled {
352 opacity: 0.5;
353 cursor: not-allowed;
354 }
355
356 .error-banner {
357 background: #ffebee;
358 color: #c62828;
359 padding: 12px 16px;
360 border-radius: 6px;
361 margin-bottom: 20px;
362 border: 1px solid #ffcdd2;
363 }
364
365 .status-summary {
366 display: grid;
367 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
368 gap: 16px;
369 margin-bottom: 32px;
370 }
371
372 .summary-card {
373 background: var(--card-bg);
374 border: 1px solid var(--border-color);
375 border-radius: 8px;
376 padding: 16px;
377 display: flex;
378 flex-direction: column;
379 gap: 4px;
380 }
381
382 .summary-card .label {
383 font-size: 0.85rem;
384 color: var(--muted-color);
385 }
386
387 .summary-card .value {
388 font-size: 1.25rem;
389 font-weight: 600;
390 color: var(--text-color);
391 }
392
393 .status-indicator.running {
394 color: var(--success, #4caf50);
395 }
396
397 .status-indicator.stopped {
398 color: var(--error, #f44336);
399 }
400
401 h3 {
402 font-size: 1.1rem;
403 color: var(--text-color);
404 margin-bottom: 16px;
405 }
406
407 .processes-grid {
408 display: grid;
409 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
410 gap: 16px;
411 }
412
413 .loading {
414 text-align: center;
415 color: var(--muted-color);
416 padding: 40px;
417 }
418 </style>
419