{% 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:100px 1fr 1fr 1.2fr 1fr;gap:.5rem;align-items:center"></div>
    <span class="form-hint" style="margin-top:.4rem"><b>GPU power lock</b> pins an AMD card to a fixed power/clock state, avoiding the DPM transitions that hang Polaris/GCN cards under sustained Vulkan compute. <em>high</em> = max clocks (runs hotter). Applied at engine start; in an unprivileged container it logs the host command instead.</span>
    <span class="form-hint"><b>Rate limit</b> spaces inference requests to the engine by at least this many milliseconds (0 = no limit) — throttles GPU duty cycle to stabilise a marginal card that wedges under sustained back-to-back compute.</span>
  </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-gpu-split">
      <span>Split models across all GPUs (cross-backend pooling)</span>
    </label>
    <span class="form-hint">Default for every model: pool one model's layers across all GPUs of all backends (e.g. NVIDIA 3090 + Radeon) for more total VRAM. Off = each model stays on its own backend's card(s) (multiple same-backend cards still split). A per-model "Engine / card → All GPUs" setting overrides this. The slower card bottlenecks each token.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label class="form-label">Default split strategy</label>
    <select id="s-split-strategy" class="form-input">
      <option value="vram">Maximize VRAM — proportional to free memory (biggest model)</option>
      <option value="performance">Maximize speed — fill the fast card first, least on the slow one</option>
    </select>
    <span class="form-hint">Default for split models when no explicit ratio is set. <b>VRAM</b> fits the largest model; <b>Speed</b> keeps most layers on the fast card so the slow one bottlenecks less. Per-model setting overrides this.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label class="form-label">Per-card VRAM cap <span class="muted">(GB per physical card)</span></label>
    <div id="s-split-card-caps"><span class="form-hint">Detecting cards…</span></div>
    <span class="form-hint">Cap how much VRAM the auto-split may place on each specific card on this machine — applies to every engine. e.g. set the <b>RX 580</b> to 4 GB and the <b>RTX 3090</b> to 20 GB independently; the rest stays on another card or spills to CPU. Blank/0 = no cap for that card.</span>
  </div>
  <div class="form-row" style="margin:0">
    <label class="form-label">Default weight distribution <span class="muted">(when splitting)</span></label>
    <input type="text" id="s-tensor-split" class="form-input" placeholder="auto (by strategy)">
    <span class="form-hint">Comma-separated ratio in llama.cpp device order (CUDA cards first, then Vulkan), e.g. <b>0.8,0.2</b>. Blank = automatic per the strategy above.</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>

<!-- GLM-5.2 (colibri) -->
<div class="card">
  <div class="card-title">GLM-5.2 (colibri)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Run GLM-5.2 through JustVugg's pure-C <a href="https://github.com/JustVugg/colibri" target="_blank" rel="noopener">colibri</a> MoE engine. Unlike ds4, colibri ships no server — coderai drives the C engine binary <b>directly</b> over its stdin/stdout mux protocol (no colibri Python at runtime). First use clones + builds the engine (CUDA when available). The GLM-5.2 int4 container (~372&nbsp;GB directory) is <b>not</b> downloaded automatically — point the model path at it (see <a href="https://huggingface.co/mastouri/GLM-5.2-colibri-int4-g64-with-int8-mtp" target="_blank" rel="noopener">the int4 g64 + int8-MTP container</a>).</p>
  <div class="form-group">
    <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-colibri-enabled" onchange="toggleColibriFields()">
      <span style="font-size:13px;font-weight:500">Enable colibri (GLM-5.2)</span>
    </label>
  </div>
  <div id="colibri-fields" style="display:none">
    <div class="form-group">
      <label class="form-label">Download the GLM-5.2 container</label>
      <div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
        <select id="s-colibri-dl-select" class="form-input" style="flex:1;min-width:280px"></select>
        <button type="button" id="s-colibri-dl-btn" class="btn btn-secondary" onclick="colibriDownloadDefault()">Download</button>
      </div>
      <span class="form-hint">Fetches the full HF repo (a directory of int4 shards, ~372&nbsp;GB) like any other model and registers it as a colibri-backed model pointing at the snapshot dir. Keep the HF cache on fast storage (NVMe/ext4). Resumable.</span>
      <div id="s-colibri-dl-status" class="form-hint" style="margin-top:.45rem;display:none"></div>
    </div>
    <div class="form-group">
      <label class="form-label">Model id / alias</label>
      <input type="text" id="s-colibri-model-id" class="form-input" placeholder="glm-5.2-colibri">
      <span class="form-hint">When enabled, this id (and names containing <b>glm-5.2</b> / <b>colibri</b>, or any model whose config <code>backend</code> is <code>colibri</code>) routes to the colibri engine.</span>
    </div>
    <div class="form-group">
      <label class="form-label">Model container path</label>
      <input type="text" id="s-colibri-model-path" class="form-input" placeholder="/nvme/glm52_i4">
      <span class="form-hint">The GLM-5.2 int4 container <b>directory</b> (engine env <code>SNAP</code>). Keep it on fast local storage (NVMe/ext4), never on a network mount. Leave blank to serve the requested model's own configured directory.</span>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:160px">
        <label class="form-label">Context (ctx)</label>
        <input type="number" id="s-colibri-ctx" class="form-input" min="1024" placeholder="100000">
      </div>
      <div class="form-group" style="flex:1;min-width:120px">
        <label class="form-label">KV slots</label>
        <input type="number" id="s-colibri-kv-slots" class="form-input" min="1" max="16" placeholder="1">
        <span class="form-hint">1–16 concurrent cached conversations (continuous batching).</span>
      </div>
      <div class="form-group" style="flex:1;min-width:120px">
        <label class="form-label">Cap</label>
        <input type="number" id="s-colibri-cap" class="form-input" min="1" placeholder="8">
        <span class="form-hint">Engine worker cap (positional arg).</span>
      </div>
    </div>
    <div class="form-group">
      <label class="form-label">CUDA expert VRAM (GB)</label>
      <input type="text" id="s-colibri-cuda-expert-gb" class="form-input" placeholder="empty = auto; 'all' = pin all that fit">
      <span class="form-hint">Exports <code>CUDA_EXPERT_GB</code> — size of the resident expert tier colibri pins on the GPU. Blank leaves colibri's auto sizing.</span>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:180px">
        <label class="form-label">Build target</label>
        <select id="s-colibri-build-target" class="form-input">
          <option value="auto">auto (CUDA if nvcc present)</option>
          <option value="cuda">cuda</option>
          <option value="hip">hip (ROCm/AMD)</option>
          <option value="metal">metal (macOS)</option>
          <option value="cpu">cpu</option>
        </select>
      </div>
      <div class="form-group" style="flex:2;min-width:220px">
        <label class="form-label">Install dir</label>
        <input type="text" id="s-colibri-install-dir" class="form-input" placeholder="~/.coderai/colibri">
      </div>
    </div>
    <div class="form-group">
      <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-colibri-auto-build">
        <span>Auto clone + build the colibri engine binary if missing</span>
      </label>
    </div>
    <div class="form-group">
      <label class="form-label">Repo URL</label>
      <input type="text" id="s-colibri-repo-url" class="form-input" placeholder="https://github.com/JustVugg/colibri">
    </div>
    <div class="form-group">
      <label class="form-label">Extra engine env</label>
      <input type="text" id="s-colibri-extra-env" class="form-input" placeholder="COLI_MODEL_MIRROR=/nvme2/glm52_i4 DIRECT=1 PIPE=2">
      <span class="form-hint">Whitespace-separated <code>KEY=VALUE</code> pairs added to the engine's environment — colibri exposes its tuning only via env (<code>COLI_MODEL_MIRROR</code>, <code>COLI_DISK_WEIGHTS</code>, <code>COLI_NUMA</code>, <code>COLI_CUDA_PIPE</code>, <code>DIRECT</code>, <code>PIPE</code>, <code>PILOT</code>, <code>DRAFT</code>, …). Context window is set per-model via <b>n_ctx</b>.</span>
    </div>
    <p class="form-hint" style="margin-top:.6rem">colibri v1.5.0 also serves <b>DeepSeek-V4</b> and <b>Kimi-K3</b> (CPU engines) from the same install — add a model whose <code>backend</code> is <code>colibri</code> pointing at that family's container; the engine auto-detects the family from the container's <code>config.json</code>.</p>
  </div>
</div>

<!-- Kimi-K3 (kimi-k3-in-c) -->
<div class="card">
  <div class="card-title">Kimi-K3 (kimi-k3-in-c)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Run Kimi-K3 through FareedKhan-dev's portable-C <a href="https://github.com/FareedKhan-dev/kimi-k3-in-c" target="_blank" rel="noopener">kimi-k3-in-c</a> engine. coderai patches in a resident serve loop and drives it over the same mux protocol as colibri. <b>CPU-only</b> (AVX2+FMA) and needs ~1.7&nbsp;TB fast local storage (the ~1.56&nbsp;TB checkpoint + a ~109&nbsp;GB packed trunk from the repo's <code>scripts/pack-trunk.sh</code>). Nothing is downloaded automatically.</p>
  <div class="form-group">
    <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-k3-enabled" onchange="toggleK3Fields()">
      <span style="font-size:13px;font-weight:500">Enable k3 (Kimi-K3)</span>
    </label>
  </div>
  <div id="k3-fields" style="display:none">
    <div class="form-group">
      <label class="form-label">Model id / alias</label>
      <input type="text" id="s-k3-model-id" class="form-input" placeholder="kimi-k3">
      <span class="form-hint">When enabled, this id (and names containing <b>kimi-k3</b>, or any model whose config <code>backend</code> is <code>k3</code>) routes to the k3 engine. If colibri is also enabled, a bare Kimi name is ambiguous — set the model's <code>backend</code> to choose.</span>
    </div>
    <div class="form-group">
      <label class="form-label">Checkpoint path</label>
      <input type="text" id="s-k3-model-path" class="form-input" placeholder="/nvme/Kimi-K3">
      <span class="form-hint">The Kimi-K3 checkpoint <b>directory</b> (config.json + tokenizer + safetensors shards).</span>
    </div>
    <div class="form-group">
      <label class="form-label">Packed trunk dir</label>
      <input type="text" id="s-k3-trunk-dir" class="form-input" placeholder="/nvme/Kimi-K3-trunk">
      <span class="form-hint">The packed dense trunk (<code>scripts/pack-trunk.sh</code>). Blank = fully resident (needs ~114&nbsp;GB RAM).</span>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:150px">
        <label class="form-label">Preset</label>
        <select id="s-k3-preset" class="form-input">
          <option value="">(explicit budgets)</option>
          <option value="laptop">laptop</option>
          <option value="desktop">desktop</option>
          <option value="workstation">workstation</option>
          <option value="server">server</option>
          <option value="max">max</option>
        </select>
      </div>
      <div class="form-group" style="flex:1;min-width:120px">
        <label class="form-label">Trunk GB</label>
        <input type="number" id="s-k3-trunk-gb" class="form-input" min="0" step="1" placeholder="16">
      </div>
      <div class="form-group" style="flex:1;min-width:120px">
        <label class="form-label">Cache GB</label>
        <input type="number" id="s-k3-cache-gb" class="form-input" min="0" step="1" placeholder="64">
      </div>
      <div class="form-group" style="flex:1;min-width:120px">
        <label class="form-label">Context (K3_MAXT)</label>
        <input type="number" id="s-k3-ctx" class="form-input" min="32" placeholder="4096">
      </div>
    </div>
    <div class="form-group">
      <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-k3-auto-build">
        <span>Auto clone + patch + build the k3 engine binary if missing</span>
      </label>
    </div>
    <div class="form-group">
      <label class="form-label">Extra engine env</label>
      <input type="text" id="s-k3-extra-env" class="form-input" placeholder="K3_EXPERT_GB=8 K3_BITS=4 K3_DIRECT=1">
      <span class="form-hint">Whitespace-separated <code>KEY=VALUE</code> pairs (<code>K3_EXPERT_GB</code>, <code>K3_BITS</code>, <code>K3_DIRECT</code>, <code>K3_PIPE</code>, …).</span>
    </div>
  </div>
