{% extends "base.html" %}
{% block title %}Settings — CoderAI{% endblock %}

{% block content %}
<div class="page-header">
  <div>
    <h1>Settings</h1>
    <p>Server configuration — restart CoderAI to apply changes</p>
  </div>
  <div class="header-actions">
    <span id="save-status" class="muted small" style="margin-right:.5rem"></span>
    <button class="btn btn-primary" onclick="saveSettings()">Save changes</button>
  </div>
</div>

<div id="settings-alert" style="display:none"></div>

<!-- Server -->
<div class="card mb-0">
  <div class="card-title">Server</div>
  <div style="display:grid;grid-template-columns:1fr 160px;gap:1rem;align-items:start">
    <div class="form-row" style="margin:0">
      <label class="form-label">Listen host</label>
      <input type="text" id="s-host" class="form-input" placeholder="0.0.0.0">
      <span class="form-hint">IP address or hostname to bind to (0.0.0.0 = all interfaces)</span>
    </div>
    <div class="form-row" style="margin:0">
      <label class="form-label">Port</label>
      <input type="number" id="s-port" class="form-input" placeholder="8000" min="1" max="65535">
    </div>
  </div>
  <div class="form-row" style="margin-top:1rem;margin-bottom:.25rem">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-https" onchange="toggleHttps()">
      <span style="font-size:13px;font-weight:500">Enable HTTPS</span>
    </label>
  </div>
  <div id="https-fields" style="display:none;margin-top:.75rem">
    <div class="form-row">
      <label class="form-label">SSL key path <span class="muted">(leave blank to auto-generate)</span></label>
      <input type="text" id="s-key" class="form-input" placeholder="/path/to/key.pem">
    </div>
    <div class="form-row" style="margin:0">
      <label class="form-label">SSL certificate path</label>
      <input type="text" id="s-cert" class="form-input" placeholder="/path/to/cert.pem">
    </div>
  </div>
  <div class="form-row" style="margin-top:1rem;margin-bottom:0">
    <label class="form-label">Request queue max size</label>
    <input type="number" id="s-queue-max" class="form-input" placeholder="6" min="1" max="1000" style="max-width:160px">
    <span class="form-hint">Maximum number of concurrent queued requests. Authenticated requests arriving when the queue is full receive a 429 response.</span>
  </div>
  <div class="form-row" style="margin-top:1rem;margin-bottom:0">
    <label class="form-label">Engine base port</label>
    <input type="number" id="s-internal-port-base" class="form-input" placeholder="8780" min="1" max="65535" style="max-width:160px">
    <span class="form-hint">First internal port for engine subprocesses (the front assigns this and the next free ports). Keep it different from the listen port above. Restart to apply.</span>
  </div>
  <div class="form-row" id="default-engine-row" style="margin-top:1rem;margin-bottom:0;display:none">
    <label class="form-label">Default engine</label>
    <select id="s-default-engine" class="form-input" style="max-width:260px">
      <option value="">Auto (least-loaded compatible)</option>
    </select>
    <span class="form-hint">When a model can run on more than one card (e.g. a GGUF on either the NVIDIA or Radeon engine), prefer this one. A per-model <em>engine</em> set on the Models page overrides this. Only shown when multiple engines are declared.</span>
  </div>
</div>

<!-- Concurrency -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Concurrency</div>
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
    <div class="form-row" style="margin:0">
      <label class="form-label">Max parallel requests <span class="muted">(per engine)</span></label>
      <input type="number" id="s-max-parallel" class="form-input" min="1" max="64" placeholder="2" style="max-width:160px">
      <span class="form-hint">How many requests each engine runs at once. Each engine is its own process, so this applies per-engine — total concurrency is the sum across engines.</span>
    </div>
    <div class="form-row" style="margin:0">
      <label class="form-label">Max instances per model <span class="muted">(per engine)</span></label>
      <input type="number" id="s-max-instances" class="form-input" min="1" max="16" placeholder="1" style="max-width:160px">
      <span class="form-hint">Default number of concurrent copies of one model. Override per model on the Models page.</span>
    </div>
  </div>
  <div id="concurrency-overrides" style="margin-top:.7rem;display:none">
    <label class="form-label" style="margin-bottom:.3rem">Per-engine overrides <span class="muted">(blank = use the defaults above)</span></label>
    <span class="form-hint" style="margin-bottom:.4rem">Give a bigger card more concurrency than a smaller one — e.g. more parallel requests on a 24 GB NVIDIA than an 8 GB Radeon.</span>
    <div id="concurrency-override-rows" style="display:grid;grid-template-columns:110px 1fr 1fr;gap:.5rem;align-items:center"></div>
  </div>
</div>

<!-- Storage -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Storage</div>
  <div class="form-row">
    <label class="form-label">HuggingFace cache directory <span class="muted">(leave blank for default ~/.cache/huggingface)</span></label>
    <input type="text" id="s-hf-cache" class="form-input" placeholder="e.g. /data/models/huggingface">
  </div>
  <div class="form-row">
    <label class="form-label">GGUF cache directory <span class="muted">(leave blank for default ~/.cache/coderai/models)</span></label>
    <input type="text" id="s-gguf-cache" class="form-input" placeholder="e.g. /data/models/gguf">
  </div>
  <div class="form-row" style="margin:0">
    <label class="form-label">Default offload directory <span class="muted">(default: ./offload)</span></label>
    <input type="text" id="s-offload-dir" class="form-input" placeholder="./offload">
    <span class="form-hint">Models will inherit this as default when configured</span>
  </div>
  <div class="form-row" style="margin-top:.75rem">
    <label class="form-label">Max global RAM (GB) <span class="muted">(blank = no cap)</span></label>
    <input type="number" id="s-max-ram" class="form-input" min="0" step="1" placeholder="e.g. 96">
    <span class="form-hint">Server-wide ceiling on host RAM (process-tree RSS). New model loads get a CPU-offload budget clamped to the remaining headroom, so the overflow spills to the offload directory (disk) instead of pushing past the limit. Applied live on save.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-evict-idle-ram">
      <span>Evict idle models when over the RAM limit</span>
    </label>
    <span class="form-hint">Unload least-recently-used idle models to free real RAM before forcing disk offload.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-ram-leak-watch">
      <span>Watch for RAM leaks (auto-mitigate)</span>
    </label>
    <span class="form-hint">Background watcher samples RSS; when it keeps climbing while idle or nears the cap it runs gc / CUDA cache release / heap trim and (if enabled) evicts idle models.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-ram-watch-cuda">
      <span>Allow CUDA cache release in mitigation</span>
    </label>
    <span class="form-hint">When the watcher mitigates, let it call <code>torch.cuda.empty_cache()</code>. Turn this <b>off</b> on a GPU that drops off the bus (Xid 79) to stop the background thread from touching CUDA — host-RAM mitigation (gc / heap trim / idle eviction) still runs. Loads are always skipped regardless.</span>
  </div>
  <div class="form-row" style="margin-top:.75rem">
    <label class="form-label">RAM watch poll interval (s) <span class="muted">(default: 15)</span></label>
    <input type="number" id="s-ram-watch-poll" class="form-input" min="1" step="1" placeholder="15">
    <span class="form-hint">How often the watcher samples process-tree RSS.</span>
  </div>
  <div class="form-row" style="margin-top:.75rem">
    <label class="form-label">RAM watch soft threshold <span class="muted">(0–1, default: 0.90)</span></label>
    <input type="number" id="s-ram-watch-soft" class="form-input" min="0" max="1" step="0.05" placeholder="0.90">
    <span class="form-hint">Fraction of the RAM cap at which the mitigation ladder engages.</span>
  </div>
  <div class="form-row" style="margin-top:.75rem">
    <label class="form-label">Temporary working directory <span class="muted">(default: system /tmp)</span></label>
    <input type="text" id="s-tmp-dir" class="form-input" placeholder="e.g. /data/tmp">
    <span class="form-hint">Scratch space for frame extraction, upscaling and interpolation. Point it at a large volume — 4× upscaling can exhaust a small /tmp ("No space left on device"). Applied live on save.</span>
  </div>
