Config.svelte raw
1 <script>
2 import { onMount } from 'svelte';
3 import { userSigner, userPubkey, configData, isLoading, error } from '../stores.js';
4 import { fetchConfig, saveConfig, restartServices } from '../api.js';
5
6 let editMode = false;
7 let editedConfig = {};
8 let saveMessage = '';
9 let saveSuccess = false;
10 let isSaving = false;
11
12 onMount(async () => {
13 await loadConfig();
14 });
15
16 async function loadConfig() {
17 $isLoading = true;
18 try {
19 $configData = await fetchConfig($userSigner, $userPubkey);
20 editedConfig = JSON.parse(JSON.stringify($configData)); // Deep copy
21 $error = '';
22 } catch (e) {
23 $error = e.message;
24 } finally {
25 $isLoading = false;
26 }
27 }
28
29 function startEdit() {
30 editedConfig = JSON.parse(JSON.stringify($configData));
31 editMode = true;
32 saveMessage = '';
33 }
34
35 function cancelEdit() {
36 editedConfig = JSON.parse(JSON.stringify($configData));
37 editMode = false;
38 saveMessage = '';
39 }
40
41 async function handleSave() {
42 isSaving = true;
43 saveMessage = '';
44 try {
45 const result = await saveConfig($userSigner, $userPubkey, editedConfig);
46 saveSuccess = result.success;
47 saveMessage = result.message;
48 if (result.success) {
49 $configData = { ...editedConfig };
50 editMode = false;
51 }
52 } catch (e) {
53 saveSuccess = false;
54 saveMessage = e.message;
55 } finally {
56 isSaving = false;
57 }
58 }
59
60 async function handleRestart() {
61 if (!confirm('Restart all services? This will briefly interrupt the relay.')) {
62 return;
63 }
64 try {
65 await restartServices($userSigner, $userPubkey);
66 saveMessage = 'Restart initiated. Services are restarting...';
67 saveSuccess = true;
68 } catch (e) {
69 saveMessage = e.message;
70 saveSuccess = false;
71 }
72 }
73
74 function addOwner() {
75 const newOwner = prompt('Enter hex pubkey for new admin owner:');
76 if (newOwner && newOwner.match(/^[0-9a-fA-F]{64}$/)) {
77 editedConfig.admin_owners = [...(editedConfig.admin_owners || []), newOwner.toLowerCase()];
78 } else if (newOwner) {
79 alert('Invalid pubkey. Must be 64 hex characters.');
80 }
81 }
82
83 function removeOwner(index) {
84 editedConfig.admin_owners = editedConfig.admin_owners.filter((_, i) => i !== index);
85 }
86 </script>
87
88 <div class="config-page">
89 <div class="page-header">
90 <h2>Configuration</h2>
91 <div class="header-buttons">
92 {#if editMode}
93 <button class="cancel-btn" on:click={cancelEdit} disabled={isSaving}>Cancel</button>
94 <button class="save-btn" on:click={handleSave} disabled={isSaving}>
95 {isSaving ? 'Saving...' : 'Save'}
96 </button>
97 {:else}
98 <button class="refresh-btn" on:click={loadConfig} disabled={$isLoading}>Refresh</button>
99 <button class="edit-btn" on:click={startEdit} disabled={$isLoading || !$configData}>Edit</button>
100 {/if}
101 </div>
102 </div>
103
104 {#if $error}
105 <div class="error-banner">{$error}</div>
106 {/if}
107
108 {#if saveMessage}
109 <div class="message-banner" class:success={saveSuccess} class:error={!saveSuccess}>
110 {saveMessage}
111 {#if saveSuccess && saveMessage.includes('Restart required')}
112 <button class="restart-btn-inline" on:click={handleRestart}>Restart Now</button>
113 {/if}
114 </div>
115 {/if}
116
117 {#if $configData}
118 <div class="config-sections">
119 <section class="config-section">
120 <h3>Database</h3>
121 <div class="config-grid">
122 <div class="config-item">
123 <label class="label">Backend</label>
124 {#if editMode}
125 <select bind:value={editedConfig.db_backend}>
126 <option value="badger">Badger</option>
127 <option value="neo4j">Neo4j</option>
128 </select>
129 {:else}
130 <span class="value">{$configData.db_backend}</span>
131 {/if}
132 </div>
133 <div class="config-item">
134 <label class="label">Binary</label>
135 {#if editMode}
136 <input type="text" bind:value={editedConfig.db_binary} placeholder="orly-db-badger" />
137 {:else}
138 <span class="value mono">{$configData.db_binary}</span>
139 {/if}
140 </div>
141 <div class="config-item">
142 <label class="label">Listen Address</label>
143 {#if editMode}
144 <input type="text" bind:value={editedConfig.db_listen} placeholder="127.0.0.1:50051" />
145 {:else}
146 <span class="value mono">{$configData.db_listen}</span>
147 {/if}
148 </div>
149 <div class="config-item">
150 <label class="label">Data Directory</label>
151 {#if editMode}
152 <input type="text" bind:value={editedConfig.data_dir} />
153 {:else}
154 <span class="value mono">{$configData.data_dir}</span>
155 {/if}
156 </div>
157 </div>
158 </section>
159
160 <section class="config-section">
161 <h3>ACL</h3>
162 <div class="config-grid">
163 <div class="config-item">
164 <label class="label">Enabled</label>
165 {#if editMode}
166 <label class="toggle">
167 <input type="checkbox" bind:checked={editedConfig.acl_enabled} />
168 <span>{editedConfig.acl_enabled ? 'Enabled' : 'Disabled'}</span>
169 </label>
170 {:else}
171 <span class="value bool" class:enabled={$configData.acl_enabled}>
172 {$configData.acl_enabled ? 'Yes' : 'No'}
173 </span>
174 {/if}
175 </div>
176 <div class="config-item">
177 <label class="label">Mode</label>
178 {#if editMode}
179 <select bind:value={editedConfig.acl_mode}>
180 <option value="follows">Follows</option>
181 <option value="managed">Managed</option>
182 <option value="curation">Curation</option>
183 </select>
184 {:else}
185 <span class="value">{$configData.acl_mode}</span>
186 {/if}
187 </div>
188 <div class="config-item">
189 <label class="label">Binary</label>
190 {#if editMode}
191 <input type="text" bind:value={editedConfig.acl_binary} />
192 {:else}
193 <span class="value mono">{$configData.acl_binary}</span>
194 {/if}
195 </div>
196 <div class="config-item">
197 <label class="label">Listen Address</label>
198 {#if editMode}
199 <input type="text" bind:value={editedConfig.acl_listen} placeholder="127.0.0.1:50052" />
200 {:else}
201 <span class="value mono">{$configData.acl_listen}</span>
202 {/if}
203 </div>
204 </div>
205 </section>
206
207 <section class="config-section">
208 <h3>Relay</h3>
209 <div class="config-grid">
210 <div class="config-item">
211 <label class="label">Binary</label>
212 {#if editMode}
213 <input type="text" bind:value={editedConfig.relay_binary} placeholder="orly" />
214 {:else}
215 <span class="value mono">{$configData.relay_binary}</span>
216 {/if}
217 </div>
218 <div class="config-item">
219 <label class="label">Log Level</label>
220 {#if editMode}
221 <select bind:value={editedConfig.log_level}>
222 <option value="trace">Trace</option>
223 <option value="debug">Debug</option>
224 <option value="info">Info</option>
225 <option value="warn">Warn</option>
226 <option value="error">Error</option>
227 </select>
228 {:else}
229 <span class="value">{$configData.log_level}</span>
230 {/if}
231 </div>
232 </div>
233 </section>
234
235 <section class="config-section">
236 <h3>Sync Services</h3>
237 <div class="config-grid">
238 <div class="config-item">
239 <label class="label">Distributed Sync</label>
240 {#if editMode}
241 <label class="toggle">
242 <input type="checkbox" bind:checked={editedConfig.distributed_sync_enabled} />
243 <span>{editedConfig.distributed_sync_enabled ? 'Enabled' : 'Disabled'}</span>
244 </label>
245 {:else}
246 <span class="value bool" class:enabled={$configData.distributed_sync_enabled}>
247 {$configData.distributed_sync_enabled ? 'Enabled' : 'Disabled'}
248 </span>
249 {/if}
250 </div>
251 <div class="config-item">
252 <label class="label">Cluster Sync</label>
253 {#if editMode}
254 <label class="toggle">
255 <input type="checkbox" bind:checked={editedConfig.cluster_sync_enabled} />
256 <span>{editedConfig.cluster_sync_enabled ? 'Enabled' : 'Disabled'}</span>
257 </label>
258 {:else}
259 <span class="value bool" class:enabled={$configData.cluster_sync_enabled}>
260 {$configData.cluster_sync_enabled ? 'Enabled' : 'Disabled'}
261 </span>
262 {/if}
263 </div>
264 <div class="config-item">
265 <label class="label">Relay Group</label>
266 {#if editMode}
267 <label class="toggle">
268 <input type="checkbox" bind:checked={editedConfig.relay_group_enabled} />
269 <span>{editedConfig.relay_group_enabled ? 'Enabled' : 'Disabled'}</span>
270 </label>
271 {:else}
272 <span class="value bool" class:enabled={$configData.relay_group_enabled}>
273 {$configData.relay_group_enabled ? 'Enabled' : 'Disabled'}
274 </span>
275 {/if}
276 </div>
277 <div class="config-item">
278 <label class="label">Negentropy</label>
279 {#if editMode}
280 <label class="toggle">
281 <input type="checkbox" bind:checked={editedConfig.negentropy_enabled} />
282 <span>{editedConfig.negentropy_enabled ? 'Enabled' : 'Disabled'}</span>
283 </label>
284 {:else}
285 <span class="value bool" class:enabled={$configData.negentropy_enabled}>
286 {$configData.negentropy_enabled ? 'Enabled' : 'Disabled'}
287 </span>
288 {/if}
289 </div>
290 </div>
291 </section>
292
293 <section class="config-section">
294 <h3>Admin</h3>
295 <div class="config-grid">
296 <div class="config-item">
297 <label class="label">Binary Directory</label>
298 {#if editMode}
299 <input type="text" bind:value={editedConfig.bin_dir} />
300 {:else}
301 <span class="value mono">{$configData.bin_dir}</span>
302 {/if}
303 </div>
304 <div class="config-item full-width">
305 <label class="label">
306 Admin Owners
307 {#if editMode}
308 <button class="add-owner-btn" on:click={addOwner}>+ Add</button>
309 {/if}
310 </label>
311 <div class="owners-list">
312 {#each (editMode ? editedConfig.admin_owners : $configData.admin_owners) || [] as owner, index}
313 <div class="owner-item">
314 <code class="owner">{owner}</code>
315 {#if editMode}
316 <button class="remove-owner-btn" on:click={() => removeOwner(index)}>x</button>
317 {/if}
318 </div>
319 {:else}
320 <span class="no-owners">No owners configured</span>
321 {/each}
322 </div>
323 </div>
324 </div>
325 </section>
326 </div>
327
328 {#if !editMode}
329 <div class="config-note">
330 <p>Configuration is saved to <code>{$configData.bin_dir?.replace(/\/bin$/, '')}/launcher.json</code>. Environment variables override file settings.</p>
331 </div>
332 {/if}
333 {:else if !$error}
334 <div class="loading">Loading configuration...</div>
335 {/if}
336 </div>
337
338 <style>
339 .config-page {
340 padding: 20px 0;
341 }
342
343 .page-header {
344 display: flex;
345 justify-content: space-between;
346 align-items: center;
347 margin-bottom: 24px;
348 }
349
350 .page-header h2 {
351 font-size: 1.5rem;
352 color: var(--text-color);
353 }
354
355 .header-buttons {
356 display: flex;
357 gap: 8px;
358 }
359
360 .refresh-btn, .edit-btn, .cancel-btn, .save-btn {
361 padding: 8px 16px;
362 background: var(--card-bg);
363 border: 1px solid var(--border-color);
364 color: var(--text-color);
365 border-radius: 4px;
366 cursor: pointer;
367 font-size: 0.9rem;
368 }
369
370 .edit-btn {
371 background: var(--primary);
372 border-color: var(--primary);
373 color: white;
374 }
375
376 .save-btn {
377 background: var(--success);
378 border-color: var(--success);
379 color: white;
380 }
381
382 .cancel-btn:hover:not(:disabled) {
383 background: var(--border-color);
384 }
385
386 .edit-btn:hover:not(:disabled), .save-btn:hover:not(:disabled) {
387 opacity: 0.9;
388 }
389
390 button:disabled {
391 opacity: 0.5;
392 cursor: not-allowed;
393 }
394
395 .error-banner {
396 background: #ffebee;
397 color: #c62828;
398 padding: 12px 16px;
399 border-radius: 6px;
400 margin-bottom: 20px;
401 border: 1px solid #ffcdd2;
402 }
403
404 .message-banner {
405 padding: 12px 16px;
406 border-radius: 6px;
407 margin-bottom: 20px;
408 display: flex;
409 align-items: center;
410 gap: 12px;
411 }
412
413 .message-banner.success {
414 background: #e8f5e9;
415 color: #2e7d32;
416 border: 1px solid #c8e6c9;
417 }
418
419 .message-banner.error {
420 background: #ffebee;
421 color: #c62828;
422 border: 1px solid #ffcdd2;
423 }
424
425 .restart-btn-inline {
426 padding: 4px 12px;
427 background: var(--primary);
428 border: none;
429 color: white;
430 border-radius: 4px;
431 cursor: pointer;
432 font-size: 0.85rem;
433 }
434
435 .config-sections {
436 display: flex;
437 flex-direction: column;
438 gap: 24px;
439 }
440
441 .config-section {
442 background: var(--card-bg);
443 border: 1px solid var(--border-color);
444 border-radius: 8px;
445 padding: 20px;
446 }
447
448 .config-section h3 {
449 font-size: 1.1rem;
450 color: var(--text-color);
451 margin-bottom: 16px;
452 padding-bottom: 8px;
453 border-bottom: 1px solid var(--border-color);
454 }
455
456 .config-grid {
457 display: grid;
458 grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
459 gap: 16px;
460 }
461
462 .config-item {
463 display: flex;
464 flex-direction: column;
465 gap: 4px;
466 }
467
468 .config-item.full-width {
469 grid-column: 1 / -1;
470 }
471
472 .config-item .label {
473 font-size: 0.85rem;
474 color: var(--muted-color);
475 display: flex;
476 align-items: center;
477 gap: 8px;
478 }
479
480 .config-item .value {
481 font-size: 0.95rem;
482 color: var(--text-color);
483 }
484
485 .config-item .value.mono {
486 font-family: monospace;
487 font-size: 0.85rem;
488 }
489
490 .config-item .value.bool {
491 font-weight: 500;
492 }
493
494 .config-item .value.bool.enabled {
495 color: var(--success);
496 }
497
498 .config-item input[type="text"],
499 .config-item select {
500 padding: 8px 12px;
501 border: 1px solid var(--border-color);
502 border-radius: 4px;
503 background: var(--bg-color);
504 color: var(--text-color);
505 font-size: 0.9rem;
506 }
507
508 .config-item input[type="text"]:focus,
509 .config-item select:focus {
510 outline: none;
511 border-color: var(--primary);
512 }
513
514 .toggle {
515 display: flex;
516 align-items: center;
517 gap: 8px;
518 cursor: pointer;
519 }
520
521 .toggle input[type="checkbox"] {
522 width: 18px;
523 height: 18px;
524 }
525
526 .owners-list {
527 display: flex;
528 flex-wrap: wrap;
529 gap: 8px;
530 margin-top: 4px;
531 }
532
533 .owner-item {
534 display: flex;
535 align-items: center;
536 gap: 4px;
537 }
538
539 .owner {
540 font-size: 0.75rem;
541 background: var(--bg-color);
542 padding: 4px 8px;
543 border-radius: 4px;
544 word-break: break-all;
545 }
546
547 .remove-owner-btn {
548 padding: 2px 6px;
549 background: #ffebee;
550 border: none;
551 color: #c62828;
552 border-radius: 4px;
553 cursor: pointer;
554 font-size: 0.8rem;
555 }
556
557 .add-owner-btn {
558 padding: 2px 8px;
559 background: var(--primary);
560 border: none;
561 color: white;
562 border-radius: 4px;
563 cursor: pointer;
564 font-size: 0.75rem;
565 }
566
567 .no-owners {
568 color: var(--muted-color);
569 font-style: italic;
570 }
571
572 .config-note {
573 margin-top: 24px;
574 padding: 16px;
575 background: var(--card-bg);
576 border: 1px solid var(--border-color);
577 border-radius: 8px;
578 }
579
580 .config-note p {
581 color: var(--muted-color);
582 font-size: 0.9rem;
583 margin: 0;
584 }
585
586 .config-note code {
587 background: var(--bg-color);
588 padding: 2px 6px;
589 border-radius: 4px;
590 font-size: 0.85rem;
591 }
592
593 .loading {
594 text-align: center;
595 color: var(--muted-color);
596 padding: 40px;
597 }
598 </style>
599