</div>

<!-- ktransformers (SGLang) -->
<div class="card">
  <div class="card-title">ktransformers (SGLang)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Run many large MoE families (DeepSeek, Kimi, Qwen, GLM, MiniMax) through <a href="https://github.com/kvcache-ai/ktransformers" target="_blank" rel="noopener">ktransformers</a>' CPU+GPU heterogeneous engine, served over SGLang's OpenAI API. coderai launches <code>python -m sglang.launch_server</code> as a managed subprocess and proxies to it. Requires <b>SGLang + kt-kernel</b> installed out of band (best on AMX/AVX-512 CPUs). Selected per model via an explicit <code>backend: kt</code> pin or the model id below — never by a broad name marker.</p>
  <div class="form-group">
    <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-kt-enabled" onchange="toggleKtFields()">
      <span style="font-size:13px;font-weight:500">Enable ktransformers</span>
    </label>
  </div>
  <div id="kt-fields" style="display:none">
    <div class="form-group">
      <label class="form-label">Model id / alias</label>
      <input type="text" id="s-kt-model-id" class="form-input" placeholder="ktransformers">
      <span class="form-hint">This id routes to ktransformers and is passed to SGLang as <code>--served-model-name</code>. Also matched by any model whose config <code>backend</code> is <code>kt</code>.</span>
    </div>
    <div class="form-group">
      <label class="form-label">Model path (HF dir)</label>
      <input type="text" id="s-kt-model-path" class="form-input" placeholder="/nvme/DeepSeek-V4-Flash">
    </div>
    <div class="form-group">
      <label class="form-label">KT weight path</label>
      <input type="text" id="s-kt-weight-path" class="form-input" placeholder="/nvme/DeepSeek-V4-Flash-kt">
      <span class="form-hint">The KT quantized weights directory (<code>--kt-weight-path</code>).</span>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:2;min-width:160px">
        <label class="form-label">Host</label>
        <input type="text" id="s-kt-host" class="form-input" placeholder="127.0.0.1">
      </div>
      <div class="form-group" style="flex:1;min-width:110px">
        <label class="form-label">Port</label>
        <input type="number" id="s-kt-port" class="form-input" min="0" placeholder="0 = auto">
      </div>
      <div class="form-group" style="flex:1;min-width:140px">
        <label class="form-label">Context length</label>
        <input type="number" id="s-kt-ctx" class="form-input" min="512" placeholder="32768">
      </div>
    </div>
    <div class="form-group">
      <label class="form-label">Extra SGLang args</label>
      <input type="text" id="s-kt-extra-args" class="form-input" placeholder="--tp-size 1 --mem-fraction-static 0.8">
    </div>
    <div class="form-group">
      <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-kt-auto-build">
        <span>Attempt <code>pip install sglang</code> if missing (heavy; kt-kernel still installed manually)</span>
      </label>
    </div>
    <div class="form-group">
      <label class="form-label">Extra subprocess env</label>
      <input type="text" id="s-kt-extra-env" class="form-input" placeholder="CUDA_VISIBLE_DEVICES=0">
    </div>
  </div>
</div>

