wsb-daemon 0.275.7622

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
<script lang="ts">
  import SettingsSection from "./SettingsSection.svelte";
  import { settings } from "../../stores/settings.svelte.js";
  import {
    getServerUrl,
    setServerUrl,
    getAuthToken,
    setAuthToken,
    isRemoteConnection,
  } from "../../api/client.js";

  let serverUrl: string = $state(getServerUrl());
  let tokenInput: string = $state(getAuthToken());
  let testing: boolean = $state(false);
  let testResult: { ok: boolean; message: string } | null = $state(null);
  let saving: boolean = $state(false);

  let isRemote: boolean = $derived(isRemoteConnection());

  async function handleTestConnection() {
    if (!serverUrl.trim()) return;
    testing = true;
    testResult = null;
    try {
      const base = serverUrl.replace(/\/+$/, "");
      const headers: Record<string, string> = {};
      if (tokenInput.trim()) {
        headers["Authorization"] = `Bearer ${tokenInput.trim()}`;
      }
      const res = await fetch(`${base}/api/v1/version`, { headers });
      if (res.ok) {
        const data = await res.json();
        testResult = {
          ok: true,
          message: `Connected (v${data.version || "unknown"})`,
        };
      } else {
        testResult = { ok: false, message: `Server returned ${res.status}` };
      }
    } catch (e) {
      testResult = {
        ok: false,
        message: e instanceof Error ? e.message : "Connection failed",
      };
    } finally {
      testing = false;
    }
  }

  function handleConnect() {
    if (!serverUrl.trim()) return;
    const url = serverUrl.replace(/\/+$/, "");
    setServerUrl(url);
    setAuthToken(tokenInput.trim());
    setTimeout(() => window.location.reload(), 500);
  }

  function handleDisconnect() {
    // Clear the remote token before clearing the URL, so the
    // scoped key resolves to the remote server's token.
    setAuthToken("");
    setServerUrl("");
    setTimeout(() => window.location.reload(), 500);
  }

</script>

<SettingsSection
  title="Remote Access"
  description="Where this workbench is served from, and connecting to one served elsewhere."