</div>

<!-- Video enhancement (upscale / FPS interpolation) -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Video Enhancement</div>
  <p class="form-hint" style="margin-bottom:.6rem">Upscaling and FPS interpolation run on in-process torch models by default (ESRGAN upscaler, RIFE/FILM interpolator) — no external tools. Enable these only to fall back to external tools when no model is configured.</p>
  <div class="form-row" style="margin:0">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-allow-ffmpeg">
      <span>Allow ffmpeg (frame I/O / minterpolate)</span>
    </label>
    <span class="form-hint">Off = use PyAV + models only. On = ffmpeg may be used as a fallback.</span>
  </div>
  <div class="form-row" style="margin-top:.5rem">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-allow-rife-ncnn">
      <span>Allow rife-ncnn-vulkan binary</span>
    </label>
    <span class="form-hint">Off = use an in-process RIFE/FILM model. On = the external rife-ncnn-vulkan binary may be used when no model is configured.</span>
  </div>
</div>

<!-- Archive -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Generation Archive</div>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-arc-enabled">
      <span style="font-size:13px;font-weight:500">Enable generation archive</span>
    </label>
    <span class="form-hint">When enabled, every generation (image, video, audio, text) is logged with its prompt, model, and output files.</span>
  </div>
  <div class="form-row">
    <label class="form-label">Archive directory <span class="muted" id="s-arc-dir-hint"></span></label>
    <input type="text" id="s-arc-dir" class="form-input" placeholder="(default)">
    <span class="form-hint">Absolute path or relative to the config directory. Leave blank to use the default.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label class="form-label">Retention period</label>
    <select id="s-arc-retention" class="form-input" style="max-width:200px">
      <option value="1h">1 hour</option>
      <option value="1d">1 day</option>
      <option value="2d">2 days</option>
      <option value="1w">1 week</option>
      <option value="1m">1 month</option>
      <option value="3m">3 months</option>
      <option value="6m">6 months</option>
      <option value="1y">1 year</option>
      <option value="never">Never (keep forever)</option>
    </select>
    <span class="form-hint">Archived entries older than this are automatically deleted. Takes effect immediately on save.</span>
  </div>
</div>

<!-- Thermal protection -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Thermal Protection</div>
  <span class="form-hint" style="display:block;margin-bottom:.75rem">
    Before serving a request against a loaded model, wait until temperatures are
    safe so a long sequence of heavy generations can't overheat the machine and
    trip its power-off protection. The wait is non-blocking (other requests keep
    being accepted) and takes effect immediately on save. Temperatures in °C.
  </span>

  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-therm-gpu-enabled" onchange="toggleThermalFields()">
      <span style="font-size:13px;font-weight:500">Enable GPU temperature protection</span>
    </label>
  </div>
  <div id="therm-gpu-fields" class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
    <div>
      <label class="form-label">Pause when GPU reaches (°C)</label>
      <input type="number" id="s-therm-gpu-high" class="form-input" min="40" max="120" step="1" placeholder="90">
    </div>
    <div>
      <label class="form-label">Resume when GPU drops to (°C)</label>
      <input type="number" id="s-therm-gpu-resume" class="form-input" min="30" max="120" step="1" placeholder="87">
    </div>
  </div>
  <div id="therm-gpu-overrides" class="form-row" style="margin-top:.5rem;display:none">
    <label class="form-label" style="margin-bottom:.3rem">Per-card overrides <span class="muted">(optional — blank uses the limits above)</span></label>
    <span class="form-hint" style="margin-bottom:.4rem">Set a different pause/resume limit per GPU vendor detected on this machine — e.g. let a Radeon run hotter than an NVIDIA card. Each card is judged against its own vendor's limit.</span>
    <!-- One row per GPU vendor actually present, built from detected hardware. -->
    <div id="therm-gpu-overrides-rows" style="display:grid;grid-template-columns:90px 1fr 1fr;gap:.5rem;align-items:center"></div>
  </div>

  <div class="form-row" style="margin-top:.5rem">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-therm-cpu-enabled" onchange="toggleThermalFields()">
      <span style="font-size:13px;font-weight:500">Enable CPU temperature protection</span>
    </label>
  </div>
  <div id="therm-cpu-fields" class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
    <div>
      <label class="form-label">Pause when CPU reaches (°C)</label>
      <input type="number" id="s-therm-cpu-high" class="form-input" min="40" max="120" step="1" placeholder="90">
    </div>
    <div>
      <label class="form-label">Resume when CPU drops to (°C)</label>
      <input type="number" id="s-therm-cpu-resume" class="form-input" min="30" max="120" step="1" placeholder="87">
    </div>
  </div>

  <div class="form-row" style="margin:0 0 .75rem">
    <label class="form-label">Re-check interval while cooling down (seconds)</label>
    <input type="number" id="s-therm-poll" class="form-input" style="max-width:200px" min="1" max="120" step="1" placeholder="5">
    <span class="form-hint">How often to re-read temperatures while waiting for cooldown.</span>
  </div>

  <div class="form-row" style="margin:0;border-top:1px solid var(--border,#2a2a2a);padding-top:.75rem">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-therm-soft-enabled" onchange="toggleThermalFields()">
      <span style="font-size:13px;font-weight:500">Enable CPU soft-throttle</span>
    </label>
    <span class="form-hint" style="display:block;margin-top:.35rem">
      Before the hard pause, gently slow generation (short per-step sleeps) once the
      CPU enters the warm band, so its temperature climbs slower and the full
      cooldown is rarely hit. CPU only — GPU is unaffected.
    </span>
  </div>
  <div id="therm-soft-fields" class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;margin-top:.5rem">
    <div>
      <label class="form-label">Engage above CPU temp (°C)</label>
      <input type="number" id="s-therm-soft-temp" class="form-input" min="40" max="120" step="1" placeholder="80">
    </div>
    <div>
      <label class="form-label">Max slow-down per step (seconds)</label>
      <input type="number" id="s-therm-soft-sleep" class="form-input" min="0" max="30" step="0.5" placeholder="3">
    </div>
  </div>
