SprocketView.svelte raw
1 <script>
2 export let isLoggedIn = false;
3 export let userRole = "";
4 export let sprocketStatus = null;
5 export let isLoadingSprocket = false;
6 export let sprocketUploadFile = null;
7 export let sprocketScript = "";
8 export let sprocketMessage = "";
9 export let sprocketMessageType = "";
10 export let sprocketVersions = [];
11
12 import { createEventDispatcher } from "svelte";
13 const dispatch = createEventDispatcher();
14
15 function restartSprocket() {
16 dispatch("restartSprocket");
17 }
18
19 function deleteSprocket() {
20 dispatch("deleteSprocket");
21 }
22
23 function handleSprocketFileSelect(event) {
24 dispatch("sprocketFileSelect", event);
25 }
26
27 function uploadSprocketScript() {
28 dispatch("uploadSprocketScript");
29 }
30
31 function saveSprocket() {
32 dispatch("saveSprocket");
33 }
34
35 function loadSprocket() {
36 dispatch("loadSprocket");
37 }
38
39 function loadVersions() {
40 dispatch("loadVersions");
41 }
42
43 function loadVersion(version) {
44 dispatch("loadVersion", version);
45 }
46
47 function deleteVersion(versionName) {
48 dispatch("deleteVersion", versionName);
49 }
50
51 function openLoginModal() {
52 dispatch("openLoginModal");
53 }
54 </script>
55
56 <div class="sprocket-view">
57 <h2>Sprocket Script Management</h2>
58 {#if isLoggedIn && userRole === "owner"}
59 <div class="sprocket-section">
60 <div class="sprocket-header">
61 <h3>Script Editor</h3>
62 <div class="sprocket-controls">
63 <button
64 class="sprocket-btn restart-btn"
65 on:click={restartSprocket}
66 disabled={isLoadingSprocket}
67 >
68 🔄 Restart
69 </button>
70 <button
71 class="sprocket-btn delete-btn"
72 on:click={deleteSprocket}
73 disabled={isLoadingSprocket ||
74 !sprocketStatus?.script_exists}
75 >
76 🗑️ Delete Script
77 </button>
78 </div>
79 </div>
80
81 <div class="sprocket-upload-section">
82 <h4>Upload Script</h4>
83 <div class="upload-controls">
84 <input
85 type="file"
86 id="sprocket-upload-file"
87 accept=".sh,.bash"
88 on:change={handleSprocketFileSelect}
89 disabled={isLoadingSprocket}
90 />
91 <button
92 class="sprocket-btn upload-btn"
93 on:click={uploadSprocketScript}
94 disabled={isLoadingSprocket || !sprocketUploadFile}
95 >
96 📤 Upload & Update
97 </button>
98 </div>
99 </div>
100
101 <div class="sprocket-status">
102 <div class="status-item">
103 <span class="status-label">Status:</span>
104 <span
105 class="status-value"
106 class:running={sprocketStatus?.is_running}
107 >
108 {sprocketStatus?.is_running
109 ? "🟢 Running"
110 : "🔴 Stopped"}
111 </span>
112 </div>
113 {#if sprocketStatus?.pid}
114 <div class="status-item">
115 <span class="status-label">PID:</span>
116 <span class="status-value">{sprocketStatus.pid}</span>
117 </div>
118 {/if}
119 <div class="status-item">
120 <span class="status-label">Script:</span>
121 <span class="status-value"
122 >{sprocketStatus?.script_exists
123 ? "✅ Exists"
124 : "❌ Not found"}</span
125 >
126 </div>
127 </div>
128
129 <div class="script-editor-container">
130 <textarea
131 class="script-editor"
132 bind:value={sprocketScript}
133 placeholder="#!/bin/bash # Enter your sprocket script here..."
134 disabled={isLoadingSprocket}
135 ></textarea>
136 </div>
137
138 <div class="script-actions">
139 <button
140 class="sprocket-btn save-btn"
141 on:click={saveSprocket}
142 disabled={isLoadingSprocket}
143 >
144 💾 Save & Update
145 </button>
146 <button
147 class="sprocket-btn load-btn"
148 on:click={loadSprocket}
149 disabled={isLoadingSprocket}
150 >
151 📥 Load Current
152 </button>
153 </div>
154
155 {#if sprocketMessage}
156 <div
157 class="sprocket-message"
158 class:error={sprocketMessageType === "error"}
159 >
160 {sprocketMessage}
161 </div>
162 {/if}
163 </div>
164
165 <div class="sprocket-section">
166 <h3>Script Versions</h3>
167 <div class="versions-list">
168 {#each sprocketVersions as version}
169 <div
170 class="version-item"
171 class:current={version.is_current}
172 >
173 <div class="version-info">
174 <div class="version-name">
175 {version.name}
176 </div>
177 <div class="version-date">
178 {new Date(version.modified).toLocaleString()}
179 {#if version.is_current}
180 <span class="current-badge">Current</span>
181 {/if}
182 </div>
183 </div>
184 <div class="version-actions">
185 <button
186 class="version-btn load-btn"
187 on:click={() => loadVersion(version)}
188 disabled={isLoadingSprocket}
189 >
190 📥 Load
191 </button>
192 {#if !version.is_current}
193 <button
194 class="version-btn delete-btn"
195 on:click={() => deleteVersion(version.name)}
196 disabled={isLoadingSprocket}
197 >
198 🗑️ Delete
199 </button>
200 {/if}
201 </div>
202 </div>
203 {/each}
204 </div>
205
206 <button
207 class="sprocket-btn refresh-btn"
208 on:click={loadVersions}
209 disabled={isLoadingSprocket}
210 >
211 🔄 Refresh Versions
212 </button>
213 </div>
214 {:else if isLoggedIn}
215 <div class="permission-denied">
216 <p>❌ Owner permission required for sprocket management.</p>
217 <p>
218 To enable sprocket functionality, set the <code
219 >ORLY_OWNERS</code
220 > environment variable with your npub when starting the relay.
221 </p>
222 <p>
223 Current user role: <strong>{userRole || "none"}</strong>
224 </p>
225 </div>
226 {:else}
227 <div class="login-prompt">
228 <p>Please log in to access sprocket management.</p>
229 <button class="login-btn" on:click={openLoginModal}>Log In</button>
230 </div>
231 {/if}
232 </div>
233
234 <style>
235 .sprocket-view {
236 width: 100%;
237 max-width: 1200px;
238 margin: 0;
239 padding: 20px;
240 background: var(--header-bg);
241 color: var(--text-color);
242 border-radius: 8px;
243 }
244
245 .sprocket-view h2 {
246 margin: 0 0 1.5rem 0;
247 color: var(--text-color);
248 font-size: 1.8rem;
249 font-weight: 600;
250 }
251
252 .sprocket-section {
253 background-color: var(--card-bg);
254 border-radius: 8px;
255 padding: 1em;
256 margin-bottom: 1.5rem;
257 border: 1px solid var(--border-color);
258 width: 32em;
259 }
260
261 .sprocket-header {
262 display: flex;
263 justify-content: space-between;
264 align-items: center;
265 margin-bottom: 1rem;
266 }
267
268 .sprocket-header h3 {
269 margin: 0;
270 color: var(--text-color);
271 font-size: 1.2rem;
272 font-weight: 600;
273 }
274
275 .sprocket-controls {
276 display: flex;
277 gap: 0.5rem;
278 }
279
280 .sprocket-btn {
281 background: var(--primary);
282 color: var(--text-color);
283 border: none;
284 padding: 0.5em 1em;
285 border-radius: 4px;
286 cursor: pointer;
287 font-size: 0.9em;
288 transition: background-color 0.2s;
289 display: flex;
290 align-items: center;
291 gap: 0.25em;
292 }
293
294 .sprocket-btn:hover:not(:disabled) {
295 background: var(--accent-hover-color);
296 }
297
298 .sprocket-btn:disabled {
299 background: var(--secondary);
300 cursor: not-allowed;
301 }
302
303 .restart-btn {
304 background: var(--warning);
305 }
306
307 .restart-btn:hover:not(:disabled) {
308 background: var(--warning);
309 filter: brightness(0.9);
310 }
311
312 .delete-btn {
313 background: var(--danger);
314 }
315
316 .delete-btn:hover:not(:disabled) {
317 background: var(--danger);
318 filter: brightness(0.9);
319 }
320
321 .sprocket-upload-section {
322 margin-bottom: 1.5rem;
323 }
324
325 .sprocket-upload-section h4 {
326 margin: 0 0 0.5rem 0;
327 color: var(--text-color);
328 font-size: 1rem;
329 font-weight: 600;
330 }
331
332 .upload-controls {
333 display: flex;
334 flex-direction: column;
335 gap: 0.5rem;
336 }
337
338 #sprocket-upload-file {
339 padding: 0.5em;
340 border: 1px solid var(--border-color);
341 border-radius: 4px;
342 background: var(--input-bg);
343 color: var(--input-text-color);
344 }
345
346 .upload-btn {
347 background: var(--success);
348 align-self: flex-start;
349 }
350
351 .upload-btn:hover:not(:disabled) {
352 background: var(--success);
353 filter: brightness(0.9);
354 }
355
356 .sprocket-status {
357 display: flex;
358 flex-direction: column;
359 gap: 0.5rem;
360 margin-bottom: 1.5rem;
361 padding: 1rem;
362 background: var(--bg-color);
363 border-radius: 4px;
364 border: 1px solid var(--border-color);
365 }
366
367 .status-item {
368 display: flex;
369 justify-content: space-between;
370 align-items: center;
371 }
372
373 .status-label {
374 font-weight: 600;
375 color: var(--text-color);
376 }
377
378 .status-value {
379 color: var(--text-color);
380 }
381
382 .status-value.running {
383 color: var(--success);
384 }
385
386 .script-editor-container {
387 margin-bottom: 1.5rem;
388 }
389
390 .script-editor {
391 width: 100%;
392 height: 300px;
393 padding: 1em;
394 border: 1px solid var(--border-color);
395 border-radius: 4px;
396 background: var(--input-bg);
397 color: var(--input-text-color);
398 font-family: monospace;
399 font-size: 0.9em;
400 line-height: 1.4;
401 resize: vertical;
402 }
403
404 .script-editor:disabled {
405 opacity: 0.6;
406 cursor: not-allowed;
407 }
408
409 .script-actions {
410 display: flex;
411 gap: 0.5rem;
412 margin-bottom: 1rem;
413 }
414
415 .save-btn {
416 background: var(--success);
417 }
418
419 .save-btn:hover:not(:disabled) {
420 background: var(--success);
421 filter: brightness(0.9);
422 }
423
424 .load-btn {
425 background: var(--info);
426 }
427
428 .load-btn:hover:not(:disabled) {
429 background: var(--info);
430 filter: brightness(0.9);
431 }
432
433 .sprocket-message {
434 padding: 1rem;
435 border-radius: 4px;
436 margin-top: 1rem;
437 background: var(--success-bg);
438 color: var(--success-text);
439 border: 1px solid var(--success);
440 }
441
442 .sprocket-message.error {
443 background: var(--danger-bg);
444 color: var(--danger-text);
445 border: 1px solid var(--danger);
446 }
447
448 .versions-list {
449 display: flex;
450 flex-direction: column;
451 gap: 0.5rem;
452 margin-bottom: 1rem;
453 }
454
455 .version-item {
456 display: flex;
457 justify-content: space-between;
458 align-items: center;
459 padding: 0.75rem;
460 background: var(--bg-color);
461 border: 1px solid var(--border-color);
462 border-radius: 4px;
463 }
464
465 .version-item.current {
466 border-color: var(--primary);
467 background: var(--primary-bg);
468 }
469
470 .version-info {
471 flex: 1;
472 }
473
474 .version-name {
475 font-weight: 600;
476 color: var(--text-color);
477 margin-bottom: 0.25rem;
478 }
479
480 .version-date {
481 font-size: 0.8em;
482 color: var(--text-color);
483 opacity: 0.7;
484 display: flex;
485 align-items: center;
486 gap: 0.5rem;
487 }
488
489 .current-badge {
490 background: var(--primary);
491 color: var(--text-color);
492 padding: 0.1em 0.4em;
493 border-radius: 0.25rem;
494 font-size: 0.7em;
495 font-weight: 600;
496 }
497
498 .version-actions {
499 display: flex;
500 gap: 0.25rem;
501 }
502
503 .version-btn {
504 background: var(--primary);
505 color: var(--text-color);
506 border: none;
507 padding: 0.25em 0.5em;
508 border-radius: 0.25rem;
509 cursor: pointer;
510 font-size: 0.8em;
511 transition: background-color 0.2s;
512 }
513
514 .version-btn:hover:not(:disabled) {
515 background: var(--accent-hover-color);
516 }
517
518 .version-btn:disabled {
519 background: var(--secondary);
520 cursor: not-allowed;
521 }
522
523 .version-btn.delete-btn {
524 background: var(--danger);
525 }
526
527 .version-btn.delete-btn:hover:not(:disabled) {
528 background: var(--danger);
529 filter: brightness(0.9);
530 }
531
532 .refresh-btn {
533 background: var(--info);
534 }
535
536 .refresh-btn:hover:not(:disabled) {
537 background: var(--info);
538 filter: brightness(0.9);
539 }
540
541 .permission-denied,
542 .login-prompt {
543 text-align: center;
544 padding: 2em;
545 background-color: var(--card-bg);
546 border-radius: 8px;
547 border: 1px solid var(--border-color);
548 color: var(--text-color);
549 }
550
551 .permission-denied p,
552 .login-prompt p {
553 margin: 0 0 1rem 0;
554 line-height: 1.4;
555 }
556
557 .permission-denied code {
558 background: var(--code-bg);
559 padding: 0.2em 0.4em;
560 border-radius: 0.25rem;
561 font-family: monospace;
562 font-size: 0.9em;
563 }
564
565 .login-btn {
566 background: var(--primary);
567 color: var(--text-color);
568 border: none;
569 padding: 0.75em 1.5em;
570 border-radius: 4px;
571 cursor: pointer;
572 font-weight: bold;
573 font-size: 0.9em;
574 transition: background-color 0.2s;
575 }
576
577 .login-btn:hover {
578 background: var(--accent-hover-color);
579 }
580 </style>
581