<!-- vLLM (high-concurrency) -->
<div class="card">
  <div class="card-title">vLLM (high-concurrency)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Serve an LLM (or VLM) through <a href="https://github.com/vllm-project/vllm" target="_blank" rel="noopener">vLLM</a>'s OpenAI server with <b>continuous batching</b> — far higher aggregate throughput than serialized single-instance backends. coderai launches <code>vllm.entrypoints.openai.api_server</code> as a managed subprocess and proxies to it. vLLM pins its own torch/CUDA, so it runs in an <b>isolated venv</b> (CUDA-only). Selected per model via a <code>backend: vllm</code> pin or the model id below. Also serves Surya2 for OCR.</p>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-vllm-enabled" onchange="toggleVllmFields()">
      <span style="font-size:13px;font-weight:500">Enable vLLM</span>
    </label>
  </div>
  <div id="vllm-fields" style="display:none">
    <div class="form-group">
      <label class="form-label">Model id (alias + --served-model-name)</label>
      <input type="text" id="s-vllm-model-id" class="form-input" placeholder="vllm">
      <span class="form-hint">This id routes to vLLM. Also matched by any model whose config <code>backend</code> is <code>vllm</code>.</span>
    </div>
    <div class="form-group">
      <label class="form-label">Model path (HF dir or repo id)</label>
      <input type="text" id="s-vllm-model-path" class="form-input" placeholder="/nvme/Qwen3-8B or Qwen/Qwen3-8B">
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:130px"><label class="form-label">Max model len</label><input type="number" id="s-vllm-ctx" class="form-input" min="512" placeholder="32768"></div>
      <div class="form-group" style="flex:1;min-width:130px"><label class="form-label">GPU mem util</label><input type="number" id="s-vllm-gmu" class="form-input" min="0.1" max="1" step="0.05" placeholder="0.90"></div>
      <div class="form-group" style="flex:1;min-width:110px"><label class="form-label">TP size</label><input type="number" id="s-vllm-tp" class="form-input" min="1" placeholder="1"></div>
      <div class="form-group" style="flex:1;min-width:120px"><label class="form-label">Max num seqs</label><input type="number" id="s-vllm-mns" class="form-input" min="0" placeholder="0 = default"></div>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:120px"><label class="form-label">dtype</label><input type="text" id="s-vllm-dtype" class="form-input" placeholder="auto"></div>
      <div class="form-group" style="flex:1;min-width:120px"><label class="form-label">Quantization</label><input type="text" id="s-vllm-quant" class="form-input" placeholder="none (awq/gptq/fp8)"></div>
      <div class="form-group" style="flex:1;min-width:90px"><label class="form-label">Port</label><input type="number" id="s-vllm-port" class="form-input" min="0" placeholder="0 = auto"></div>
    </div>
    <div class="form-group">
      <label class="form-label">Isolated venv dir</label>
      <input type="text" id="s-vllm-venv" class="form-input" placeholder="(auto: /opt/coderai/vllm_venv, else /cache/vllm_venv)">
    </div>
    <div class="form-group">
      <label class="form-label">Extra args</label>
      <input type="text" id="s-vllm-extra-args" class="form-input" placeholder="--enable-prefix-caching --swap-space 4">
    </div>
    <div class="form-group">
      <label class="form-label">Extra subprocess env</label>
      <input type="text" id="s-vllm-extra-env" class="form-input" placeholder="CUDA_VISIBLE_DEVICES=0">
    </div>
    <div class="form-group">
      <label class="toggle-row" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
        <input type="checkbox" id="s-vllm-auto-build">
        <span>Auto-build the isolated venv (<code>pip install vllm</code>) on first use</span>
      </label>
    </div>
  </div>
</div>