</div>

<!-- Background jobs -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">Background Jobs</div>
  <span class="form-hint" style="display:block;margin-bottom:.75rem">
    Controls how interrupted LoRA training is handled when CoderAI restarts.
    Equivalent to the <code>--no-resume-jobs</code> launch flag.
  </span>
  <div class="form-row" style="margin:0">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-jobs-resume">
      <span style="font-size:13px;font-weight:500">Resume interrupted training on restart</span>
    </label>
    <span class="form-hint">When off, a training job that was running at restart is marked
      <em>cancelled</em> instead of resuming. Its checkpoint is kept, so you can still
      restart it manually from the Tasks page.</span>
  </div>
</div>

<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">AISBF Broker</div>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-broker-enabled" onchange="toggleBrokerFields()">
      <span style="font-size:13px;font-weight:500">Enable broker client</span>
    </label>
    <span class="form-hint">Registers this CoderAI instance with an AISBF broker so it can receive brokered requests.</span>
  </div>
  <div id="broker-fields" style="display:none">
    <div class="form-row">
      <label class="form-label">Broker base URL</label>
      <input type="text" id="s-broker-base-url" class="form-input" placeholder="https://broker.example.com">
      <span class="form-hint">Supports `http`, `https`, `ws`, or `wss`. The websocket endpoint is derived automatically.</span>
    </div>
    <div style="display:grid;grid-template-columns:180px 1fr;gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Scope</label>
        <select id="s-broker-scope" class="form-input" onchange="toggleBrokerFields()">
          <option value="user">user</option>
          <option value="global">global</option>
        </select>
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Username</label>
        <input type="text" id="s-broker-username" class="form-input" placeholder="alice or global">
        <span class="form-hint">This is forced to `global` for global scope; user scope requires the AISBF username.</span>
      </div>
    </div>
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Provider ID</label>
        <input type="text" id="s-broker-provider-id" class="form-input" placeholder="coderai-local">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Client ID</label>
        <input type="text" id="s-broker-client-id" class="form-input" placeholder="workstation-01">
      </div>
    </div>
    <div class="form-row">
      <label class="form-label">Registration token</label>
      <input type="password" id="s-broker-registration-token" class="form-input" placeholder="Broker registration token">
    </div>
    <div class="form-row">
      <label class="form-label">Advertised endpoint</label>
      <input type="text" id="s-broker-advertised-endpoint" class="form-input" placeholder="http://127.0.0.1:8776">
      <span class="form-hint">Optional external URL advertised to the broker for this instance.</span>
    </div>
    <div class="form-row">
      <label class="form-label">Websocket path override</label>
      <input type="text" id="s-broker-websocket-path" class="form-input" placeholder="/api/coderai/wss">
      <span class="form-hint">Optional manual websocket route override for proxied or custom broker deployments; leave empty to derive from scope.</span>
    </div>
    <div style="display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Heartbeat seconds</label>
        <input type="number" id="s-broker-heartbeat" class="form-input" min="1" placeholder="30">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Connect timeout</label>
        <input type="number" id="s-broker-connect-timeout" class="form-input" min="1" placeholder="10">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Request timeout</label>
        <input type="number" id="s-broker-request-timeout" class="form-input" min="1" placeholder="30">
      </div>
    </div>
    <div style="display:grid;grid-template-columns:repeat(3, minmax(0, 1fr));gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Reconnect initial delay</label>
        <input type="number" id="s-broker-reconnect-initial" class="form-input" min="1" placeholder="1">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Reconnect max delay</label>
        <input type="number" id="s-broker-reconnect-max" class="form-input" min="1" placeholder="60">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">WS ping interval (s)</label>
        <input type="number" id="s-broker-ws-ping" class="form-input" min="5" placeholder="20">
        <span class="form-hint" style="font-size:11px">Keeps connection alive through nginx proxies. Lower if you get 504 timeouts.</span>
      </div>
    </div>
  </div>
</div>

