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&#10;# 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