>
  {#if !isRemote}
    <!--
      Reported, not offered. Which address the workbench is served on is
      decided when the daemon starts and cannot be changed from here.

      There used to be an "Allow remote connections" toggle in this space. It
      persisted a preference nothing read — the server bound loopback either
      way — so it promised remote access it never delivered. Had anything
      honoured it, a box ticked once would have reopened the port on every
      restart afterwards, which is worse.
    -->
    <div class="subsection">
      <div class="server-info">
        <span class="field-label">Serving</span>
        {#if settings.server.reachable_remotely}
          <span class="info-value">
            {settings.server.host}:{settings.server.port} — every interface.
            Connect using this machine's IP address or hostname.
          </span>
        {:else}
          <code class="info-value"
            >http://{settings.server.host}:{settings.server.port}</code
          >
        {/if}
      </div>

      {#if settings.server.reachable_remotely}
        <div class="security-warning">
          Warning: this workbench is reachable from other machines and has no
          authentication — anyone who can reach it can read this worktree and
          start operations on this host. It is also unencrypted HTTP. Use a
          secure tunnel (Tailscale, SSH tunnel, or a reverse proxy with TLS).
        </div>
      {:else}
        <p class="restart-note">
          Reachable only from this machine. To serve it wider, restart the
          daemon with an address: <code>wsb daemon stop</code> then
          <code>wsb serve --listen 0.0.0.0</code>.
        </p>
      {/if}
    </div>

    <div class="divider"></div>
  {/if}

  <div class="subsection">
    <span class="subsection-title">
      {isRemote ? "Remote Connection" : "Connect to Remote Server"}
    </span>

    {#if isRemote}
      <div class="connected-info">
        <span class="field-label">Connected to</span>
        <code class="info-value">{getServerUrl()}</code>
      </div>
      <button class="disconnect-btn" onclick={handleDisconnect}>
        Disconnect
      </button>
    {:else}
      <div class="field">
        <label class="field-label" for="remote-url">Server URL</label>
        <input
          id="remote-url"
          class="setting-input"
          type="url"
          placeholder="http://192.168.1.100:8080"
          bind:value={serverUrl}
        />
      </div>

      <div class="field">
        <label class="field-label" for="remote-token">Auth Token</label>
        <input
          id="remote-token"
          class="setting-input"
          type="password"
          placeholder="Paste auth token from server"
          bind:value={tokenInput}
        />
      </div>

      <div class="actions">
        <button
          class="test-btn"
          disabled={testing || !serverUrl.trim()}
          onclick={handleTestConnection}
        >
          {testing ? "Testing..." : "Test Connection"}
        </button>
        <button
          class="connect-btn"
          disabled={saving || !serverUrl.trim()}
          onclick={handleConnect}
        >
          Connect
        </button>
      </div>

      {#if testResult}
        <p class="msg" class:success={testResult.ok} class:error={!testResult.ok}>
          {testResult.message}
        </p>
      {/if}
    {/if}
  </div>
</SettingsSection>

<style>
  .subsection {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-10);
  }

  .subsection-title {
    font-size: var(--wb-font-size-12);
    font-weight: 600;
    color: var(--vscode-foreground);
  }

  .divider {
    border-top: 1px solid var(--vscode-widget-border);
    margin: var(--wb-spacing-2) 0;
  }

  .server-info,
  .connected-info {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-4);
  }

  .field-label {
    font-size: var(--wb-font-size-11);
    font-weight: 500;
    color: var(--vscode-disabledForeground);
  }

  .info-value {
    font-size: var(--wb-font-size-12);
    font-family: var(--wsb-font-mono, monospace);
    color: var(--vscode-editor-foreground);
  }

  .field {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-4);
  }

  .setting-input {
    height: var(--wb-control-size-30);
    padding: 0 var(--wb-spacing-10);
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-12);
    font-family: var(--wsb-font-mono, monospace);
    color: var(--vscode-editor-foreground);
    background: var(--vscode-input-background);
    border: 1px solid var(--vscode-widget-border);
    transition: border-color 0.15s;
  }

  .setting-input:focus {
    outline: none;
    border-color: var(--vscode-focusBorder);
  }

  .actions {
    display: flex;
    gap: var(--wb-spacing-8);
  }

  .test-btn,
  .connect-btn,
  .disconnect-btn {
    height: var(--wb-control-size-30);
    padding: 0 var(--wb-spacing-14);
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-12);
    font-weight: 500;
    border: none;
    cursor: pointer;
    white-space: nowrap;
    transition: opacity 0.12s;
  }

  .test-btn {
    color: var(--vscode-editor-foreground);
    background: var(--vscode-input-background);
    border: 1px solid var(--vscode-widget-border);
  }

  .connect-btn {
    color: white;
    background: var(--vscode-focusBorder);
  }

  .disconnect-btn {
    color: white;
    background: var(--vscode-errorForeground, #ef4444);
  }

  .test-btn:hover:not(:disabled),
  .connect-btn:hover:not(:disabled),
  .disconnect-btn:hover:not(:disabled) {
    opacity: 0.9;
  }

  .test-btn:disabled,
  .connect-btn:disabled {
    opacity: 0.6;
    cursor: default;
  }

  .msg {
    font-size: var(--wb-font-size-11);
    margin: 0;
  }

  .msg.error {
    color: var(--vscode-errorForeground, #ef4444);
  }

  .msg.success {
    color: var(--vscode-testing-iconPassed, #22c55e);
  }

  .restart-note {
    font-size: var(--wb-font-size-11);
    color: var(--vscode-disabledForeground);
    margin: 0;
    font-style: italic;
  }

  .security-warning {
    font-size: var(--wb-font-size-11);
    color: var(--vscode-editorWarning-foreground, #f59e0b);
    background: color-mix(in srgb, var(--vscode-editorWarning-foreground, #f59e0b) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--vscode-editorWarning-foreground, #f59e0b) 25%, transparent);
    border-radius: var(--wsb-radius-sm);
    padding: var(--wb-spacing-8) var(--wb-spacing-10);
    line-height: 1.5;
  }
</style>