<!DOCTYPE html>
<!-- Revision: 2 -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>GNSS Guard Provisioning</title>
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
  <style>
    :root {
      --bg-primary: #0a0e14;
      --bg-secondary: #11151c;
      --bg-tertiary: #1a1f2b;
      --bg-card: #151a24;
      --accent: #00d4aa;
      --accent-dim: #00b894;
      --accent-glow: rgba(0, 212, 170, 0.12);
      --text: #e6e8eb;
      --text-dim: #8b949e;
      --text-muted: #5c6370;
      --border: #2d333b;
      --danger: #f87171;
      --danger-bg: rgba(248, 113, 113, 0.1);
      --warn: #fbbf24;
      --warn-bg: rgba(251, 191, 36, 0.1);
      --success: #00d4aa;
      --success-bg: rgba(0, 212, 170, 0.1);
      --radius: 12px;
      --radius-sm: 8px;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg-primary);
      color: var(--text);
      min-height: 100vh;
      line-height: 1.5;
      font-size: 15px;
    }

    /* ----- Layout ----- */
    .wrap {
      max-width: 900px;
      margin: 0 auto;
      padding: 1.5rem;
    }
    .header {
      margin-bottom: 1.75rem;
      padding-bottom: 1rem;
      border-bottom: 1px solid var(--border);
    }
    .header h1 {
      font-size: 1.5rem;
      font-weight: 700;
      color: var(--text);
      letter-spacing: -0.02em;
    }
    .header p {
      margin-top: 0.25rem;
      font-size: 0.9rem;
      color: var(--text-dim);
    }

    /* ----- Section blocks ----- */
    .section {
      background: var(--bg-card);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 1.25rem 1.5rem;
      margin-bottom: 1rem;
    }
    .section-title {
      font-size: 0.75rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.06em;
      color: var(--text-dim);
      margin-bottom: 0.75rem;
    }

    /* ----- Status block ----- */
    .status-row {
      display: flex;
      align-items: center;
      gap: 1rem;
      flex-wrap: wrap;
    }
    .status-pill {
      flex-shrink: 0;
      font-size: 0.75rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      padding: 0.35rem 0.75rem;
      border-radius: 999px;
    }
    .status-pill.idle { background: var(--accent-glow); color: var(--accent); }
    .status-pill.rpiboot,
    .status-pill.waiting_choice,
    .status-pill.flashing,
    .status-pill.backup,
    .status-pill.eeprom_update { background: var(--warn-bg); color: var(--warn); }
    .status-pill.done { background: var(--success-bg); color: var(--success); }
    .status-pill.error { background: var(--danger-bg); color: var(--danger); }
    .status-msg {
      flex: 1;
      min-width: 0;
      font-size: 0.95rem;
      color: var(--text);
    }
    .status-err {
      width: 100%;
      margin-top: 0.5rem;
      font-size: 0.875rem;
      color: var(--danger);
    }
    .status-meta {
      font-size: 0.75rem;
      color: var(--text-muted);
      margin-top: 0.5rem;
    }
    .progress-track {
      margin-top: 0.75rem;
      height: 4px;
      background: var(--bg-tertiary);
      border-radius: 2px;
      overflow: hidden;
    }
    .progress-fill {
      height: 100%;
      background: var(--accent);
      border-radius: 2px;
      transition: width 0.3s ease;
    }
    .progress-fill.indeterminate {
      width: 35%;
      animation: slide 1.2s ease-in-out infinite;
    }
    @keyframes slide {
      0% { transform: translateX(-100%); }
      100% { transform: translateX(400%); }
    }

    /* ----- Pending devices ----- */
    .devices-list { list-style: none; }
    .device-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      padding: 0.875rem 1rem;
      background: var(--bg-tertiary);
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      margin-bottom: 0.5rem;
    }
    .device-item:last-child { margin-bottom: 0; }
    .device-info { min-width: 0; }
    .device-type {
      font-size: 0.7rem;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--text-dim);
      margin-bottom: 0.2rem;
    }
    .device-desc { font-size: 0.9rem; color: var(--text); }
    .device-actions { display: flex; gap: 0.5rem; flex-shrink: 0; align-items: center; }
    .eeprom-preset { padding: 0.35rem 0.5rem; font-size: 0.85rem; font-family: inherit; background: var(--bg-tertiary); color: var(--text); border: 1px solid var(--border); border-radius: 6px; max-width: 12rem; }
    .btn {
      padding: 0.5rem 1rem;
      font-size: 0.85rem;
      font-weight: 500;
      font-family: inherit;
      border-radius: var(--radius-sm);
      cursor: pointer;
      border: none;
      transition: background 0.15s, color 0.15s;
    }
    .btn-outline {
      background: transparent;
      border: 1px solid var(--border);
      color: var(--text);
    }
    .btn-outline:hover {
      border-color: var(--accent);
      color: var(--accent);
    }
    .btn-primary {
      background: var(--accent);
      color: var(--bg-primary);
    }
    .btn-primary:hover {
      background: var(--accent-dim);
      color: var(--bg-primary);
    }
    .empty-msg {
      text-align: center;
      padding: 1.5rem;
      font-size: 0.9rem;
      color: var(--text-muted);
    }

    /* ----- Backups table ----- */
    .backups-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 0.9rem;
    }
    .backups-table th {
      text-align: left;
      font-weight: 600;
      font-size: 0.75rem;
      text-transform: uppercase;
      letter-spacing: 0.05em;
      color: var(--text-dim);
      padding: 0.5rem 0;
      border-bottom: 1px solid var(--border);
    }
    .backups-table td {
      padding: 0.6rem 0;
      border-bottom: 1px solid var(--border);
      color: var(--text);
    }
    .backups-table tr:last-child td { border-bottom: none; }
    .backups-table a {
      color: var(--accent);
      text-decoration: none;
      font-weight: 500;
    }
    .backups-table a:hover { text-decoration: underline; }
    .backups-mono {
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.85em;
      color: var(--text-muted);
    }
    .backup-name-edit, .backup-desc-edit {
      cursor: text;
      padding: 0.2rem 0.35rem;
      margin: -0.2rem -0.35rem;
      border-radius: 4px;
    }
    .backup-name-edit:hover, .backup-desc-edit:hover { background: var(--bg-tertiary); }
    .backup-name-edit input, .backup-desc-edit input, .backup-desc-edit textarea {
      width: 100%;
      min-width: 120px;
      background: var(--bg-tertiary);
      border: 1px solid var(--accent);
      color: var(--text);
      padding: 0.25rem 0.4rem;
      border-radius: 4px;
      font: inherit;
    }
    .backup-desc-edit textarea { min-height: 2em; resize: vertical; }
    .backups-table .actions-cell { white-space: nowrap; }
    .backups-table .btn-sm { padding: 0.35rem 0.6rem; font-size: 0.8rem; }
    .golden-badge { font-size: 0.7rem; color: var(--accent); font-weight: 600; }
    .backups-table a.download-link { margin-right: 0.5rem; }
    .backup-deploy-hint {
      font-size: 0.9rem;
      color: var(--text-dim);
      margin-bottom: 1rem;
      padding: 0.75rem 1rem;
      background: var(--bg-tertiary);
      border-radius: var(--radius-sm);
      border-left: 3px solid var(--accent);
    }
    .placeholder-actions {
      display: flex;
      align-items: center;
      gap: 1rem;
      padding: 1rem;
      background: var(--bg-tertiary);
      border: 1px dashed var(--border);
      border-radius: var(--radius-sm);
      color: var(--text-muted);
      font-size: 0.9rem;
    }
    .placeholder-actions .btns { display: flex; gap: 0.5rem; }
    .placeholder-actions .btn { opacity: 0.5; pointer-events: none; }

    /* ----- Help & Log (collapsible) ----- */
    details {
      margin-top: 0.5rem;
      border: 1px solid var(--border);
      border-radius: var(--radius-sm);
      overflow: hidden;
    }
    details[open] { border-color: var(--text-muted); }
    summary {
      padding: 0.75rem 1rem;
      font-size: 0.9rem;
      font-weight: 500;
      color: var(--text-dim);
      cursor: pointer;
      list-style: none;
      user-select: none;
    }
    summary::-webkit-details-marker { display: none; }
    summary::before {
      content: '▶';
      display: inline-block;
      margin-right: 0.5rem;
      font-size: 0.65rem;
      transition: transform 0.2s;
    }
    details[open] summary::before { transform: rotate(90deg); }
    details .inner {
      padding: 1rem;
      padding-top: 0;
      font-size: 0.875rem;
      color: var(--text-dim);
      border-top: 1px solid var(--border);
    }
    .steps-list { list-style: none; }
    .steps-list li {
      display: flex;
      gap: 0.6rem;
      padding: 0.4rem 0;
    }
    .steps-list .num {
      flex-shrink: 0;
      width: 1.35rem;
      height: 1.35rem;
      background: var(--bg-tertiary);
      color: var(--text-dim);
      border-radius: 50%;
      font-size: 0.75rem;
      font-weight: 600;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    .steps-list strong { color: var(--text); }
    .help-sub { font-weight: 600; color: var(--text); margin: 0.75rem 0 0.25rem 0; }
    .log-pre {
      font-family: 'JetBrains Mono', monospace;
      font-size: 0.8rem;
      background: var(--bg-tertiary);
      border-radius: var(--radius-sm);
      padding: 0.75rem 1rem;
      max-height: 200px;
      overflow: auto;
      white-space: pre-wrap;
      word-break: break-all;
      color: var(--text-muted);
    }
    .log-pre:empty::before { content: 'No log output yet.'; }

    @media (max-width: 640px) {
      .wrap { padding: 1rem; }
      .device-item { flex-direction: column; align-items: flex-start; }
      .device-actions { width: 100%; justify-content: flex-end; }
    }
  </style>
</head>
<body>
  <div class="wrap">
    <header class="header">
      <h1>GNSS Guard Provisioning</h1>
      <p>Deploy or backup device via USB boot mode</p>
    </header>

    <!-- 1. Current status -->
    <section class="section">
      <h2 class="section-title">Current status</h2>
      <div id="status" class="status-row">
        <span id="statusPill" class="status-pill idle">Idle</span>
        <span id="statusMsg" class="status-msg">Waiting for Device in USB boot mode.</span>
        <button type="button" id="statusClearBtn" class="btn btn-outline btn-sm" style="margin-left: auto;">Clear status</button>
      </div>
      <div id="statusErr" class="status-err" style="display:none;"></div>
      <div id="statusGoldenHint" class="backup-deploy-hint" style="display:none; margin-top:0.75rem;">
        No golden image is required to <strong>backup</strong>. Connect a device in USB boot mode; when it appears under “Deploy and Backup”, click <strong>Backup</strong> to save its image. Then set that backup as golden in the list below.
        <button type="button" id="statusClearHintBtn" class="btn btn-outline btn-sm" style="margin-left:0.5rem;">Clear message</button>
      </div>
      <div id="statusMeta" class="status-meta" style="display:none;"></div>
      <div id="progressWrap" class="progress-track" style="display:none;">
        <div id="progressFill" class="progress-fill"></div>
      </div>
    </section>

    <!-- 2. Capture (Backup) or Deploy -->
    <section class="section">
      <h2 class="section-title">Deploy and Backup</h2>
      <p class="backup-deploy-hint">Connect a device in USB boot mode. When it appears below, click <strong>Backup</strong> to save its eMMC to a file, or <strong>Deploy</strong> to write the golden image to the device (requires a golden image set in Admin).</p>
      <p id="shrinkOptionWrap" class="backup-deploy-hint" style="display:none; margin-top:0.5rem;">
        <label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label> <span class="backups-mono" style="font-size:0.8rem;">(reduces size; requires PiShrink on host)</span>
      </p>
      <div id="pendingDevices"></div>
      <div id="noPendingPlaceholder" class="placeholder-actions" style="display:none;">
        <span>Connect a device to see:</span>
        <span class="btns">
          <button type="button" class="btn btn-outline btn-sm" disabled>Backup</button>
          <button type="button" class="btn btn-primary btn-sm" disabled>Deploy</button>
        </span>
        <span>— USB boot mode</span>
      </div>
      <p id="noPending" class="empty-msg" style="display:none;">No device connected. Connect device in USB boot mode (eMMC disable jumper + USB to host) — then the <strong>Backup</strong> and <strong>Deploy</strong> buttons will appear above.</p>
    </section>

    <!-- 3. Saved backups -->
    <section class="section">
      <h2 class="section-title">Saved backups
        <button type="button" class="btn btn-outline btn-sm" id="refreshBackupsBtn" title="Reload list">Refresh</button>
        <button type="button" class="btn btn-outline btn-sm" id="uploadImageBtn" title="Upload an image file">Upload image</button>
        <input type="file" id="uploadImageInput" accept=".img,.img.gz,.img.xz,image/*" style="display:none;" />
      </h2>
      <p id="goldenHint" class="backups-mono" style="margin-bottom:0.25rem;font-size:0.8rem;"></p>
      <p id="backupsDirHint" class="backups-mono" style="margin-bottom:0.75rem;font-size:0.75rem;color:var(--muted);"></p>
      <table class="backups-table" id="backupsTable">
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Size</th>
            <th>Date</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody id="backupsBody"></tbody>
      </table>
      <p id="backupsEmpty" class="empty-msg" style="display:none;">No backups yet. Capture one from a device (Backup above), then set it as golden for future deploys.</p>
    </section>

    <!-- 3b. Build cloud-init image from official Raspberry Pi OS -->
    <section class="section">
      <h2 class="section-title">Build cloud-init image</h2>
      <p class="backup-deploy-hint">Download the latest <strong>Raspberry Pi OS (arm64)</strong> from the official repository and inject cloud-init NoCloud files. The built image appears in Saved backups; you can then choose it and click <strong>Set as golden</strong> for deployment.</p>
      <div style="margin-bottom:0.5rem;">
        <label>Variant: </label>
        <select id="buildVariant">
          <option value="desktop" selected>Desktop (recommended)</option>
          <option value="lite">Lite (no desktop)</option>
          <option value="full">Full (desktop + software)</option>
        </select>
        <span id="buildRaspiosUrl" class="backups-mono" style="font-size:0.8rem; margin-left:0.5rem;"></span>
      </div>
      <div style="margin-bottom:0.5rem;">
        <label>Image name (optional): </label>
        <input type="text" id="buildImageName" placeholder="e.g. reterminal-kiosk" style="width:12rem;" />
        <span class="backups-mono" style="font-size:0.8rem; margin-left:0.5rem;">+ date suffix (e.g. 20251204-143022)</span>
      </div>
      <div style="margin-bottom:0.5rem;">
        <label><input type="checkbox" id="buildSetGolden" /> Set as golden image after build</label>
        <span class="backups-mono" style="font-size:0.8rem;"> (use for Deploy without clicking manually)</span>
      </div>
      <div style="margin-bottom:0.75rem;">
        <button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download &amp; build cloud-init image</button>
        <button type="button" id="buildCloudInitCancelBtn" class="btn btn-outline" style="display:none; margin-left:0.5rem;">Cancel build</button>
      </div>
      <div id="buildCloudInitStatus" class="backups-mono" style="font-size:0.85rem; min-height:1.5em; margin-bottom:0.5rem;"></div>
      <a href="#" id="buildCloudInitDismiss" style="display:none; font-size:0.85rem;">Dismiss</a>
      <details style="margin-top:0.5rem;">
        <summary>Cloud-init templates &amp; customize</summary>
        <div class="inner" style="margin-top:0.5rem;">
          <p><strong>Templates:</strong> <select id="buildTemplateSelect"><option value="">— Load a template —</option></select>
          <button type="button" id="buildTemplateLoad" class="btn btn-outline btn-sm">Load</button>
          <button type="button" id="buildTemplateUpdate" class="btn btn-outline btn-sm" title="Save current content into the selected template">Update</button>
          <button type="button" id="buildTemplateSave" class="btn btn-outline btn-sm">Save current as template…</button></p>
          <ul id="buildTemplateList" class="backups-mono" style="font-size:0.85rem; list-style:none; padding:0;"></ul>
          <label>user-data (YAML)</label>
          <textarea id="buildUserData" rows="8" style="width:100%; font-family:monospace; font-size:0.85rem; margin-bottom:0.5rem;" placeholder="Leave empty to use default (remote bootstrap example)"></textarea>
          <label>meta-data (optional)</label>
          <textarea id="buildMetaData" rows="3" style="width:100%; font-family:monospace; font-size:0.85rem; margin-bottom:0.5rem;"></textarea>
          <label>network-config (optional)</label>
          <textarea id="buildNetworkConfig" rows="5" style="width:100%; font-family:monospace; font-size:0.85rem;"></textarea>
        </div>
      </details>
    </section>

    <!-- 4. How to connect (collapsible) -->
    <details class="section" style="padding:0;">
      <summary>How to connect</summary>
      <div class="inner">
        <p class="help-sub">USB boot mode</p>
        <ol class="steps-list">
          <li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper, e.g. J2 / nRPIBOOT).</li>
          <li><span class="num">2</span> Connect <strong>USB slave</strong> to the host and power on. The device appears above; choose <strong>Backup</strong> or <strong>Deploy</strong>.</li>
          <li><span class="num">3</span> When done, remove the jumper and power cycle to boot from eMMC.</li>
        </ol>
      </div>
    </details>

    <!-- 5. Recent log (collapsible) -->
    <details class="section" style="padding:0;" open>
      <summary>Recent log</summary>
      <div class="inner">
        <pre id="log" class="log-pre"></pre>
      </div>
    </details>
  </div>

  <script>
    const statusPill = document.getElementById('statusPill');
    const statusMsg = document.getElementById('statusMsg');
    const statusErr = document.getElementById('statusErr');
    const statusMeta = document.getElementById('statusMeta');
    const progressWrap = document.getElementById('progressWrap');
    const progressFill = document.getElementById('progressFill');

    const phaseLabels = {
      idle: 'Idle',
      rpiboot: 'Connecting',
      waiting_choice: 'Choose action',
      flashing: 'Flashing',
      backup: 'Backing up',
      eeprom_update: 'Updating EEPROM',
      done: 'Done',
      error: 'Error'
    };

    function renderStatus(data) {
      const phase = data.phase || 'idle';
      statusPill.className = 'status-pill ' + phase;
      statusPill.textContent = phaseLabels[phase] || phase;
      statusMsg.textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');

      if (data.error) {
        statusErr.textContent = data.error;
        statusErr.style.display = 'block';
      } else {
        statusErr.style.display = 'none';
      }

      var goldenHint = document.getElementById('statusGoldenHint');
      if (goldenHint) {
        var isGoldenError = phase === 'error' && /golden|Golden image/i.test((data.error || '') + (data.message || ''));
        goldenHint.style.display = isGoldenError ? 'block' : 'none';
      }

      if (data.updated) {
        statusMeta.textContent = 'Updated ' + data.updated;
        statusMeta.style.display = 'block';
      } else {
        statusMeta.style.display = 'none';
      }

      const progress = data.progress;
      const inProgress = ['rpiboot', 'flashing', 'backup', 'eeprom_update'].includes(phase);
      if (inProgress || (phase === 'done' && progress != null)) {
        progressWrap.style.display = 'block';
        progressFill.classList.remove('indeterminate');
        if (progress != null) {
          progressFill.style.width = progress + '%';
        } else {
          progressFill.classList.add('indeterminate');
          progressFill.style.width = '35%';
        }
      } else {
        progressWrap.style.display = 'none';
      }
    }

    function renderPending(usb, network) {
      const container = document.getElementById('pendingDevices');
      const noPending = document.getElementById('noPending');
      container.innerHTML = '';
      let hasAny = false;

      const shrinkWrap = document.getElementById('shrinkOptionWrap');
      if (usb) {
        hasAny = true;
        if (shrinkWrap) shrinkWrap.style.display = 'block';
        const el = document.createElement('div');
        el.className = 'device-item';
        el.innerHTML = '<div class="device-info"><div class="device-type">USB boot</div><div class="device-desc">Device connected — choose Backup or Deploy</div></div><div class="device-actions"><button type="button" class="btn btn-outline" data-source="usb" data-action="backup">Backup</button><button type="button" class="btn btn-primary" data-source="usb" data-action="deploy">Deploy</button></div>';
        container.appendChild(el);
      } else {
        if (shrinkWrap) shrinkWrap.style.display = 'none';
      }

      const placeholder = document.getElementById('noPendingPlaceholder');
      noPending.style.display = hasAny ? 'none' : 'block';
      if (placeholder) placeholder.style.display = hasAny ? 'none' : 'flex';

      container.querySelectorAll('button[data-action]').forEach(function(btn) {
        btn.onclick = function() {
          const source = btn.getAttribute('data-source');
          const action = btn.getAttribute('data-action');
          const mac = btn.getAttribute('data-mac');
          const body = { source: source, action: action };
          if (mac) body.mac = mac;
          const shrinkCb = document.getElementById('shrinkAfterBackup');
          if (action === 'backup' && shrinkCb && shrinkCb.checked) body.shrink = true;
          fetch('/api/device-action', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) { fetchPending(); fetchStatus(); }
              else alert(data.error || 'Failed');
            })
            .catch(function() { alert('Request failed'); });
        };
      });
    }

    function renderBackups(backups) {
      const tbody = document.getElementById('backupsBody');
      const empty = document.getElementById('backupsEmpty');
      tbody.innerHTML = '';
      if (!backups || backups.length === 0) {
        empty.style.display = 'block';
        return;
      }
      empty.style.display = 'none';
      backups.forEach(function(b) {
        const tr = document.createElement('tr');
        tr.dataset.name = b.name;
        const displayName = (b.display_name || b.name);
        const desc = (b.description || '');
        const isRawImg = b.name.endsWith('.img') && !b.name.endsWith('.img.gz') && !b.name.endsWith('.img.xz');
        const shrinkBtn = isRawImg ? '<button type="button" class="btn btn-outline btn-sm shrink-btn" data-name="' + escapeHtml(b.name) + '" title="Shrink image (PiShrink)">Shrink</button> ' : '';
        const compressBtn = isRawImg ? '<button type="button" class="btn btn-outline btn-sm compress-btn" data-name="' + escapeHtml(b.name) + '" data-format="xz" title="Shrink and compress to .img.xz (minimum size)">Compress</button> ' : '';
        tr.innerHTML =
          '<td class="backup-name-edit" data-field="name" title="Click to rename">' + escapeHtml(displayName) + '</td>' +
          '<td class="backup-desc-edit" data-field="description" title="Click to add description">' + escapeHtml(desc) + '</td>' +
          '<td class="backups-mono">' + fmtSize(b.size) + '</td>' +
          '<td class="backups-mono">' + fmtDate(b.mtime) + '</td>' +
          '<td class="actions-cell">' +
            shrinkBtn +
            compressBtn +
            '<button type="button" class="btn btn-primary btn-sm set-golden-btn" data-name="' + escapeHtml(b.name) + '">Set as golden</button> ' +
            '<button type="button" class="btn btn-outline btn-sm rename-file-btn" data-name="' + escapeHtml(b.name) + '" title="Rename file">Rename file</button> ' +
            '<a href="/api/backups/' + encodeURIComponent(b.name) + '" download class="btn btn-outline btn-sm download-link">Download</a> ' +
            '<button type="button" class="btn btn-outline btn-sm delete-backup-btn" data-name="' + escapeHtml(b.name) + '" title="Delete this backup">Delete</button>' +
          '</td>';
        tbody.appendChild(tr);
      });
      bindBackupEdits();
      bindSetGolden();
      bindRenameFile();
      bindShrink();
      bindCompress();
      bindDeleteBackup();
    }

    function bindDeleteBackup() {
      document.querySelectorAll('.delete-backup-btn').forEach(function(btn) {
        btn.onclick = function() {
          const name = btn.getAttribute('data-name');
          if (!confirm('Delete this backup? This cannot be undone.\n\n' + name)) return;
          btn.disabled = true;
          fetch('/api/backups/' + encodeURIComponent(name), { method: 'DELETE' })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) { fetchBackups(); fetchGoldenInfo(); }
              else alert(data.error || 'Failed');
            })
            .catch(function() { alert('Request failed'); })
            .finally(function() { btn.disabled = false; });
        };
      });
    }

    function bindRenameFile() {
      document.querySelectorAll('.rename-file-btn').forEach(function(btn) {
        btn.onclick = function() {
          const name = btn.getAttribute('data-name');
          const newName = prompt('New filename (e.g. production-v1.img)', name);
          if (newName == null || newName.trim() === '') return;
          const n = newName.trim();
          if (!/\.(img|img\.gz|img\.xz)$/i.test(n)) { alert('Filename must end with .img, .img.gz or .img.xz'); return; }
          if (n === name) return;
          fetch('/api/backups/' + encodeURIComponent(name), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ filename: n }) })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) fetchBackups();
              else alert(data.error || 'Failed');
            })
            .catch(function() { alert('Request failed'); });
        };
      });
    }

    function bindBackupEdits() {
      document.querySelectorAll('.backup-name-edit[data-field], .backup-desc-edit[data-field]').forEach(function(cell) {
        if (cell._bound) return;
        cell._bound = true;
        cell.addEventListener('click', function() {
          if (cell.querySelector('input, textarea')) return;
          const field = cell.getAttribute('data-field');
          const row = cell.closest('tr');
          const filename = row.dataset.name;
          const isDesc = field === 'description';
          const current = cell.textContent.trim();
          const input = document.createElement(isDesc ? 'textarea' : 'input');
          input.type = isDesc ? 'text' : 'text';
          input.value = current;
          input.placeholder = isDesc ? 'Add a description…' : 'Name';
          cell.textContent = '';
          cell.appendChild(input);
          input.focus();
          if (isDesc) input.rows = 2;
          function save() {
            const val = input.value.trim();
            const body = isDesc ? { description: val } : { name: val || filename };
            fetch('/api/backups/' + encodeURIComponent(filename), { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
              .then(function(r) { return r.json(); })
              .then(function(data) {
                if (data.ok) { fetchBackups(); }
                else { alert(data.error || 'Failed'); cell.innerHTML = escapeHtml(current); }
              })
              .catch(function() { cell.innerHTML = escapeHtml(current); });
          }
          input.addEventListener('blur', save);
          input.addEventListener('keydown', function(e) {
            if (e.key === 'Enter' && !isDesc) { e.preventDefault(); input.blur(); }
          });
        });
      });
    }

    function bindSetGolden() {
      document.querySelectorAll('.set-golden-btn').forEach(function(btn) {
        btn.onclick = function() {
          const name = btn.getAttribute('data-name');
          if (!confirm('Set this backup as the golden image? Future deploys will use it.\n\n' + name)) return;
          fetch('/api/backups/' + encodeURIComponent(name) + '/set-as-golden', { method: 'POST' })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) { fetchBackups(); fetchGoldenInfo(); }
              else alert(data.error || 'Failed');
            })
            .catch(function() { alert('Request failed'); });
        };
      });
    }

    function bindShrink() {
      document.querySelectorAll('.shrink-btn').forEach(function(btn) {
        btn.onclick = function() {
          const name = btn.getAttribute('data-name');
          if (!confirm('Shrink this image with PiShrink? This reduces file size and may take a few minutes.\n\n' + name)) return;
          btn.disabled = true;
          btn.textContent = 'Shrinking…';
          fetch('/api/backups/' + encodeURIComponent(name) + '/shrink', { method: 'POST' })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) { fetchBackups(); }
              else alert(data.error || data.detail || 'Failed');
            })
            .catch(function() { alert('Request failed'); })
            .finally(function() { btn.disabled = false; btn.textContent = 'Shrink'; });
        };
      });
    }

    function bindCompress() {
      document.querySelectorAll('.compress-btn').forEach(function(btn) {
        btn.onclick = function() {
          const name = btn.getAttribute('data-name');
          const format = btn.getAttribute('data-format') || 'xz';
          if (!confirm('Shrink and compress this image to .img.' + format + '? This minimizes size and may take several minutes.\n\n' + name)) return;
          btn.disabled = true;
          btn.textContent = 'Compressing…';
          fetch('/api/backups/' + encodeURIComponent(name) + '/compress', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ format: format }) })
            .then(function(r) { return r.json(); })
            .then(function(data) {
              if (data.ok) { fetchBackups(); }
              else alert(data.error || data.detail || 'Failed');
            })
            .catch(function() { alert('Request failed'); })
            .finally(function() { btn.disabled = false; btn.textContent = 'Compress'; });
        };
      });
    }

    function fetchGoldenInfo() {
      fetch('/api/golden-info').then(function(r) { return r.json(); }).then(function(d) {
        const el = document.getElementById('goldenHint');
        el.textContent = d.present ? ('Golden image: ' + fmtSize(d.size) + ', updated ' + fmtDate(d.mtime)) : 'No golden image set. Capture a backup and click "Set as golden" to use it for Deploy.';
      }).catch(function() {});
    }

    function fetchStatus() {
      fetch('/api/status').then(function(r) { return r.json(); }).then(renderStatus).catch(function() { renderStatus({ phase: 'error', message: 'Could not load status.' }); });
    }
    function fetchPending() {
      fetch('/api/pending-devices').then(function(r) { return r.json(); }).then(function(d) { renderPending(d.usb || null, d.network || []); }).catch(function() { renderPending(null, []); });
    }
    function fetchLog() {
      fetch('/api/log').then(function(r) { return r.json(); }).then(function(d) {
        var logEl = document.getElementById('log');
        if (!logEl) return;
        logEl.textContent = d.log || '';
        logEl.scrollTop = logEl.scrollHeight;
      }).catch(function() {});
    }
    function fetchBackups() {
      fetch('/api/backups').then(function(r) { return r.json(); }).then(function(d) {
        renderBackups(d.backups || []);
        var dirEl = document.getElementById('backupsDirHint');
        if (dirEl && d.backups_dir) dirEl.textContent = 'Stored in: ' + d.backups_dir;
      }).catch(function() {});
    }

    function getBuildVariant() {
      var sel = document.getElementById('buildVariant');
      return (sel && sel.value) ? sel.value : 'desktop';
    }
    function fetchRaspiosUrl() {
      var variant = getBuildVariant();
      fetch('/api/raspios-latest-url?variant=' + encodeURIComponent(variant)).then(function(r) { return r.json(); }).then(function(d) {
        var el = document.getElementById('buildRaspiosUrl');
        if (el) el.textContent = d.ok && d.filename ? ('Latest: ' + d.filename) : (d.error || 'Could not resolve latest image URL');
      }).catch(function() {});
    }
    function fetchCloudInitTemplates() {
      fetch('/api/cloudinit-templates').then(function(r) { return r.json(); }).then(function(d) {
        var list = d.templates || [];
        var sel = document.getElementById('buildTemplateSelect');
        var listEl = document.getElementById('buildTemplateList');
        if (sel) {
          sel.innerHTML = '<option value="">— Load a template —</option>';
          list.forEach(function(t) {
            var opt = document.createElement('option');
            opt.value = t.id;
            opt.textContent = t.name;
            sel.appendChild(opt);
          });
        }
        if (listEl) {
          listEl.innerHTML = list.map(function(t) {
            return '<li><span>' + escapeHtml(t.name) + '</span> <button type="button" class="btn btn-outline btn-sm template-load-btn" data-id="' + escapeHtml(t.id) + '">Load</button> <button type="button" class="btn btn-outline btn-sm template-upd-btn" data-id="' + escapeHtml(t.id) + '" title="Save current content into this template">Update</button> <button type="button" class="btn btn-outline btn-sm template-del-btn" data-id="' + escapeHtml(t.id) + '">Delete</button></li>';
          }).join('') || '<li>No templates saved.</li>';
          listEl.querySelectorAll('.template-load-btn').forEach(function(btn) {
            btn.onclick = function() { loadTemplate(btn.getAttribute('data-id')); };
          });
          listEl.querySelectorAll('.template-upd-btn').forEach(function(btn) {
            btn.onclick = function() { updateTemplate(btn.getAttribute('data-id')); };
          });
          listEl.querySelectorAll('.template-del-btn').forEach(function(btn) {
            btn.onclick = function() {
              if (!confirm('Delete this template?')) return;
              fetch('/api/cloudinit-templates/' + encodeURIComponent(btn.getAttribute('data-id')), { method: 'DELETE' })
                .then(function(r) { return r.json(); }).then(function(data) { if (data.ok) fetchCloudInitTemplates(); });
            };
          });
        }
      }).catch(function() {});
    }
    function loadTemplate(id) {
      fetch('/api/cloudinit-templates/' + encodeURIComponent(id)).then(function(r) { return r.json(); }).then(function(t) {
        var ud = document.getElementById('buildUserData');
        var md = document.getElementById('buildMetaData');
        var nc = document.getElementById('buildNetworkConfig');
        if (ud) ud.value = t.user_data || '';
        if (md) md.value = t.meta_data || '';
        if (nc) nc.value = t.network_config || '';
      }).catch(function() {});
    }
    function updateTemplate(id) {
      var ud = document.getElementById('buildUserData');
      var md = document.getElementById('buildMetaData');
      var nc = document.getElementById('buildNetworkConfig');
      fetch('/api/cloudinit-templates/' + encodeURIComponent(id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
        user_data: ud ? ud.value : '',
        meta_data: md ? md.value : '',
        network_config: nc ? nc.value : ''
      })}).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) { fetchCloudInitTemplates(); alert('Template updated'); }
        else alert(d.error || 'Update failed');
      }).catch(function() { alert('Update failed'); });
    }
    function saveTemplate() {
      var name = prompt('Template name');
      if (!name || !name.trim()) return;
      var ud = document.getElementById('buildUserData');
      var md = document.getElementById('buildMetaData');
      var nc = document.getElementById('buildNetworkConfig');
      fetch('/api/cloudinit-templates', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
        name: name.trim(),
        user_data: ud ? ud.value : '',
        meta_data: md ? md.value : '',
        network_config: nc ? nc.value : ''
      })}).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) { fetchCloudInitTemplates(); alert('Saved as "' + name + '"'); }
        else alert(d.error || 'Failed');
      }).catch(function() { alert('Failed'); });
    }

    function fetchBuildStatus() {
      fetch('/api/build-cloudinit-status').then(function(r) { return r.json(); }).then(function(d) {
        var el = document.getElementById('buildCloudInitStatus');
        var btn = document.getElementById('buildCloudInitBtn');
        var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
        var dismissEl = document.getElementById('buildCloudInitDismiss');
        if (!el) return;
        var busy = ['resolving','downloading','decompressing','injecting','finalizing'].indexOf(d.phase) >= 0;
        if (btn) btn.disabled = busy;
        if (cancelBtn) { cancelBtn.style.display = busy ? 'inline-block' : 'none'; cancelBtn.disabled = false; }
        if (dismissEl) dismissEl.style.display = (d.phase !== 'idle' || (d.message && d.message.trim())) ? 'inline' : 'none';
        if (d.phase === 'idle' && !d.message) {
          el.textContent = '';
        } else if (d.phase === 'done') {
          el.textContent = 'Done: ' + (d.output_name || '') + ' — see Saved backups above. Click "Set as golden" next to it to use for Deploy. If missing, click Refresh.';
          fetchBackups();
          fetchGoldenInfo();
        } else if (d.phase === 'error') {
          el.textContent = 'Error: ' + (d.error || d.message || 'Unknown');
        } else if (d.phase === 'cancelled') {
          el.textContent = 'Build cancelled.';
          if (btn) btn.disabled = false;
          if (!window._buildClearScheduled) {
            window._buildClearScheduled = true;
            setTimeout(function() {
              fetch('/api/build-cloudinit-status-clear?force=1', { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(function() { fetchBuildStatus(); }).finally(function() { window._buildClearScheduled = false; });
            }, 3000);
          }
        } else {
          window._buildClearScheduled = false;
          el.textContent = (d.phase || '') + ': ' + (d.message || '');
        }
        if (busy) setTimeout(fetchBuildStatus, 5000);
      }).catch(function() {});
    }
    function cancelBuildCloudInit() {
      var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
      if (cancelBtn) cancelBtn.disabled = true;
      document.getElementById('buildCloudInitStatus').textContent = 'Cancelling…';
      fetch('/api/build-cloudinit-cancel', { method: 'POST', headers: { 'Content-Type': 'application/json' } })
        .then(function(r) { return r.json(); })
        .then(function(d) {
          if (d.ok) setTimeout(fetchBuildStatus, 2000);
          else alert(d.error || 'Cancel request failed');
        })
        .catch(function() { if (cancelBtn) cancelBtn.disabled = false; });
    }

    function startBuildCloudInit() {
      var btn = document.getElementById('buildCloudInitBtn');
      if (btn) btn.disabled = true;
      var ud = document.getElementById('buildUserData');
      var md = document.getElementById('buildMetaData');
      var nc = document.getElementById('buildNetworkConfig');
      var setGolden = document.getElementById('buildSetGolden');
      var nameEl = document.getElementById('buildImageName');
      var body = {
        variant: getBuildVariant(),
        set_as_golden_after: setGolden && setGolden.checked,
        image_name: (nameEl && nameEl.value.trim()) ? nameEl.value.trim() : undefined,
        user_data: (ud && ud.value.trim()) ? ud.value.trim() : undefined,
        meta_data: (md && md.value.trim()) ? md.value.trim() : undefined,
        network_config: (nc && nc.value.trim()) ? nc.value.trim() : undefined
      };
      fetch('/api/build-cloudinit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
        .then(function(r) { return r.json(); })
        .then(function(data) {
          if (data.ok) {
            document.getElementById('buildCloudInitStatus').textContent = 'Build started on host. Waiting…';
            setTimeout(fetchBuildStatus, 2000);
          } else {
            alert(data.error || 'Failed to start build');
            if (btn) btn.disabled = false;
          }
        })
        .catch(function() { alert('Request failed'); if (btn) btn.disabled = false; });
    }

    function fmtSize(n) {
      if (n >= 1e9) return (n / 1e9).toFixed(1) + ' GB';
      if (n >= 1e6) return (n / 1e6).toFixed(1) + ' MB';
      return (n / 1e3).toFixed(0) + ' KB';
    }
    function fmtDate(ts) { return new Date(ts * 1000).toLocaleString(); }
    function escapeHtml(s) {
      const d = document.createElement('div');
      d.textContent = s;
      return d.innerHTML;
    }

    document.getElementById('statusClearBtn').addEventListener('click', function() {
      fetch('/api/status-clear', { method: 'POST' }).then(function(r) { return r.json(); }).then(function(d) { if (d.ok) fetchStatus(); });
    });
    var statusClearHintBtn = document.getElementById('statusClearHintBtn');
    if (statusClearHintBtn) statusClearHintBtn.addEventListener('click', function() {
      fetch('/api/status-clear', { method: 'POST' }).then(function(r) { return r.json(); }).then(function(d) { if (d.ok) fetchStatus(); });
    });
    var refreshBackupsBtn = document.getElementById('refreshBackupsBtn');
    if (refreshBackupsBtn) refreshBackupsBtn.onclick = function() { fetchBackups(); fetchGoldenInfo(); };
    var uploadImageBtn = document.getElementById('uploadImageBtn');
    var uploadImageInput = document.getElementById('uploadImageInput');
    if (uploadImageBtn && uploadImageInput) {
      uploadImageBtn.onclick = function() { uploadImageInput.click(); };
      uploadImageInput.onchange = function() {
        var file = uploadImageInput.files && uploadImageInput.files[0];
        if (!file) return;
        var fd = new FormData();
        fd.append('file', file);
        uploadImageBtn.disabled = true;
        fetch('/api/backups/upload', { method: 'POST', body: fd })
          .then(function(r) { return r.json(); })
          .then(function(d) {
            if (d.ok) { fetchBackups(); fetchGoldenInfo(); alert('Uploaded: ' + (d.name || file.name)); }
            else alert(d.error || 'Upload failed');
          })
          .catch(function() { alert('Upload failed'); })
          .finally(function() { uploadImageBtn.disabled = false; uploadImageInput.value = ''; });
      };
    }

    fetchStatus();
    fetchLog();
    fetchPending();
    fetchBackups();
    fetchGoldenInfo();
    fetchRaspiosUrl();
    fetchBuildStatus();
    fetchCloudInitTemplates();
    var buildBtn = document.getElementById('buildCloudInitBtn');
    if (buildBtn) buildBtn.onclick = startBuildCloudInit;
    var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn');
    if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuildCloudInit;
    var dismissBuildBtn = document.getElementById('buildCloudInitDismiss');
    if (dismissBuildBtn) dismissBuildBtn.onclick = function(e) { e.preventDefault(); fetch('/api/build-cloudinit-status-clear?force=1', { method: 'POST', headers: { 'Content-Type': 'application/json' } }).then(function() { fetchBuildStatus(); }); };
    var variantSel = document.getElementById('buildVariant');
    if (variantSel) variantSel.onchange = fetchRaspiosUrl;
    var templateLoadBtn = document.getElementById('buildTemplateLoad');
    if (templateLoadBtn) templateLoadBtn.onclick = function() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) loadTemplate(s.value); };
    var templateUpdateBtn = document.getElementById('buildTemplateUpdate');
    if (templateUpdateBtn) templateUpdateBtn.onclick = function() { var s = document.getElementById('buildTemplateSelect'); if (s && s.value) updateTemplate(s.value); else alert('Select a template to update'); };
    var templateSaveBtn = document.getElementById('buildTemplateSave');
    if (templateSaveBtn) templateSaveBtn.onclick = saveTemplate;
    setInterval(fetchStatus, 2000);
    setInterval(fetchLog, 4000);
    setInterval(fetchPending, 2000);
    setInterval(fetchBackups, 5000);
    setInterval(fetchBuildStatus, 15000);
    setInterval(fetchGoldenInfo, 10000);
  </script>
</body>
</html>
