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