<!-- OCR (dedicated document OCR engines) -->
<div class="card">
  <div class="card-title">OCR (document → text)</div>
  <p class="form-hint" style="margin-bottom:.6rem">Dedicated OCR engines (detection + recognition), <b>not</b> a vision LLM — faithful text with bounding boxes and layout, batched on GPU. Serve <a href="https://github.com/PaddlePaddle/PaddleOCR" target="_blank" rel="noopener">PaddleOCR</a>, <a href="https://github.com/mindee/doctr" target="_blank" rel="noopener">docTR</a> or <a href="https://github.com/VikParuchuri/surya" target="_blank" rel="noopener">Surya</a>, selectable per request. Endpoints: <code>POST /v1/ocr</code>, <code>POST /v1/ocr/batch</code>. Heavy deps install out of band (<code>pip install -r requirements-ocr.txt</code> or <code>./build.sh … --ocr</code>); a missing engine returns HTTP 503.</p>
  <div class="form-row">
    <label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
      <input type="checkbox" id="s-ocr-enabled" onchange="toggleOcrFields()">
      <span style="font-size:13px;font-weight:500">Enable OCR subsystem</span>
    </label>
  </div>
  <div id="ocr-fields" style="display:none">
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:150px">
        <label class="form-label">Default engine</label>
        <select id="s-ocr-default-engine" class="form-input">
          <option value="paddle">PaddleOCR</option>
          <option value="doctr">docTR</option>
          <option value="surya">Surya</option>
        </select>
      </div>
      <div class="form-group" style="flex:1;min-width:110px">
        <label class="form-label">PDF DPI</label>
        <input type="number" id="s-ocr-dpi" class="form-input" min="72" placeholder="200">
      </div>
      <div class="form-group" style="flex:1;min-width:110px">
        <label class="form-label">Max concurrency</label>
        <input type="number" id="s-ocr-max-concurrency" class="form-input" min="1" placeholder="4">
      </div>
      <div class="form-group" style="flex:1;min-width:90px">
        <label class="form-label">Language</label>
        <input type="text" id="s-ocr-lang" class="form-input" placeholder="it">
      </div>
    </div>

    <div class="card-title" style="font-size:13px;margin-top:.4rem">PaddleOCR</div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-paddle-enabled"><span>Enable</span></label>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-paddle-use-gpu"><span>Use GPU</span></label>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-paddle-structure"><span>PP-Structure (layout + tables)</span></label>
      <div class="form-group" style="flex:1;min-width:110px"><label class="form-label">Instances</label><input type="number" id="s-ocr-paddle-instances" class="form-input" min="1" placeholder="2"></div>
      <div class="form-group" style="flex:1;min-width:90px"><label class="form-label">Lang</label><input type="text" id="s-ocr-paddle-lang" class="form-input" placeholder="it"></div>
    </div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:180px"><label class="form-label">Det model dir <span class="muted">(opt)</span></label><input type="text" id="s-ocr-paddle-det-dir" class="form-input" placeholder="(default)"></div>
      <div class="form-group" style="flex:1;min-width:180px"><label class="form-label">Rec model dir <span class="muted">(opt)</span></label><input type="text" id="s-ocr-paddle-rec-dir" class="form-input" placeholder="(default)"></div>
    </div>
    <p class="form-hint" style="margin:0 0 .3rem">Runs in an <b>isolated venv</b> (opencv-contrib clash + bundled CUDA — a cu12x wheel runs on GPU even on newer host CUDA).</p>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <div class="form-group" style="flex:2;min-width:200px"><label class="form-label">Isolated venv dir</label><input type="text" id="s-ocr-paddle-venv" class="form-input" placeholder="/opt/coderai/paddle_venv"></div>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-paddle-auto-build"><span>Auto-build on first use</span></label>
    </div>
    <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap"><button type="button" class="btn btn-sm" onclick="ocrBuildVenv('paddle')">Build Paddle venv now</button><span id="s-ocr-paddle-build-status" class="form-hint"></span></div>

    <div class="card-title" style="font-size:13px;margin-top:.4rem">docTR</div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-doctr-enabled"><span>Enable</span></label>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-doctr-use-gpu"><span>Use GPU</span></label>
      <div class="form-group" style="flex:1;min-width:110px"><label class="form-label">Instances</label><input type="number" id="s-ocr-doctr-instances" class="form-input" min="1" placeholder="1"></div>
      <div class="form-group" style="flex:1;min-width:150px"><label class="form-label">Det arch</label><input type="text" id="s-ocr-doctr-det" class="form-input" placeholder="db_resnet50"></div>
      <div class="form-group" style="flex:1;min-width:150px"><label class="form-label">Reco arch</label><input type="text" id="s-ocr-doctr-reco" class="form-input" placeholder="crnn_vgg16_bn"></div>
    </div>

    <div class="card-title" style="font-size:13px;margin-top:.4rem">Surya</div>
    <p class="form-hint" style="margin:0 0 .4rem">GPL-licensed (compatible with coderai's GPLv3). Enable + accept to load it. Some releases add a revenue-capped commercial clause — check the version you install.</p>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-surya-enabled"><span>Enable</span></label>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-surya-license"><span>Accept license</span></label>
      <div class="form-group" style="flex:1;min-width:110px"><label class="form-label">Instances</label><input type="number" id="s-ocr-surya-instances" class="form-input" min="1" placeholder="1"></div>
      <div class="form-group" style="flex:1;min-width:120px"><label class="form-label">Langs</label><input type="text" id="s-ocr-surya-langs" class="form-input" placeholder="it"></div>
    </div>
    <p class="form-hint" style="margin:0 0 .3rem">Runs in an <b>isolated venv</b> (Surya caps pillow&lt;11, incompatible with the main venv).</p>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <div class="form-group" style="flex:2;min-width:200px"><label class="form-label">Isolated venv dir</label><input type="text" id="s-ocr-surya-venv" class="form-input" placeholder="/opt/coderai/surya_venv"></div>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-surya-auto-build"><span>Auto-build on first use</span></label>
    </div>
    <div style="display:flex;gap:.5rem;align-items:center;flex-wrap:wrap"><button type="button" class="btn btn-sm" onclick="ocrBuildVenv('surya')">Build Surya venv now</button><span id="s-ocr-surya-build-status" class="form-hint"></span></div>

    <div class="card-title" style="font-size:13px;margin-top:.4rem">Stamp / signature detection</div>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap">
      <div class="form-group" style="flex:1;min-width:150px">
        <label class="form-label">Mode</label>
        <select id="s-ocr-detect-mode" class="form-input">
          <option value="off">Off</option>
          <option value="layout">Layout regions (coarse, no extra model)</option>
          <option value="detector">YOLO detector (accurate)</option>
          <option value="both">Both</option>
        </select>
      </div>
      <div class="form-group" style="flex:2;min-width:200px"><label class="form-label">YOLO weights (.pt)</label><input type="text" id="s-ocr-detect-model" class="form-input" placeholder="/models/signature-stamp-yolo.pt"></div>
      <div class="form-group" style="flex:1;min-width:100px"><label class="form-label">Confidence</label><input type="number" id="s-ocr-detect-conf" class="form-input" min="0" max="1" step="0.05" placeholder="0.35"></div>
    </div>

    <div class="card-title" style="font-size:13px;margin-top:.4rem">Structured field extraction</div>
    <p class="form-hint" style="margin:0 0 .4rem">Extract JSON fields from the OCR text using an existing coderai text model. Choose the default schema below; override per request with the <code>schema</code> field.</p>
    <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-extract-enabled"><span>Enable extraction</span></label>
      <label style="display:flex;align-items:center;gap:.4rem;cursor:pointer"><input type="checkbox" id="s-ocr-extract-validate"><span>Validate (JSON Schema)</span></label>
      <div class="form-group" style="flex:1;min-width:110px"><label class="form-label">Max tokens</label><input type="number" id="s-ocr-extract-max-tokens" class="form-input" min="64" placeholder="2048"></div>
    </div>
    <div class="form-group">
      <label class="form-label">Extraction model id</label>
      <input type="text" id="s-ocr-extract-model" class="form-input" placeholder="a coderai text model id/alias">
    </div>
    <div class="form-group">
      <label class="form-label">Default schema</label>
      <div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center">
        <select id="s-ocr-extract-schema" class="form-input" style="flex:1;min-width:180px" onchange="ocrSchemaPicked()"></select>
        <button type="button" class="btn btn-sm" onclick="ocrLoadSchemas()">Refresh</button>
      </div>
      <span class="form-hint">The default schema for extraction. Pick a built-in or one of your saved schemas, or <b>auto</b> for generic key/value extraction.</span>
    </div>

    <!-- Schema editor: picker (above) + raw JSON + form builder, kept in sync -->
    <div class="card mb-0" style="margin-top:.4rem;background:rgba(127,127,127,.06)">
      <div class="card-title" style="font-size:13px">Schema editor</div>
      <p class="form-hint" style="margin:0 0 .5rem">Create or edit a <b>user</b> schema (built-ins are read-only; saving a built-in's name creates an override). The field builder and raw JSON stay in sync.</p>
      <div class="form-row" style="display:flex;gap:1rem;flex-wrap:wrap;align-items:center">
        <div class="form-group" style="flex:2;min-width:160px"><label class="form-label">Name</label><input type="text" id="s-ocr-sch-name" class="form-input" placeholder="my_schema"></div>
        <div class="form-group" style="flex:1;min-width:130px"><label class="form-label">Type</label>
          <select id="s-ocr-sch-type" class="form-input"><option value="template">template</option><option value="jsonschema">jsonschema</option></select>
        </div>
        <div class="form-group" style="flex:1;min-width:90px"><label class="form-label">Lang</label><input type="text" id="s-ocr-sch-lang" class="form-input" placeholder=""></div>
      </div>
      <div class="form-group"><label class="form-label">Description</label><input type="text" id="s-ocr-sch-desc" class="form-input" placeholder=""></div>
      <div class="form-group"><label class="form-label">Prompt hint <span class="muted">(opt)</span></label><input type="text" id="s-ocr-sch-hint" class="form-input" placeholder=""></div>

      <label class="form-label" style="margin-top:.3rem">Fields <span class="muted">(builder — template type)</span></label>
      <div id="s-ocr-sch-builder"></div>
      <div style="margin:.35rem 0 .6rem"><button type="button" class="btn btn-sm" onclick="ocrBuilderAddRow()">+ Add field</button>
        <button type="button" class="btn btn-sm" onclick="ocrBuilderToJson()">Builder → JSON ↓</button>
        <button type="button" class="btn btn-sm" onclick="ocrJsonToBuilder()">JSON → Builder ↑</button></div>

      <div class="form-group">
        <label class="form-label">Raw schema JSON</label>
        <textarea id="s-ocr-sch-json" class="form-input" rows="8" spellcheck="false" style="font-family:monospace;font-size:12px" placeholder='{"numero": "string", "data": "YYYY-MM-DD"}'></textarea>
        <span class="form-hint">The value of the schema's <code>schema</code> field (a fill-in template or a JSON Schema object).</span>
      </div>
      <div style="display:flex;gap:.5rem;flex-wrap:wrap">
        <button type="button" class="btn btn-sm" onclick="ocrSchemaLoadIntoEditor()">Load selected</button>
        <button type="button" class="btn btn-sm btn-primary" onclick="ocrSchemaSave()">Save schema</button>
        <button type="button" class="btn btn-sm btn-danger" onclick="ocrSchemaDelete()">Delete (user only)</button>
        <span id="s-ocr-sch-status" class="form-hint" style="align-self:center"></span>
      </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>

<!-- Credits & acknowledgements -->
<div class="card">
  <div class="card-title">Credits &amp; acknowledgements</div>
  <p class="form-hint" style="margin-bottom:.7rem">CoderAI stands on the shoulders of remarkable open-source work. Heartfelt thanks to the developers whose engines and research make these capabilities possible.</p>

  <div style="display:flex;flex-direction:column;gap:.6rem">
    <div style="border-left:3px solid var(--accent,#5b8cff);padding:.15rem 0 .15rem .7rem">
      <div style="font-weight:600">colibri <span class="muted" style="font-weight:400">— GLM-5.2 on consumer hardware</span></div>
      <div class="form-hint">A brilliant pure-C MoE engine by <b>JustVugg</b> that streams a 744B-parameter model across VRAM/RAM/disk to run it on a single GPU. <a href="https://github.com/JustVugg/colibri" target="_blank" rel="noopener">github.com/JustVugg/colibri</a></div>
    </div>
    <div style="border-left:3px solid var(--accent,#5b8cff);padding:.15rem 0 .15rem .7rem">
      <div style="font-weight:600">ds4 / DwarfStar <span class="muted" style="font-weight:400">— DeepSeek-V4 native engine</span></div>
      <div class="form-hint">A superb from-scratch DeepSeek-V4 inference engine by <b>Salvatore Sanfilippo (antirez)</b>. <a href="https://github.com/antirez/ds4" target="_blank" rel="noopener">github.com/antirez/ds4</a></div>
    </div>
    <div style="border-left:3px solid var(--border,#444);padding:.15rem 0 .15rem .7rem">
      <div style="font-weight:600">llama.cpp &amp; whisper.cpp <span class="muted" style="font-weight:400">— GGUF LLM inference &amp; Whisper STT</span></div>
      <div class="form-hint">The foundational C/C++ inference engines by <b>Georgi Gerganov</b> and contributors. <a href="https://github.com/ggml-org/llama.cpp" target="_blank" rel="noopener">llama.cpp</a> · <a href="https://github.com/ggml-org/whisper.cpp" target="_blank" rel="noopener">whisper.cpp</a></div>
    </div>
    <div style="border-left:3px solid var(--border,#444);padding:.15rem 0 .15rem .7rem">
      <div style="font-weight:600">Visual place recognition &amp; geolocation research</div>
      <div class="form-hint"><b>EigenPlaces</b> (Gabriele Berton et&nbsp;al.) · <b>DINOv2-SALAD</b> (Sergio Izquierdo, Javier Civera) · <b>GeoCLIP</b> (Vicente Vivanco et&nbsp;al.) · <b>DINOv2</b> (Meta AI) — the models that power visual matching &amp; geolocation.</div>
    </div>
  </div>
  <p class="form-hint" style="margin-top:.7rem;font-size:11px">…and the wider open-source ecosystem (PyTorch, Hugging Face Transformers/Diffusers, and many more) that CoderAI builds on.</p>
</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, dpmOv, rateOv){
  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 = '';
  dpmOv = dpmOv || {}; rateOv = rateOv || {};
  const DPM = ['','auto','low','high','profile_standard','profile_peak'];
  const dpmSel = (n) => `<select class="form-input conc-dpm" data-engine="${n}">` +
    DPM.map(l => `<option value="${l}"${(dpmOv[n]||'')===l?' selected':''}>${l||'default'}</option>`).join('') +
    `</select>`;
  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>
     <span class="muted" style="font-size:11px">GPU power lock</span>
     <span class="muted" style="font-size:11px">Rate limit (ms)</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] ?? ''}">
      ${dpmSel(n)}
      <input type="number" class="form-input conc-rate" data-engine="${n}" min="0" max="60000" placeholder="0" value="${rateOv[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 _collectDpmOverrides(){
  const o = {};
  document.querySelectorAll('#concurrency-override-rows .conc-dpm').forEach(sel => {
    const v = (sel.value || '').trim();
    if(v) o[sel.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';
}
function toggleColibriFields(){
  document.getElementById('colibri-fields').style.display =
    document.getElementById('s-colibri-enabled').checked ? 'block' : 'none';
}

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

function toggleKtFields(){
  document.getElementById('kt-fields').style.display =
    document.getElementById('s-kt-enabled').checked ? 'block' : 'none';
}
function toggleOcrFields(){
  document.getElementById('ocr-fields').style.display =
    document.getElementById('s-ocr-enabled').checked ? 'block' : 'none';
}
function toggleVllmFields(){
  document.getElementById('vllm-fields').style.display =
    document.getElementById('s-vllm-enabled').checked ? 'block' : 'none';
}

// ---- OCR isolated-venv build (paddle/surya) ----
const _ocrBuildPollers = {};
async function ocrBuildVenv(engine){
  const statusEl = document.getElementById('s-ocr-'+engine+'-build-status');
  statusEl.textContent = 'starting build…';
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/ocr/build-venv', {
      method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({engine})});
    if(!r.ok){ const e=await r.json(); throw new Error(e.detail||'failed'); }
    ocrPollBuild(engine);
  }catch(e){ statusEl.textContent = 'error: '+e.message; }
}
function ocrPollBuild(engine){
  if(_ocrBuildPollers[engine]) clearInterval(_ocrBuildPollers[engine]);
  const statusEl = document.getElementById('s-ocr-'+engine+'-build-status');
  _ocrBuildPollers[engine] = setInterval(async () => {
    try{
      const r = await fetch(ROOT_PATH + '/admin/api/ocr/build-venv/status?engine='+engine);
      const d = await r.json();
      const tail = (d.log||[]).slice(-1)[0] || '';
      statusEl.textContent = d.state + (tail ? ' — '+tail : '');
      if(d.state === 'done' || d.state === 'error'){
        clearInterval(_ocrBuildPollers[engine]); _ocrBuildPollers[engine] = null;
      }
    }catch(e){ /* keep polling */ }
  }, 3000);
}