<!-- DeepSeek V4 (ds4) -->
<div class="card mb-0" style="margin-top:1rem">
  <div class="card-title">DeepSeek V4 (ds4)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Run DeepSeek V4 through antirez's native <a href="https://github.com/antirez/ds4" target="_blank" rel="noopener">ds4 / DwarfStar</a> engine as a managed subprocess. When enabled, requests for a matching model name are proxied to ds4-server. First use clones + builds ds4 and downloads several GB of weights — this is slow and needs lots of RAM (96 GB+).</p>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-ds4-enabled" onchange="toggleDs4Fields()">
      <span style="font-size:13px;font-weight:500">Enable ds4 (DeepSeek V4)</span>
    </label>
  </div>
  <div id="ds4-fields" style="display:none">
    <div class="form-row" style="background:var(--bg-2,#1a1a1a);border:1px solid var(--border,#333);border-radius:8px;padding:.7rem .8rem">
      <label class="form-label" style="margin-bottom:.3rem">⬇ Download a default DeepSeek V4 model</label>
      <span class="form-hint" style="margin-bottom:.5rem">Fetches an official GGUF from <code>antirez/deepseek-v4-gguf</code> straight into the gguf cache, where it appears in the model list to configure. Large (tens to ~150 GB) — runs in the background.</span>
      <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap">
        <select id="s-ds4-dl-select" class="form-input" style="flex:1;min-width:280px"></select>
        <button type="button" id="s-ds4-dl-btn" class="btn btn-secondary" onclick="ds4DownloadDefault()">Download</button>
      </div>
      <div id="s-ds4-dl-status" class="form-hint" style="margin-top:.45rem;display:none"></div>
    </div>
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Model id / alias</label>
        <input type="text" id="s-ds4-model-id" class="form-input" placeholder="deepseek-v4">
        <span class="form-hint">When enabled, ds4 automatically serves any <b>deepseek4</b>-architecture GGUF you have (detected by reading the file); mainline DeepSeek GGUFs stay on llama.cpp. This alias also routes (for ds4's own downloaded variant).</span>
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Weight variant <span class="muted">(download fallback)</span></label>
        <select id="s-ds4-variant" class="form-input">
          <option value="q2-imatrix">q2-imatrix (96/128 GB)</option>
          <option value="q2-q4-imatrix">q2-q4-imatrix (96/128 GB)</option>
          <option value="q4-imatrix">q4-imatrix (256 GB+)</option>
          <option value="pro-q2-imatrix">pro-q2-imatrix (512 GB)</option>
        </select>
        <span class="form-hint">Only used when auto-download is enabled below.</span>
      </div>
    </div>
    <div class="form-row">
      <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-ds4-auto-download">
        <span>Auto-download the weight variant <span class="muted">— OFF by default; when off, ds4 only serves deepseek4 GGUFs you already have and errors if none resolves (never pulls tens of GB silently)</span></span>
      </label>
    </div>
    <div class="form-row">
      <label class="form-label">Model GGUF path <span class="muted">(optional override)</span></label>
      <input type="text" id="s-ds4-model-path" class="form-input" placeholder="/AI/guffcache/…-ds4-Q2_K.gguf">
      <span class="form-hint">Force ds4-server to load this exact GGUF (skips the variant download). Leave blank to auto-serve the requested deepseek4 model.</span>
    </div>
    <div class="form-row">
      <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-ds4-ssd-streaming">
        <span>SSD streaming <span class="muted">— stream MoE experts from disk/SSD (run a 100GB+ model on a small GPU + modest RAM; slower)</span></span>
      </label>
    </div>
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Build target</label>
        <select id="s-ds4-build-target" class="form-input">
          <option value="auto">auto-detect</option>
          <option value="cuda-generic">cuda-generic</option>
          <option value="cuda-spark">cuda-spark (DGX Spark/GB10)</option>
          <option value="metal">metal (macOS)</option>
          <option value="cpu">cpu</option>
        </select>
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Install dir</label>
        <input type="text" id="s-ds4-install-dir" class="form-input" placeholder="~/.coderai/ds4">
      </div>
    </div>
    <div class="form-row">
      <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-ds4-auto-build">
        <span>Auto clone + build the ds4-server binary if missing</span>
      </label>
    </div>
    <div style="display:grid;grid-template-columns:1fr 120px 140px;gap:1rem;align-items:start">
      <div class="form-row" style="margin:0">
        <label class="form-label">Bind host</label>
        <input type="text" id="s-ds4-host" class="form-input" placeholder="127.0.0.1">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Port</label>
        <input type="number" id="s-ds4-port" class="form-input" min="0" placeholder="0 = auto">
      </div>
      <div class="form-row" style="margin:0">
        <label class="form-label">Context (--ctx)</label>
        <input type="number" id="s-ds4-ctx" class="form-input" min="1024" placeholder="100000">
      </div>
    </div>
    <div class="form-row">
      <label class="form-label">Repo URL</label>
      <input type="text" id="s-ds4-repo-url" class="form-input" placeholder="https://github.com/antirez/ds4">
    </div>
    <div class="form-row">
      <label class="form-label">Extra ds4-server args</label>
      <input type="text" id="s-ds4-extra-args" class="form-input" placeholder="--ssd-streaming-cache-experts 12GB --ssd-streaming-cold">
      <span class="form-hint">Passed verbatim to <code>ds4-server</code>. <code>--kv-disk-dir</code> is auto-set to coderai's offload dir unless you override it here. With SSD streaming on CUDA, sizing the routed expert cache helps, e.g. <code>--ssd-streaming-cache-experts 12GB</code>. Note: extremely low quants (Q2_K) can fail ds4's CUDA prefill — prefer Q4_K+.</span>
    </div>
    <div class="form-row">
      <label class="form-label">CUDA expert-cache VRAM reserve (GiB)</label>
      <input type="number" id="s-ds4-expert-cache-reserve" class="form-input" min="0" placeholder="0 = ds4 default (½ the card)">
      <span class="form-hint">Exports <code>DS4_CUDA_STREAMING_EXPERT_CACHE_RESERVE_GB</code> — VRAM ds4 keeps free for non-cache use under SSD streaming. ds4 defaults to <b>half the card</b>, which over-reserves for small-weight MoE models and caps the expert cache far below your <code>--ssd-streaming-cache-experts</code> request. Set just above the model's resident weights (+~2 GiB headroom) to hand the rest to the cache; <code>0</code> leaves ds4's default. There is no CLI flag for this — it's env-only.</span>
    </div>
    <div class="form-row">
      <label class="form-label">Extra ds4-server env</label>
      <input type="text" id="s-ds4-extra-env" class="form-input" placeholder="DS4_CUDA_WEIGHT_ARENA_CHUNK_MB=512">
      <span class="form-hint">Whitespace-separated <code>KEY=VALUE</code> pairs added to ds4-server's environment — for the many CUDA tunables it exposes only via env. Notably <code>DS4_CUDA_WEIGHT_ARENA_CHUNK_MB</code> (default 1792): lower it (e.g. <code>512</code>) so the weight arena allocates in smaller chunks that fit a heap fragmented by the streaming expert cache, fixing <i>model arena alloc failed … out of memory</i>.</span>
    </div>
    <div class="form-row" style="background:var(--bg-2,#1a1a1a);border:1px solid var(--border,#333);border-radius:8px;padding:.7rem .8rem">
      <label class="form-label" style="margin-bottom:.3rem">⚡ Tuning ds4 for performance</label>
      <span class="form-hint" style="font-size:11.5px;line-height:1.55">
        These are global <b>defaults</b>; override them per-model on the <b>Models</b> page (a big-quant streamer and a small one that fits VRAM want different settings). Order of impact:
        <br>• <b>Put the GGUF on a fast NVMe/SSD</b>, not a spinning disk — for a streamed MoE this is the single biggest win (≈10× prefill). A symlink from the old path works.
        <br>• <b>Cap the expert cache</b> with <code>--ssd-streaming-cache-experts N</code> (a COUNT, e.g. <code>400</code> — not NGB). Bigger = better decode hit-rate, but too big fragments VRAM and the weight arena OOMs; find the ceiling for your card.
        <br>• <b>Reserve</b> ≈ resident weights + ~2 GiB (field above). ds4 defaults to half the card, which starves the cache.
        <br>• Add <code>DS4_CUDA_WEIGHT_ARENA_CHUNK_MB=512</code> (env) to stop the weight-arena OOM under heavy caching.
        <br>• <b>Avoid</b> <code>DS4_CUDA_WEIGHT_CACHE</code> — it disables ds4's zero-copy path and was slower in testing.
        <br>• Decode of a model far larger than VRAM is streaming-bound (~0.5 tok/s here); a <b>smaller quant</b> that mostly fits VRAM+RAM is the only real decode fix. But <b>i-quants (IQ2/IQ3) and Q2_K fail ds4's CUDA prefill</b> (<i>gpu layer 0 ffn batch encode failed</i> → empty reply) — they load but can't generate. Use <b>K-quants Q4_K and up</b>. Context window is set per-model via <b>n_ctx</b>.
      </span>
    </div>
    <div class="form-row">
      <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-ds4-kv-cleanup" onchange="toggleDs4KvCleanup()">
        <span style="font-size:13px;font-weight:500">Auto-clean the on-disk KV cache</span>
      </label>
      <span class="form-hint">ds4 writes prompt KV checkpoints to its <code>--kv-disk-dir</code> (<code>&lt;offload&gt;/ds4-kv</code>) to reuse prefixes across requests; abandoned sessions accumulate large files that never self-prune. When on, a janitor deletes a checkpoint only when BOTH: its newest file is older than the age below, AND no running <code>ds4-server</code> currently has it open (fd or mmap). Files ds4 is actively using are always kept, regardless of age.</span>
    </div>
    <div id="ds4-kv-cleanup-fields" style="display:none">
      <div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
        <div>
          <label class="form-label">Delete older than <span class="muted">(hours)</span></label>
          <input type="number" id="s-ds4-kv-max-age" class="form-input" min="0" step="1" placeholder="168 (7 days)">
        </div>
        <div>
          <label class="form-label">Sweep every <span class="muted">(minutes)</span></label>
          <input type="number" id="s-ds4-kv-interval" class="form-input" min="1" step="1" placeholder="360 (6 hours)">
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Auto-compact context -->
<div class="card">
  <div class="card-title">Auto-compact context</div>
  <p class="form-hint" style="margin-bottom:.6rem">Global <b>defaults</b> for shrinking an over-long chat history before generation so requests don't error out on context overflow. Per-model settings on the <b>Models</b> page override these. OFF by default.</p>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-compact-enabled" onchange="toggleCompactFields()">
      <span style="font-size:13px;font-weight:500">Enable auto-compaction by default</span>
    </label>
  </div>
  <div id="compact-fields" style="display:none">
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
      <div class="form-row">
        <label class="form-label">Trigger at <span class="muted">(% of context)</span></label>
        <input type="number" id="s-compact-pct" class="form-input" min="50" max="99" step="1" placeholder="85">
        <span class="form-hint">Compacts to ~65% of the context window when the prompt reaches this %.</span>
      </div>
      <div class="form-row">
        <label class="form-label">Strategy</label>
        <select id="s-compact-strategy" class="form-input">
          <option value="drop_oldest">Drop oldest — keep system + most recent turns</option>
          <option value="keep_head_tail">Keep head + tail — also keep the first turn, drop the middle</option>
          <option value="summarize">Summarize — replace the dropped middle with an LLM summary</option>
        </select>
      </div>
    </div>
    <div class="form-row">
      <label class="form-label">Summarizer model <span class="muted">(summarize strategy)</span></label>
      <input type="text" id="s-compact-model" class="form-input" placeholder="(empty = the model serving the request)">
      <span class="form-hint">Model id/alias that writes the summary. Empty = the request's own model summarizes itself. Point it at a smaller/faster model to summarize old turns while the main model answers — the dropped history is chunked to fit the chosen model's own context window. The client receives a status message and live progress while compaction runs.</span>
    </div>
  </div>
</div>
{% endblock %}

{% block scripts %}
<script>
// Build one per-vendor threshold-override row for each GPU vendor actually present
// on this machine (detected via /admin/api/gpu-stats) — never hardcode the mix.
const _VENDOR_LABEL = {nvidia:'NVIDIA', amd:'Radeon', intel:'Intel'};
const _VENDOR_COLOR = {nvidia:'#76b900', amd:'#ed1c24', intel:'#0071c5'};
async function _buildGpuOverrideRows(overrides){
  const wrap = document.getElementById('therm-gpu-overrides');
  const rows = document.getElementById('therm-gpu-overrides-rows');
  let vendors = [];
  try {
    const cards = ((await fetch(ROOT_PATH + '/admin/api/gpu-stats').then(r=>r.json())).cards) || [];
    vendors = [...new Set(cards.map(c => c.vendor).filter(Boolean))];
  } catch(e) {}
  // Also include any vendor that already has a saved override, even if not detected now.
  Object.keys(overrides || {}).forEach(v => { if(!vendors.includes(v)) vendors.push(v); });
  if(!vendors.length){ wrap.style.display = 'none'; rows.innerHTML = ''; return; }
  wrap.style.display = '';
  rows.innerHTML = vendors.map(v => {
    const ov = (overrides && overrides[v]) || {};
    const label = _VENDOR_LABEL[v] || v;
    const color = _VENDOR_COLOR[v] || 'var(--text-2)';
    return `<span style="font-size:12px;color:${color}">${label}</span>
      <input type="number" class="form-input therm-ov" data-vendor="${v}" data-k="high"
             min="40" max="120" step="1" placeholder="pause °C" value="${ov.high ?? ''}">
      <input type="number" class="form-input therm-ov" data-vendor="${v}" data-k="resume"
             min="30" max="120" step="1" placeholder="resume °C" value="${ov.resume ?? ''}">`;
  }).join('');
}
function _collectGpuOverrides(){
  const ov = {};
  document.querySelectorAll('#therm-gpu-overrides-rows .therm-ov').forEach(inp => {
    const v = parseFloat(inp.value);
    if(isNaN(v)) return;
    const vendor = inp.dataset.vendor;
    (ov[vendor] = ov[vendor] || {})[inp.dataset.k] = v;
  });
  return ov;
}
// Per-engine concurrency overrides — one row per running engine (by name).
function _buildConcurrencyOverrides(engNames, parOv, instOv){
  const wrap = document.getElementById('concurrency-overrides');
  const rows = document.getElementById('concurrency-override-rows');
  if(!engNames || engNames.length < 2){ wrap.style.display='none'; rows.innerHTML=''; return; }
  wrap.style.display = '';
  rows.innerHTML =
    `<span class="muted" style="font-size:11px">Engine</span>
     <span class="muted" style="font-size:11px">Max parallel</span>
     <span class="muted" style="font-size:11px">Max instances</span>` +
    engNames.map(n => `
      <span style="font-size:12px">${n}</span>
      <input type="number" class="form-input conc-par" data-engine="${n}" min="1" max="64" placeholder="default" value="${parOv[n] ?? ''}">
      <input type="number" class="form-input conc-inst" data-engine="${n}" min="1" max="16" placeholder="default" value="${instOv[n] ?? ''}">`).join('');
}
function _collectConcOverrides(cls){
  const o = {};
  document.querySelectorAll('#concurrency-override-rows .' + cls).forEach(inp => {
    const v = parseInt(inp.value);
    if(!isNaN(v) && v >= 1) o[inp.dataset.engine] = v;
  });
  return o;
}
function toggleDs4Fields(){
  document.getElementById('ds4-fields').style.display =
    document.getElementById('s-ds4-enabled').checked ? 'block' : 'none';
}
function toggleCompactFields(){
  document.getElementById('compact-fields').style.display =
    document.getElementById('s-compact-enabled').checked ? 'block' : 'none';
}
function toggleDs4KvCleanup(){
  document.getElementById('ds4-kv-cleanup-fields').style.display =
    document.getElementById('s-ds4-kv-cleanup').checked ? 'block' : 'none';
}

let _ds4DefaultModels = [];
async function loadDs4DefaultModels(){
  const sel = document.getElementById('s-ds4-dl-select');
  if(!sel) return;
  try{
    const d = await fetch(ROOT_PATH + '/admin/api/ds4/default-models').then(r=>r.json());
    _ds4DefaultModels = d.models || [];
    sel.innerHTML = _ds4DefaultModels.map(m =>
      `<option value="${m.file}">${m.label}${m.present ? ' ✓ downloaded' : ''}</option>`).join('');
  }catch(e){
    sel.innerHTML = '<option value="">(failed to load catalog)</option>';
  }
}

async function ds4DownloadDefault(){
  const sel = document.getElementById('s-ds4-dl-select');
  const btn = document.getElementById('s-ds4-dl-btn');
  const statusEl = document.getElementById('s-ds4-dl-status');
  const file = sel.value;
  const meta = _ds4DefaultModels.find(m => m.file === file);
  if(!file || !meta){ return; }
  if(meta.present && !confirm('This model is already in the gguf cache. Download again?')) return;
  btn.disabled = true;
  statusEl.style.display = 'block';
  statusEl.textContent = 'Starting download…';
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/model-download', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({model_id: meta.repo, file_pattern: file}),
    });
    if(!r.ok) throw new Error('HTTP '+r.status);
    const {session_id} = await r.json();
    const es = new EventSource(ROOT_PATH + '/admin/api/download-stream/' + session_id);
    es.onmessage = (ev) => {
      let m; try{ m = JSON.parse(ev.data); }catch(_){ return; }
      if(m.type === 'progress'){
        const pct = (m.percent||0).toFixed(1);
        const rate = m.rate ? ' · ' + (m.rate/1048576).toFixed(1) + ' MB/s' : '';
        statusEl.textContent = `Downloading ${meta.label.split(' (')[0]}: ${pct}%${rate}`;
      }else if(m.type === 'done'){
        statusEl.textContent = '✅ Downloaded — now available in the model list to configure.';
        es.close(); btn.disabled = false; loadDs4DefaultModels();
      }else if(m.type === 'error'){
        statusEl.textContent = '⚠ Download failed: ' + (m.message||'unknown error');
        es.close(); btn.disabled = false;
      }
    };
    es.onerror = () => { es.close(); btn.disabled = false; };
  }catch(e){
    statusEl.textContent = '⚠ Could not start download: ' + e.message;
    btn.disabled = false;
  }
}
function toggleHttps(){
  document.getElementById('https-fields').style.display =
    document.getElementById('s-https').checked ? 'block' : 'none';
}

