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">
  // The logs directory: where it is, what it holds, and how big it may get.
  //
  // Every operation started from the workbench is written to a file there, so
  // this is the setting that decides how much of the past a page can still
  // show. What it manages is stated per stream rather than as one number: an
  // operator deciding whether 2 GB is too much needs to know whether it is
  // test sessions or their own build logs.
  import SettingsSection from "./SettingsSection.svelte";
  import { settings } from "../../stores/settings.svelte.js";
  import SizeSlider from "./SizeSlider.svelte";

  const GB = 1024 * 1024 * 1024;

  /**
   * The largest cap worth offering.
   *
   * Not a limit the daemon enforces — it accepts anything at or above its
   * minimum — but the top of the slider's travel. Past this the setting stops
   * being a cap and becomes "never sweep", which is what turning the sweep off
   * is for.
   */
  const MAX_BYTES = 256 * GB;

  /** What each stream directory is, for somebody who has not read the code. */
  const STREAMS: Record<string, string> = {
    ops: "operations run from here",
    test: "test sessions",
    daemon: "the daemon's own log",
  };

  let localMax: number = $state(settings.logs.max_bytes);
  let copied = $state(false);

  $effect(() => {
    localMax = settings.logs.max_bytes;
  });

  const dirty = $derived(localMax !== settings.logs.max_bytes);
  const usage = $derived(settings.logs.usage);
  /** How full it is, against the limit that is actually in force. */
  const fraction = $derived(
    settings.logs.max_bytes > 0
      ? Math.min(1, usage.total_bytes / settings.logs.max_bytes)
      : 0,
  );
  /** Lowering the limit below what is held means the next sweep deletes. */
  const wouldSweep = $derived(dirty && localMax < usage.managed_bytes);

  function human(bytes: number): string {
    if (bytes < 1024) return `${bytes} B`;
    const units = ["KB", "MB", "GB", "TB"];
    let value = bytes / 1024;
    let unit = 0;
    while (value >= 1024 && unit + 1 < units.length) {
      value /= 1024;
      unit += 1;
    }
    return `${value >= 100 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
  }

  async function copyRoot() {
    try {
      await navigator.clipboard.writeText(settings.logs.root);
      copied = true;
      setTimeout(() => (copied = false), 1500);
    } catch {
      // The path is on screen and selectable; a clipboard the browser
      // withheld is not worth an error.
    }
  }

  async function save() {
    await settings.save({ logs: { max_bytes: localMax } });
  }
</script>

<SettingsSection
  title="Logs"
  description="Every operation run from the workbench is written to a file here, so its output survives closing the tab, restarting the daemon, and however much the run says."
>
  <div class="setting-row column">
    <span class="setting-label">Directory</span>
    <div class="path-row">
      <code class="path">{settings.logs.root}</code>
      <button class="path-copy" onclick={() => void copyRoot()}>
        {copied ? "copied" : "copy"}
      </button>
    </div>
  </div>

  <div class="setting-row column">
    <span class="setting-label">
      Holding {human(usage.total_bytes)} of {human(settings.logs.max_bytes)}
    </span>
    <div class="meter" class:meter-full={fraction > 0.9}>
      <div class="meter-fill" style="width: {Math.round(fraction * 100)}%"></div>
    </div>
    <div class="streams">
      {#each Object.entries(usage.by_group) as [name, bytes] (name)}
        <span class="stream">
          <span class="stream-name">{name}</span>
          <span class="stream-what">{STREAMS[name] ?? ""}</span>
          <span class="stream-bytes">{human(bytes)}</span>
        </span>
      {/each}
      {#if usage.unmanaged_bytes > 0}
        <span class="stream">
          <span class="stream-name">other</span>
          <span class="stream-what">not written here — counted, never removed</span>
          <span class="stream-bytes">{human(usage.unmanaged_bytes)}</span>
        </span>
      {/if}
    </div>
  </div>

  <div class="setting-row">
    <span class="setting-label">Maximum size</span>
    <SizeSlider
      value={localMax}
      min={settings.logs.min_bytes}
      max={MAX_BYTES}
      onchange={(bytes) => (localMax = bytes)}
    />
  </div>

  <p class="setting-note">
    As the limit is reached, whole logs are removed oldest first — a whole
    operation, a whole test session — and never one that is being written.
  </p>

  {#if wouldSweep}
    <p class="setting-warn">
      {human(usage.managed_bytes)} is held now, so saving this removes the
      oldest logs until {human(localMax)} is left.
    </p>
  {/if}

  {#if dirty}
    <div class="save-row">
      <button class="save-btn" disabled={settings.saving} onclick={save}>
        {settings.saving ? "Saving..." : "Save"}
      </button>
    </div>
  {/if}
</SettingsSection>

<style>
  .setting-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--wb-spacing-12);
    padding: var(--wb-spacing-8) 0;
  }

  .setting-row.column {
    flex-direction: column;
    align-items: stretch;
    gap: var(--wb-spacing-6);
  }

  .setting-label {
    font-size: var(--wb-font-size-12);
    color: var(--vscode-foreground);
  }

  .path-row {
    display: flex;
    align-items: center;
    gap: var(--wb-spacing-8);
    min-width: 0;
  }

  .path {
    font-family: var(--wsb-font-mono, monospace);
    font-size: var(--wb-font-size-11);
    color: var(--vscode-descriptionForeground, #999);
    overflow-x: auto;
    white-space: nowrap;
    user-select: text;
    min-width: 0;
  }

  .path-copy {
    background: none;
    border: 1px solid var(--vscode-widget-border);
    border-radius: var(--wsb-radius-sm);
    color: var(--vscode-descriptionForeground, #999);
    cursor: pointer;
    font-size: var(--wb-font-size-10);
    padding: var(--wb-spacing-1) var(--wb-spacing-6);
    flex-shrink: 0;
  }

  .path-copy:hover {
    color: var(--vscode-foreground);
    border-color: var(--vscode-focusBorder);
  }

  .meter {
    height: var(--wb-control-size-6);
    border-radius: var(--wb-corner-radius-3);
    background: var(--vscode-input-background);
    border: 1px solid var(--vscode-widget-border);
    overflow: hidden;
  }

  .meter-fill {
    height: 100%;
    background: var(--vscode-focusBorder);
    transition: width 0.2s;
  }

  .meter-full .meter-fill {
    background: var(--vscode-errorForeground, #f14c4c);
  }

  .streams {
    display: flex;
    flex-direction: column;
    gap: var(--wb-spacing-2);
    margin-top: var(--wb-spacing-4);
  }

  .stream {
    display: flex;
    align-items: baseline;
    gap: var(--wb-spacing-8);
    font-size: var(--wb-font-size-11);
    color: var(--vscode-descriptionForeground, #999);
  }

  .stream-name {
    font-family: var(--wsb-font-mono, monospace);
    min-width: var(--wb-control-size-56);
  }

  .stream-what {
    flex: 1;
    opacity: 0.8;
  }

  .stream-bytes {
    font-variant-numeric: tabular-nums;
  }






  .setting-note {
    margin: var(--wb-spacing-4) 0 0;
    font-size: var(--wb-font-size-11);
    color: var(--vscode-descriptionForeground, #999);
  }

  .setting-warn {
    margin: var(--wb-spacing-4) 0 0;
    font-size: var(--wb-font-size-11);
    color: var(--vscode-editorWarning-foreground, #cca700);
  }

  .save-row {
    display: flex;
    justify-content: flex-end;
  }

  .save-btn {
    height: var(--wb-control-size-28);
    padding: 0 var(--wb-spacing-16);
    border-radius: var(--wsb-radius-sm);
    font-size: var(--wb-font-size-12);
    font-weight: 500;
    color: white;
    background: var(--vscode-focusBorder);
    border: none;
    cursor: pointer;
  }

  .save-btn:disabled {
    opacity: 0.6;
    cursor: default;
  }
</style>