// ---- OCR extraction schema management (picker + raw JSON + field builder) ----
let _ocrDefaultSchema = '';
function _ocrSchStatus(msg, isErr){
  const el = document.getElementById('s-ocr-sch-status');
  el.textContent = msg || '';
  el.style.color = isErr ? '#c0392b' : '';
}
async function ocrLoadSchemas(){
  const sel = document.getElementById('s-ocr-extract-schema');
  if(!sel) return;
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/ocr/schemas');
    const d = await r.json();
    const list = (d.schemas || []);
    sel.innerHTML = '<option value="">auto (generic key/value)</option>' +
      list.map(s => `<option value="${s.name}">${s.name} — ${s.description||s.type} [${s.source}]</option>`).join('');
    sel.value = _ocrDefaultSchema || '';
  }catch(e){ _ocrSchStatus('failed to list schemas: '+e.message, true); }
}
function ocrSchemaPicked(){ /* selection is read on save; nothing else needed */ }

function ocrBuilderRow(key, val){
  const wrap = document.createElement('div');
  wrap.className = 'ocr-sch-row';
  wrap.style = 'display:flex;gap:.4rem;margin-bottom:.3rem';
  wrap.innerHTML =
    `<input type="text" class="form-input ocr-sch-key" placeholder="field name" style="flex:1" value="${(key||'').replace(/"/g,'&quot;')}">`+
    `<input type="text" class="form-input ocr-sch-val" placeholder="type/hint e.g. string|null" style="flex:1" value="${(val||'').replace(/"/g,'&quot;')}">`+
    `<button type="button" class="btn btn-sm btn-danger" onclick="this.parentNode.remove()">×</button>`;
  document.getElementById('s-ocr-sch-builder').appendChild(wrap);
}
function ocrBuilderAddRow(){ ocrBuilderRow('', ''); }
function ocrBuilderClear(){ document.getElementById('s-ocr-sch-builder').innerHTML = ''; }
function ocrBuilderToJson(){
  const rows = document.querySelectorAll('#s-ocr-sch-builder .ocr-sch-row');
  const obj = {};
  rows.forEach(r => {
    const k = r.querySelector('.ocr-sch-key').value.trim();
    const v = r.querySelector('.ocr-sch-val').value.trim();
    if(k) obj[k] = v || 'string';
  });
  document.getElementById('s-ocr-sch-json').value = JSON.stringify(obj, null, 2);
}
function ocrJsonToBuilder(){
  let obj;
  try{ obj = JSON.parse(document.getElementById('s-ocr-sch-json').value || '{}'); }
  catch(e){ _ocrSchStatus('raw JSON is invalid: '+e.message, true); return; }
  ocrBuilderClear();
  if(obj && typeof obj === 'object' && !Array.isArray(obj)){
    Object.keys(obj).forEach(k => {
      const v = obj[k];
      ocrBuilderRow(k, (typeof v === 'string') ? v : JSON.stringify(v));
    });
  }
  _ocrSchStatus('builder rebuilt from JSON (nested values shown as JSON text)');
}
async function ocrSchemaLoadIntoEditor(){
  const name = document.getElementById('s-ocr-extract-schema').value;
  if(!name){ _ocrSchStatus('pick a named schema first (auto has no body)', true); return; }
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/ocr/schemas/' + encodeURIComponent(name));
    if(!r.ok){ const e=await r.json(); throw new Error(e.detail||'not found'); }
    const s = await r.json();
    document.getElementById('s-ocr-sch-name').value = s.name || name;
    document.getElementById('s-ocr-sch-type').value = s.type || 'template';
    document.getElementById('s-ocr-sch-lang').value = s.language || '';
    document.getElementById('s-ocr-sch-desc').value = s.description || '';
    document.getElementById('s-ocr-sch-hint').value = s.prompt_hint || '';
    document.getElementById('s-ocr-sch-json').value = JSON.stringify(s.schema || {}, null, 2);
    ocrJsonToBuilder();
    _ocrSchStatus('loaded "'+(s.name||name)+'" ('+(s.source||'')+')');
  }catch(e){ _ocrSchStatus('load failed: '+e.message, true); }
}
async function ocrSchemaSave(){
  const name = document.getElementById('s-ocr-sch-name').value.trim();
  if(!name){ _ocrSchStatus('name is required', true); return; }
  let schema;
  try{ schema = JSON.parse(document.getElementById('s-ocr-sch-json').value || '{}'); }
  catch(e){ _ocrSchStatus('raw JSON invalid: '+e.message, true); return; }
  const payload = {
    name,
    type: document.getElementById('s-ocr-sch-type').value || 'template',
    language: document.getElementById('s-ocr-sch-lang').value.trim(),
    description: document.getElementById('s-ocr-sch-desc').value.trim(),
    prompt_hint: document.getElementById('s-ocr-sch-hint').value.trim(),
    schema,
  };
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/ocr/schemas', {
      method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)});
    if(!r.ok){ const e=await r.json(); throw new Error(e.detail||'save failed'); }
    _ocrSchStatus('saved "'+name+'"');
    await ocrLoadSchemas();
    document.getElementById('s-ocr-extract-schema').value = name;
  }catch(e){ _ocrSchStatus('save failed: '+e.message, true); }
}
async function ocrSchemaDelete(){
  const name = document.getElementById('s-ocr-sch-name').value.trim() ||
               document.getElementById('s-ocr-extract-schema').value;
  if(!name){ _ocrSchStatus('nothing to delete', true); return; }
  if(!confirm('Delete user schema "'+name+'"? (built-ins cannot be deleted)')) return;
  try{
    const r = await fetch(ROOT_PATH + '/admin/api/ocr/schemas/' + encodeURIComponent(name), {method:'DELETE'});
    if(!r.ok){ const e=await r.json(); throw new Error(e.detail||'delete failed'); }
    _ocrSchStatus('deleted "'+name+'"');
    await ocrLoadSchemas();
  }catch(e){ _ocrSchStatus('delete failed: '+e.message, true); }
}

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;
  }
}

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