function toggleBrokerFields(){
  document.getElementById('broker-fields').style.display =
    document.getElementById('s-broker-enabled').checked ? 'block' : 'none';

  const scope = document.getElementById('s-broker-scope').value;
  const usernameInput = document.getElementById('s-broker-username');
  if(scope === 'global'){
    usernameInput.value = 'global';
    usernameInput.readOnly = true;
  } else {
    usernameInput.readOnly = false;
    if(usernameInput.value === 'global') usernameInput.value = '';
  }
}

function toggleThermalFields(){
  document.getElementById('therm-gpu-fields').style.display =
    document.getElementById('s-therm-gpu-enabled').checked ? 'grid' : 'none';
  document.getElementById('therm-cpu-fields').style.display =
    document.getElementById('s-therm-cpu-enabled').checked ? 'grid' : 'none';
  document.getElementById('therm-soft-fields').style.display =
    document.getElementById('s-therm-soft-enabled').checked ? 'grid' : 'none';
}

function showAlert(type, msg){
  const el = document.getElementById('settings-alert');
  el.className = 'alert alert-' + (type === 'error' ? 'error' : 'info');
  el.textContent = msg;
  el.style.display = 'flex';
  if(type !== 'error') setTimeout(()=>{ el.style.display='none'; }, 4000);
}

