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