async function colibriDownloadDefault(){
  const sel = document.getElementById('s-colibri-dl-select');
  const btn = document.getElementById('s-colibri-dl-btn');
  const statusEl = document.getElementById('s-colibri-dl-status');
  const repo = sel.value;
  const meta = _colibriDefaultModels.find(m => m.repo === repo);
  if(!repo || !meta){ return; }
  if(meta.present && !confirm('This container is already downloaded. Download again?')) return;
  const modelId = (document.getElementById('s-colibri-model-id').value.trim() || _colibriModelId);
  btn.disabled = true;
  statusEl.style.display = 'block';
  statusEl.textContent = 'Starting download (full repo — this is large)…';
  try{
    // Full-repo snapshot (no file_pattern) → a directory, exactly what colibri loads.
    const r = await fetch(ROOT_PATH + '/admin/api/model-download', {
      method:'POST', headers:{'Content-Type':'application/json'},
      body: JSON.stringify({model_id: repo}),
    });
    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 = async (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 GLM-5.2 container: ${pct}%${rate}`;
      }else if(m.type === 'info'){
        statusEl.textContent = m.message || statusEl.textContent;
      }else if(m.type === 'done'){
        // Register it as a colibri-backed model, addressable by the model id.
        try{
          await fetch(ROOT_PATH + '/admin/api/model-add', {
            method:'POST', headers:{'Content-Type':'application/json'},
            body: JSON.stringify({model_id: repo, model_type:'text_models',
                                  backend:'colibri', alias: modelId}),
          });
          statusEl.textContent = '✅ Downloaded and registered as a colibri model — enable colibri above and it serves as "' + modelId + '".';
        }catch(_){
          statusEl.textContent = '✅ Downloaded. Add it as a model with backend "colibri" to serve it.';
        }
        es.close(); btn.disabled = false; loadColibriDefaultModels();
      }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);
}

function renderCardCaps(cards, caps){
  const box = document.getElementById('s-split-card-caps');
  if(!box) return;
  if(!cards || !cards.length){
    box.innerHTML = '<span class="form-hint">No GPUs detected on this machine.</span>';
    return;
  }
  box.innerHTML = '';
  cards.forEach(c => {
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;align-items:center;gap:8px;margin:4px 0';
    const lbl = document.createElement('span');
    lbl.style.cssText = 'flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
    lbl.textContent = c.name + ' (' + (c.vendor||'') + ')';
    lbl.title = c.key;
    const inp = document.createElement('input');
    inp.type = 'number'; inp.step = '0.5'; inp.min = '0';
    inp.className = 'form-input s-card-cap'; inp.placeholder = 'no cap';
    inp.style.cssText = 'width:120px;flex:none';
    inp.dataset.cardKey = c.key;
    const v = (caps && caps[c.key] != null) ? caps[c.key] : '';
    inp.value = v;
    const unit = document.createElement('span'); unit.className='muted'; unit.textContent='GB';
    row.appendChild(lbl); row.appendChild(inp); row.appendChild(unit);
    box.appendChild(row);
  });
}

function collectCardCaps(){
  const out = {};
  document.querySelectorAll('#s-split-card-caps .s-card-cap').forEach(inp => {
    const v = (inp.value||'').trim();
    if(v !== ''){ const n = parseFloat(v); if(!isNaN(n) && n > 0) out[inp.dataset.cardKey] = n; }
  });
  return out;
}

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 || {},
      d.server?.dpm_force_performance_level_overrides || {},
      d.server?.engine_request_min_interval_ms || {});
    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-gpu-split').checked = !!d.offload?.gpu_split;
    document.getElementById('s-split-strategy').value = d.offload?.split_strategy ?? 'vram';
    renderCardCaps(d.offload?.gpu_cards || [], d.offload?.split_card_caps_gb || {});
    document.getElementById('s-tensor-split').value = d.offload?.tensor_split ?? '';
    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();
    // GLM-5.2 (colibri)
    const colibri = d.colibri || {};
    document.getElementById('s-colibri-enabled').checked = !!colibri.enabled;
    document.getElementById('s-colibri-model-id').value = colibri.model_id ?? 'glm-5.2-colibri';
    document.getElementById('s-colibri-model-path').value = colibri.model_path ?? '';
    document.getElementById('s-colibri-ctx').value = colibri.ctx ?? 100000;
    document.getElementById('s-colibri-kv-slots').value = colibri.kv_slots ?? 1;
    document.getElementById('s-colibri-cap').value = colibri.cap ?? 8;
    document.getElementById('s-colibri-cuda-expert-gb').value = colibri.cuda_expert_gb ?? '';
    document.getElementById('s-colibri-build-target').value = colibri.build_target ?? 'auto';
    document.getElementById('s-colibri-install-dir').value = colibri.install_dir ?? '';
    document.getElementById('s-colibri-auto-build').checked = colibri.auto_build !== false;
    document.getElementById('s-colibri-repo-url').value = colibri.repo_url ?? 'https://github.com/JustVugg/colibri';
    document.getElementById('s-colibri-extra-env').value = colibri.extra_env ?? '';
    toggleColibriFields();
    loadColibriDefaultModels();
    // Kimi-K3 (kimi-k3-in-c)
    const k3 = d.k3 || {};
    document.getElementById('s-k3-enabled').checked = !!k3.enabled;
    document.getElementById('s-k3-model-id').value = k3.model_id ?? 'kimi-k3';
    document.getElementById('s-k3-model-path').value = k3.model_path ?? '';
    document.getElementById('s-k3-trunk-dir').value = k3.trunk_dir ?? '';
    document.getElementById('s-k3-preset').value = k3.preset ?? '';
    document.getElementById('s-k3-trunk-gb').value = k3.trunk_gb ?? 16;
    document.getElementById('s-k3-cache-gb').value = k3.cache_gb ?? 64;
    document.getElementById('s-k3-ctx').value = k3.ctx ?? 4096;
    document.getElementById('s-k3-auto-build').checked = k3.auto_build !== false;
    document.getElementById('s-k3-extra-env').value = k3.extra_env ?? '';
    toggleK3Fields();
    // ktransformers (SGLang)
    const kt = d.ktransformers || {};
    document.getElementById('s-kt-enabled').checked = !!kt.enabled;
    document.getElementById('s-kt-model-id').value = kt.model_id ?? 'ktransformers';
    document.getElementById('s-kt-model-path').value = kt.model_path ?? '';
    document.getElementById('s-kt-weight-path').value = kt.kt_weight_path ?? '';
    document.getElementById('s-kt-host').value = kt.host ?? '127.0.0.1';
    document.getElementById('s-kt-port').value = kt.port ?? 0;
    document.getElementById('s-kt-ctx').value = kt.ctx ?? 32768;
    document.getElementById('s-kt-extra-args').value = kt.extra_args ?? '';
    document.getElementById('s-kt-auto-build').checked = !!kt.auto_build;
    document.getElementById('s-kt-extra-env').value = kt.extra_env ?? '';
    toggleKtFields();

    // vLLM
    const vll = d.vllm || {};
    document.getElementById('s-vllm-enabled').checked = !!vll.enabled;
    document.getElementById('s-vllm-model-id').value = vll.model_id ?? 'vllm';
    document.getElementById('s-vllm-model-path').value = vll.model_path ?? '';
    document.getElementById('s-vllm-ctx').value = vll.ctx ?? 32768;
    document.getElementById('s-vllm-gmu').value = vll.gpu_memory_utilization ?? 0.90;
    document.getElementById('s-vllm-tp').value = vll.tensor_parallel_size ?? 1;
    document.getElementById('s-vllm-mns').value = vll.max_num_seqs ?? 0;
    document.getElementById('s-vllm-dtype').value = vll.dtype ?? '';
    document.getElementById('s-vllm-quant').value = vll.quantization ?? '';
    document.getElementById('s-vllm-port').value = vll.port ?? 0;
    document.getElementById('s-vllm-venv').value = vll.venv ?? '';
    document.getElementById('s-vllm-extra-args').value = vll.extra_args ?? '';
    document.getElementById('s-vllm-extra-env').value = vll.extra_env ?? '';
    document.getElementById('s-vllm-auto-build').checked = !!vll.auto_build;
    toggleVllmFields();

    // OCR subsystem
    const ocr = d.ocr || {};
    document.getElementById('s-ocr-enabled').checked = !!ocr.enabled;
    document.getElementById('s-ocr-default-engine').value = ocr.default_engine ?? 'paddle';
    document.getElementById('s-ocr-dpi').value = ocr.dpi ?? 200;
    document.getElementById('s-ocr-max-concurrency').value = ocr.max_concurrency ?? 4;
    document.getElementById('s-ocr-lang').value = ocr.lang ?? 'it';
    document.getElementById('s-ocr-paddle-enabled').checked = ocr.paddle_enabled !== false;
    document.getElementById('s-ocr-paddle-use-gpu').checked = ocr.paddle_use_gpu !== false;
    document.getElementById('s-ocr-paddle-structure').checked = ocr.paddle_structure !== false;
    document.getElementById('s-ocr-paddle-instances').value = ocr.paddle_instances ?? 2;
    document.getElementById('s-ocr-paddle-lang').value = ocr.paddle_lang ?? 'it';
    document.getElementById('s-ocr-paddle-det-dir').value = ocr.paddle_det_model_dir ?? '';
    document.getElementById('s-ocr-paddle-rec-dir').value = ocr.paddle_rec_model_dir ?? '';
    document.getElementById('s-ocr-paddle-venv').value = ocr.paddle_venv ?? '';
    document.getElementById('s-ocr-paddle-auto-build').checked = !!ocr.paddle_auto_build;
    document.getElementById('s-ocr-doctr-enabled').checked = !!ocr.doctr_enabled;
    document.getElementById('s-ocr-doctr-use-gpu').checked = ocr.doctr_use_gpu !== false;
    document.getElementById('s-ocr-doctr-instances').value = ocr.doctr_instances ?? 1;
    document.getElementById('s-ocr-doctr-det').value = ocr.doctr_det_arch ?? 'db_resnet50';
    document.getElementById('s-ocr-doctr-reco').value = ocr.doctr_reco_arch ?? 'crnn_vgg16_bn';
    document.getElementById('s-ocr-surya-enabled').checked = !!ocr.surya_enabled;
    document.getElementById('s-ocr-surya-license').checked = !!ocr.surya_accept_license;
    document.getElementById('s-ocr-surya-instances').value = ocr.surya_instances ?? 1;
    document.getElementById('s-ocr-surya-langs').value = ocr.surya_langs ?? 'it';
    document.getElementById('s-ocr-surya-venv').value = ocr.surya_venv ?? '';
    document.getElementById('s-ocr-surya-auto-build').checked = !!ocr.surya_auto_build;
    document.getElementById('s-ocr-detect-mode').value = ocr.detect_mode ?? 'off';
    document.getElementById('s-ocr-detect-model').value = ocr.detect_model_path ?? '';
    document.getElementById('s-ocr-detect-conf').value = ocr.detect_conf ?? 0.35;
    document.getElementById('s-ocr-extract-enabled').checked = !!ocr.extract_enabled;
    document.getElementById('s-ocr-extract-validate').checked = ocr.extract_validate !== false;
    document.getElementById('s-ocr-extract-max-tokens').value = ocr.extract_max_tokens ?? 2048;
    document.getElementById('s-ocr-extract-model').value = ocr.extract_model_id ?? '';
    _ocrDefaultSchema = ocr.extract_schema ?? '';
    toggleOcrFields();
    ocrLoadSchemas();
  }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'),
      dpm_force_performance_level_overrides: _collectDpmOverrides(),
      engine_request_min_interval_ms: _collectConcOverrides('conc-rate'),
      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,
      gpu_split: document.getElementById('s-gpu-split').checked,
      split_strategy: document.getElementById('s-split-strategy').value,
      split_card_caps_gb: collectCardCaps(),
      tensor_split: (document.getElementById('s-tensor-split').value.trim() || null),
      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,
    },
    colibri:{
      enabled: document.getElementById('s-colibri-enabled').checked,
      model_id: document.getElementById('s-colibri-model-id').value.trim() || 'glm-5.2-colibri',
      model_path: document.getElementById('s-colibri-model-path').value.trim(),
      ctx: parseInt(document.getElementById('s-colibri-ctx').value) || 100000,
      kv_slots: parseInt(document.getElementById('s-colibri-kv-slots').value) || 1,
      cap: parseInt(document.getElementById('s-colibri-cap').value) || 8,
      cuda_expert_gb: document.getElementById('s-colibri-cuda-expert-gb').value.trim(),
      build_target: document.getElementById('s-colibri-build-target').value,
      install_dir: document.getElementById('s-colibri-install-dir').value.trim(),
      auto_build: document.getElementById('s-colibri-auto-build').checked,
      repo_url: document.getElementById('s-colibri-repo-url').value.trim() || 'https://github.com/JustVugg/colibri',
      extra_env: document.getElementById('s-colibri-extra-env').value.trim(),
    },
    k3:{
      enabled: document.getElementById('s-k3-enabled').checked,
      model_id: document.getElementById('s-k3-model-id').value.trim() || 'kimi-k3',
      model_path: document.getElementById('s-k3-model-path').value.trim(),
      trunk_dir: document.getElementById('s-k3-trunk-dir').value.trim(),
      preset: document.getElementById('s-k3-preset').value,
      trunk_gb: parseFloat(document.getElementById('s-k3-trunk-gb').value) || 16,
      cache_gb: parseFloat(document.getElementById('s-k3-cache-gb').value) || 64,
      ctx: parseInt(document.getElementById('s-k3-ctx').value) || 4096,
      auto_build: document.getElementById('s-k3-auto-build').checked,
      extra_env: document.getElementById('s-k3-extra-env').value.trim(),
    },
    ktransformers:{
      enabled: document.getElementById('s-kt-enabled').checked,
      model_id: document.getElementById('s-kt-model-id').value.trim() || 'ktransformers',
      model_path: document.getElementById('s-kt-model-path').value.trim(),
      kt_weight_path: document.getElementById('s-kt-weight-path').value.trim(),
      host: document.getElementById('s-kt-host').value.trim() || '127.0.0.1',
      port: parseInt(document.getElementById('s-kt-port').value) || 0,
      ctx: parseInt(document.getElementById('s-kt-ctx').value) || 32768,
      extra_args: document.getElementById('s-kt-extra-args').value.trim(),
      auto_build: document.getElementById('s-kt-auto-build').checked,
      extra_env: document.getElementById('s-kt-extra-env').value.trim(),
    },
    vllm:{
      enabled: document.getElementById('s-vllm-enabled').checked,
      model_id: document.getElementById('s-vllm-model-id').value.trim() || 'vllm',
      model_path: document.getElementById('s-vllm-model-path').value.trim(),
      ctx: parseInt(document.getElementById('s-vllm-ctx').value) || 32768,
      gpu_memory_utilization: parseFloat(document.getElementById('s-vllm-gmu').value) || 0.90,
      tensor_parallel_size: parseInt(document.getElementById('s-vllm-tp').value) || 1,
      max_num_seqs: parseInt(document.getElementById('s-vllm-mns').value) || 0,
      dtype: document.getElementById('s-vllm-dtype').value.trim(),
      quantization: document.getElementById('s-vllm-quant').value.trim(),
      port: parseInt(document.getElementById('s-vllm-port').value) || 0,
      venv: document.getElementById('s-vllm-venv').value.trim(),
      extra_args: document.getElementById('s-vllm-extra-args').value.trim(),
      extra_env: document.getElementById('s-vllm-extra-env').value.trim(),
      auto_build: document.getElementById('s-vllm-auto-build').checked,
    },
    ocr:{
      enabled: document.getElementById('s-ocr-enabled').checked,
      default_engine: document.getElementById('s-ocr-default-engine').value || 'paddle',
      dpi: parseInt(document.getElementById('s-ocr-dpi').value) || 200,
      max_concurrency: parseInt(document.getElementById('s-ocr-max-concurrency').value) || 4,
      lang: document.getElementById('s-ocr-lang').value.trim() || 'it',
      paddle_enabled: document.getElementById('s-ocr-paddle-enabled').checked,
      paddle_use_gpu: document.getElementById('s-ocr-paddle-use-gpu').checked,
      paddle_structure: document.getElementById('s-ocr-paddle-structure').checked,
      paddle_instances: parseInt(document.getElementById('s-ocr-paddle-instances').value) || 2,
      paddle_lang: document.getElementById('s-ocr-paddle-lang').value.trim() || 'it',
      paddle_det_model_dir: document.getElementById('s-ocr-paddle-det-dir').value.trim(),
      paddle_rec_model_dir: document.getElementById('s-ocr-paddle-rec-dir').value.trim(),
      paddle_venv: document.getElementById('s-ocr-paddle-venv').value.trim(),
      paddle_auto_build: document.getElementById('s-ocr-paddle-auto-build').checked,
      doctr_enabled: document.getElementById('s-ocr-doctr-enabled').checked,
      doctr_use_gpu: document.getElementById('s-ocr-doctr-use-gpu').checked,
      doctr_instances: parseInt(document.getElementById('s-ocr-doctr-instances').value) || 1,
      doctr_det_arch: document.getElementById('s-ocr-doctr-det').value.trim() || 'db_resnet50',
      doctr_reco_arch: document.getElementById('s-ocr-doctr-reco').value.trim() || 'crnn_vgg16_bn',
      surya_enabled: document.getElementById('s-ocr-surya-enabled').checked,
      surya_accept_license: document.getElementById('s-ocr-surya-license').checked,
      surya_instances: parseInt(document.getElementById('s-ocr-surya-instances').value) || 1,
      surya_langs: document.getElementById('s-ocr-surya-langs').value.trim() || 'it',
      surya_venv: document.getElementById('s-ocr-surya-venv').value.trim(),
      surya_auto_build: document.getElementById('s-ocr-surya-auto-build').checked,
      detect_mode: document.getElementById('s-ocr-detect-mode').value || 'off',
      detect_model_path: document.getElementById('s-ocr-detect-model').value.trim(),
      detect_conf: parseFloat(document.getElementById('s-ocr-detect-conf').value) || 0.35,
      extract_enabled: document.getElementById('s-ocr-extract-enabled').checked,
      extract_validate: document.getElementById('s-ocr-extract-validate').checked,
      extract_max_tokens: parseInt(document.getElementById('s-ocr-extract-max-tokens').value) || 2048,
      extract_model_id: document.getElementById('s-ocr-extract-model').value.trim(),
      extract_schema: document.getElementById('s-ocr-extract-schema').value || '',
    },
    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 %}