async function loadSettings(){
  try{
    const d = await fetch(ROOT_PATH + '/admin/api/settings').then(r=>r.json());
    document.getElementById('s-host').value  = d.server?.host ?? '0.0.0.0';
    document.getElementById('s-port').value  = d.server?.port ?? 8000;
    document.getElementById('s-https').checked = !!d.server?.https;
    document.getElementById('s-key').value   = d.server?.https_key_path ?? '';
    document.getElementById('s-cert').value  = d.server?.https_cert_path ?? '';
    document.getElementById('s-queue-max').value = d.server?.queue_max_size ?? 6;
    document.getElementById('s-internal-port-base').value = d.server?.internal_port_base ?? 8780;
    // Default engine — surfaced when 2+ engines are running. Sourced from the
    // front's live engine list so it also covers AUTO-DETECTED engines (no
    // engine_specs needed); falls back to declared engine_specs names.
    let engNames = [];
    try {
      const er = await fetch(ROOT_PATH + '/admin/api/engines');
      if (er.ok) engNames = ((await er.json()).engines || []).map(e => e.name);
    } catch(e) {}
    if (!engNames.length) engNames = d.server?.engine_names || [];
    const engRow = document.getElementById('default-engine-row');
    const engSel = document.getElementById('s-default-engine');
    if (engNames.length > 1) {
      engSel.querySelectorAll('option:not([value=""])').forEach(o => o.remove());
      engNames.forEach(n => { const o=document.createElement('option'); o.value=n; o.textContent=n; engSel.appendChild(o); });
      engSel.value = d.server?.default_engine || '';
      engRow.style.display = '';
    } else {
      engRow.style.display = 'none';
    }
    // Concurrency: defaults + per-engine override rows.
    document.getElementById('s-max-parallel').value = d.server?.max_parallel_requests ?? 2;
    document.getElementById('s-max-instances').value = d.models?.max_model_instances ?? 1;
    _buildConcurrencyOverrides(engNames,
      d.server?.max_parallel_requests_overrides || {},
      d.models?.max_model_instances_overrides || {});
    document.getElementById('s-hf-cache').value   = d.models?.hf_cache_dir ?? '';
    document.getElementById('s-gguf-cache').value = d.models?.gguf_cache_dir ?? '';
    document.getElementById('s-offload-dir').value = d.offload?.directory ?? './offload';
    document.getElementById('s-max-ram').value = d.offload?.max_ram_gb ?? '';
    document.getElementById('s-evict-idle-ram').checked = d.offload?.evict_idle_on_ram !== false;
    document.getElementById('s-ram-leak-watch').checked = d.offload?.ram_leak_watch !== false;
    document.getElementById('s-ram-watch-cuda').checked = d.offload?.ram_watch_cuda !== false;
    document.getElementById('s-ram-watch-poll').value = d.offload?.ram_watch_poll_seconds ?? '';
    document.getElementById('s-ram-watch-soft').value = d.offload?.ram_watch_soft_fraction ?? '';
    document.getElementById('s-tmp-dir').value = d.tmp_dir ?? '';
    document.getElementById('s-allow-ffmpeg').checked = !!(d.enhance && d.enhance.allow_ffmpeg);
    document.getElementById('s-allow-rife-ncnn').checked = !!(d.enhance && d.enhance.allow_rife_ncnn);
    toggleHttps();
    // Archive
    const arc = d.archive || {};
    document.getElementById('s-arc-enabled').checked = !!arc.enabled;
    document.getElementById('s-arc-dir').value = arc.directory ?? '';
    document.getElementById('s-arc-retention').value = arc.retention ?? 'never';
    // Show effective default dir as placeholder hint
    try {
      const as = await fetch(ROOT_PATH + '/admin/api/archive-settings').then(r=>r.json());
      const hint = document.getElementById('s-arc-dir-hint');
      if (hint && as.default_directory) hint.textContent = `(default: ${as.default_directory})`;
      document.getElementById('s-arc-dir').placeholder = as.default_directory || '(default)';
    } catch(_){}
    const broker = d.broker || {};
    document.getElementById('s-broker-enabled').checked = !!broker.enabled;
    document.getElementById('s-broker-base-url').value = broker.base_url ?? '';
    document.getElementById('s-broker-scope').value = broker.scope ?? 'user';
    document.getElementById('s-broker-username').value = broker.username ?? '';
    document.getElementById('s-broker-provider-id').value = broker.provider_id ?? '';
    document.getElementById('s-broker-client-id').value = broker.client_id ?? '';
    document.getElementById('s-broker-registration-token').value = broker.registration_token ?? '';
    document.getElementById('s-broker-advertised-endpoint').value = broker.advertised_endpoint ?? '';
    document.getElementById('s-broker-websocket-path').value = broker.websocket_path ?? '';
    document.getElementById('s-broker-heartbeat').value = broker.heartbeat_interval_seconds ?? 30;
    document.getElementById('s-broker-connect-timeout').value = broker.connect_timeout_seconds ?? 10;
    document.getElementById('s-broker-request-timeout').value = broker.request_timeout_seconds ?? 30;
    document.getElementById('s-broker-reconnect-initial').value = broker.reconnect_initial_delay_seconds ?? 1;
    document.getElementById('s-broker-reconnect-max').value = broker.reconnect_max_delay_seconds ?? 60;
    document.getElementById('s-broker-ws-ping').value = broker.websocket_ping_interval ?? 20;
    toggleBrokerFields();
    // Auto-compact context
    const compact = d.compaction || {};
    document.getElementById('s-compact-enabled').checked = !!compact.enabled;
    document.getElementById('s-compact-pct').value = compact.pct ?? 85;
    document.getElementById('s-compact-strategy').value = compact.strategy || 'drop_oldest';
    document.getElementById('s-compact-model').value = compact.model ?? '';
    toggleCompactFields();
    // Thermal protection
    const therm = d.thermal || {};
    document.getElementById('s-therm-gpu-enabled').checked = therm.gpu_enabled !== false;
    document.getElementById('s-therm-cpu-enabled').checked = therm.cpu_enabled !== false;
    document.getElementById('s-therm-gpu-high').value = therm.gpu_high ?? 90;
    document.getElementById('s-therm-gpu-resume').value = therm.gpu_resume ?? 87;
    await _buildGpuOverrideRows(therm.gpu_overrides || {});
    document.getElementById('s-therm-cpu-high').value = therm.cpu_high ?? 90;
    document.getElementById('s-therm-cpu-resume').value = therm.cpu_resume ?? 87;
    document.getElementById('s-therm-poll').value = therm.poll_seconds ?? 5;
    document.getElementById('s-therm-soft-enabled').checked = !!therm.soft_throttle_enabled;
    document.getElementById('s-therm-soft-temp').value = therm.soft_throttle_temp ?? 80;
    document.getElementById('s-therm-soft-sleep').value = therm.soft_throttle_max_sleep ?? 3;
    toggleThermalFields();
    // Background jobs
    const jobs = d.jobs || {};
    document.getElementById('s-jobs-resume').checked = jobs.resume_on_restart !== false;
    // DeepSeek V4 (ds4)
    const ds4 = d.ds4 || {};
    document.getElementById('s-ds4-enabled').checked = !!ds4.enabled;
    document.getElementById('s-ds4-model-id').value = ds4.model_id ?? 'deepseek-v4';
    document.getElementById('s-ds4-variant').value = ds4.model_variant ?? 'q4-imatrix';
    document.getElementById('s-ds4-model-path').value = ds4.model_path ?? '';
    document.getElementById('s-ds4-auto-download').checked = !!ds4.auto_download;
    document.getElementById('s-ds4-ssd-streaming').checked = !!ds4.ssd_streaming;
    document.getElementById('s-ds4-build-target').value = ds4.build_target ?? 'auto';
    document.getElementById('s-ds4-install-dir').value = ds4.install_dir ?? '';
    document.getElementById('s-ds4-auto-build').checked = ds4.auto_build !== false;
    document.getElementById('s-ds4-host').value = ds4.host ?? '127.0.0.1';
    document.getElementById('s-ds4-port').value = ds4.port ?? 0;
    document.getElementById('s-ds4-ctx').value = ds4.ctx ?? 100000;
    document.getElementById('s-ds4-repo-url').value = ds4.repo_url ?? 'https://github.com/antirez/ds4';
    document.getElementById('s-ds4-extra-args').value = ds4.extra_args ?? '';
    document.getElementById('s-ds4-expert-cache-reserve').value = ds4.expert_cache_reserve_gb ?? 0;
    document.getElementById('s-ds4-extra-env').value = ds4.extra_env ?? '';
    document.getElementById('s-ds4-kv-cleanup').checked = !!ds4.kv_cache_cleanup_enabled;
    document.getElementById('s-ds4-kv-max-age').value = ds4.kv_cache_max_age_hours ?? 168;
    document.getElementById('s-ds4-kv-interval').value = ds4.kv_cache_cleanup_interval_minutes ?? 360;
    toggleDs4KvCleanup();
    toggleDs4Fields();
    loadDs4DefaultModels();
  }catch(e){ showAlert('error','Failed to load settings: '+e.message); }
}

async function saveSettings(){
  const strOrNull = id => document.getElementById(id).value.trim() || null;
  const data = {
    server:{
      host: document.getElementById('s-host').value.trim() || '0.0.0.0',
      port: parseInt(document.getElementById('s-port').value) || 8000,
      https: document.getElementById('s-https').checked,
      https_key_path:  strOrNull('s-key'),
      https_cert_path: strOrNull('s-cert'),
      queue_max_size: parseInt(document.getElementById('s-queue-max').value) || 6,
      internal_port_base: parseInt(document.getElementById('s-internal-port-base').value) || 8780,
      max_parallel_requests: parseInt(document.getElementById('s-max-parallel').value) || 2,
      max_parallel_requests_overrides: _collectConcOverrides('conc-par'),
      default_engine: document.getElementById('s-default-engine').value || null,
    },
    models:{
      hf_cache_dir:   strOrNull('s-hf-cache'),
      gguf_cache_dir: strOrNull('s-gguf-cache'),
      max_model_instances: parseInt(document.getElementById('s-max-instances').value) || 1,
      max_model_instances_overrides: _collectConcOverrides('conc-inst'),
    },
    offload:{
      directory: document.getElementById('s-offload-dir').value.trim() || './offload',
      max_ram_gb: (parseFloat(document.getElementById('s-max-ram').value) || null),
      evict_idle_on_ram: document.getElementById('s-evict-idle-ram').checked,
      ram_leak_watch: document.getElementById('s-ram-leak-watch').checked,
      ram_watch_cuda: document.getElementById('s-ram-watch-cuda').checked,
      ram_watch_poll_seconds: (parseFloat(document.getElementById('s-ram-watch-poll').value) || null),
      ram_watch_soft_fraction: (parseFloat(document.getElementById('s-ram-watch-soft').value) || null),
    },
    tmp_dir: strOrNull('s-tmp-dir'),
    enhance:{
      allow_ffmpeg: document.getElementById('s-allow-ffmpeg').checked,
      allow_rife_ncnn: document.getElementById('s-allow-rife-ncnn').checked,
    },
    archive:{
      enabled: document.getElementById('s-arc-enabled').checked,
      directory: document.getElementById('s-arc-dir').value.trim(),
      retention: document.getElementById('s-arc-retention').value,
    },
    thermal:{
      gpu_enabled: document.getElementById('s-therm-gpu-enabled').checked,
      cpu_enabled: document.getElementById('s-therm-cpu-enabled').checked,
      gpu_high:   parseFloat(document.getElementById('s-therm-gpu-high').value)   || 90,
      gpu_resume: parseFloat(document.getElementById('s-therm-gpu-resume').value) || 87,
      gpu_overrides: _collectGpuOverrides(),
      cpu_high:   parseFloat(document.getElementById('s-therm-cpu-high').value)   || 90,
      cpu_resume: parseFloat(document.getElementById('s-therm-cpu-resume').value) || 87,
      poll_seconds: parseFloat(document.getElementById('s-therm-poll').value) || 5,
      soft_throttle_enabled: document.getElementById('s-therm-soft-enabled').checked,
      soft_throttle_temp: parseFloat(document.getElementById('s-therm-soft-temp').value) || 80,
      soft_throttle_max_sleep: parseFloat(document.getElementById('s-therm-soft-sleep').value) || 0,
    },
    jobs:{
      resume_on_restart: document.getElementById('s-jobs-resume').checked,
    },
    ds4:{
      enabled: document.getElementById('s-ds4-enabled').checked,
      model_id: document.getElementById('s-ds4-model-id').value.trim() || 'deepseek-v4',
      model_variant: document.getElementById('s-ds4-variant').value,
      model_path: document.getElementById('s-ds4-model-path').value.trim(),
      auto_download: document.getElementById('s-ds4-auto-download').checked,
      ssd_streaming: document.getElementById('s-ds4-ssd-streaming').checked,
      build_target: document.getElementById('s-ds4-build-target').value,
      install_dir: document.getElementById('s-ds4-install-dir').value.trim(),
      auto_build: document.getElementById('s-ds4-auto-build').checked,
      host: document.getElementById('s-ds4-host').value.trim() || '127.0.0.1',
      port: parseInt(document.getElementById('s-ds4-port').value) || 0,
      ctx: parseInt(document.getElementById('s-ds4-ctx').value) || 100000,
      repo_url: document.getElementById('s-ds4-repo-url').value.trim() || 'https://github.com/antirez/ds4',
      extra_args: document.getElementById('s-ds4-extra-args').value.trim(),
      expert_cache_reserve_gb: parseInt(document.getElementById('s-ds4-expert-cache-reserve').value) || 0,
      extra_env: document.getElementById('s-ds4-extra-env').value.trim(),
      kv_cache_cleanup_enabled: document.getElementById('s-ds4-kv-cleanup').checked,
      kv_cache_max_age_hours: parseFloat(document.getElementById('s-ds4-kv-max-age').value) || 168,
      kv_cache_cleanup_interval_minutes: parseFloat(document.getElementById('s-ds4-kv-interval').value) || 360,
    },
    compaction:{
      enabled: document.getElementById('s-compact-enabled').checked,
      pct: parseInt(document.getElementById('s-compact-pct').value) || 85,
      strategy: document.getElementById('s-compact-strategy').value || 'drop_oldest',
      model: document.getElementById('s-compact-model').value.trim(),
    },
    broker:{
      enabled: document.getElementById('s-broker-enabled').checked,
      base_url: document.getElementById('s-broker-base-url').value.trim(),
      scope: document.getElementById('s-broker-scope').value,
      username: document.getElementById('s-broker-username').value.trim(),
      provider_id: document.getElementById('s-broker-provider-id').value.trim(),
      client_id: document.getElementById('s-broker-client-id').value.trim(),
      registration_token: document.getElementById('s-broker-registration-token').value.trim(),
      advertised_endpoint: document.getElementById('s-broker-advertised-endpoint').value.trim(),
      websocket_path: document.getElementById('s-broker-websocket-path').value.trim(),
      heartbeat_interval_seconds: parseInt(document.getElementById('s-broker-heartbeat').value) || 30,
      connect_timeout_seconds: parseInt(document.getElementById('s-broker-connect-timeout').value) || 10,
      request_timeout_seconds: parseInt(document.getElementById('s-broker-request-timeout').value) || 30,
      reconnect_initial_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-initial').value) || 1,
      reconnect_max_delay_seconds: parseInt(document.getElementById('s-broker-reconnect-max').value) || 60,
      websocket_ping_interval: parseInt(document.getElementById('s-broker-ws-ping').value) || 20,
      transport: 'websocket',
    },
  };
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/settings',{
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify(data)
    });
    if(r.ok){
      const d = await r.json().catch(()=>({}));
      if(d.warnings && d.warnings.length){
        showAlert('error', 'Saved, but: ' + d.warnings.join(' '));
      } else {
        showAlert('info','Settings saved. Archive and thermal-protection changes take effect immediately; restart CoderAI for other changes.');
      }
    }
    else{ const e=await r.json(); showAlert('error', e.detail||'Save failed'); }
  }catch(e){ showAlert('error','Error: '+e.message); }
}

loadSettings();
</script>
{% endblock %}
