<!DOCTYPE html>
<!-- Revision: 2 -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Deploy · 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-card: #151a24;
      --accent: #00d4aa;
      --accent-dim: #00b894;
      --text: #e6e8eb;
      --text-dim: #8b949e;
      --text-muted: #5c6370;
      --border: #2d333b;
      --danger: #f87171;
      --warn: #fbbf24;
      --success: #00d4aa;
      --radius: 10px;
      --radius-sm: 6px;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Outfit', sans-serif; background: var(--bg-primary); color: var(--text); min-height: 100vh; font-size: 14px; line-height: 1.45; }
    .wrap { max-width: 1100px; margin: 0 auto; padding: 1rem; }
    .header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.75rem; margin-bottom: 1rem; padding-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
    .header h1 { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.02em; }
    .header a { color: var(--text-dim); text-decoration: none; font-size: 0.9rem; }
    .header a:hover { color: var(--accent); }
    .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
    @media (max-width: 700px) { .grid { grid-template-columns: 1fr; } }
    .card {
      background: var(--bg-card);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      padding: 1rem;
    }
    .card-title { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-bottom: 0.6rem; }
    .status-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
    .status-pill { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; padding: 0.25rem 0.6rem; border-radius: 999px; }
    .status-pill.idle { background: rgba(0,212,170,0.15); color: var(--accent); }
    .status-pill.waiting_choice, .status-pill.rpiboot, .status-pill.flashing, .status-pill.backup, .status-pill.eeprom_update { background: rgba(251,191,36,0.15); color: var(--warn); }
    .status-pill.done { background: rgba(0,212,170,0.15); color: var(--success); }
    .status-pill.error { background: rgba(248,113,113,0.15); color: var(--danger); }
    .status-msg { flex: 1; min-width: 0; font-size: 0.9rem; }
    .status-err { width: 100%; margin-top: 0.4rem; font-size: 0.8rem; color: var(--danger); }
    .progress-track { margin-top: 0.5rem; height: 3px; background: var(--border); border-radius: 2px; overflow: hidden; }
    .progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
    .progress-fill.indeterminate { width: 35%; animation: slide 1.2s ease-in-out infinite; }
    @keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
    .firstboot-progress-wrap { margin-top: 0.6rem; height: 6px; background: var(--border); border-radius: 3px; overflow: hidden; }
    .firstboot-progress-fill { height: 100%; background: linear-gradient(90deg, var(--accent-dim), var(--accent)); border-radius: 3px; transition: width 0.4s ease-out; }
    .firstboot-progress-fill.animated { background: linear-gradient(90deg, var(--accent-dim), var(--accent) 50%, var(--accent-dim)); background-size: 200% 100%; animation: firstboot-shimmer 1.5s ease-in-out infinite; }
    @keyframes firstboot-shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
    .firstboot-step-label { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 0.35rem; }
    .device-item {
      display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
      padding: 0.6rem 0.75rem; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius-sm);
      margin-bottom: 0.4rem;
    }
    .device-desc { font-size: 0.85rem; color: var(--text-dim); }
    .device-actions-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
    .eeprom-preset { padding: 0.3rem 0.5rem; font-size: 0.75rem; font-family: inherit; background: var(--bg-primary); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius-sm); max-width: 11rem; }
    .btn { padding: 0.4rem 0.8rem; font-size: 0.8rem; 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); }
    .btn-sm { padding: 0.3rem 0.5rem; font-size: 0.75rem; }
    .golden-info { font-size: 0.85rem; color: var(--text-dim); }
    .golden-info strong { color: var(--text); }
    .empty-msg { text-align: center; padding: 1rem; font-size: 0.85rem; color: var(--text-muted); }
    details { margin-top: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
    summary { padding: 0.5rem 0.75rem; font-size: 0.85rem; font-weight: 500; color: var(--text-dim); cursor: pointer; list-style: none; }
    summary::-webkit-details-marker { display: none; }
    summary::before { content: '▶'; display: inline-block; margin-right: 0.4rem; font-size: 0.6rem; transition: transform 0.2s; }
    details[open] summary::before { transform: rotate(90deg); }
    details .inner { padding: 0.75rem; padding-top: 0; font-size: 0.8rem; color: var(--text-dim); border-top: 1px solid var(--border); }
    .log-pre { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; background: var(--bg-secondary); border-radius: var(--radius-sm); padding: 0.5rem 0.75rem; max-height: 160px; overflow: auto; white-space: pre-wrap; word-break: break-all; color: var(--text-muted); }
    .steps-list { list-style: none; }
    .steps-list li { display: flex; gap: 0.5rem; padding: 0.3rem 0; }
    .steps-list .num { flex-shrink: 0; width: 1.2rem; height: 1.2rem; background: var(--bg-secondary); color: var(--text-dim); border-radius: 50%; font-size: 0.7rem; 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.5rem 0 0.2rem 0; font-size: 0.85rem; }
    .leases-table { width: 100%; font-size: 0.8rem; border-collapse: collapse; }
    .leases-table th, .leases-table td { padding: 0.35rem 0.5rem; text-align: left; border-bottom: 1px solid var(--border); }
    .leases-table th { color: var(--text-dim); font-weight: 600; }
    .leases-table .mono { font-family: 'JetBrains Mono', monospace; }
  </style>
</head>
<body>
  <div class="wrap">
    <header class="header">
      <h1>GNSS Guard Provisioning</h1>
      <a href="/admin">Admin</a>
    </header>

    <div class="grid">
      <!-- Left: Status + Deploy -->
      <div>
        <div class="card" style="margin-bottom: 1rem;">
          <h2 class="card-title">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="progressWrap" class="progress-track" style="display:none;"><div id="progressFill" class="progress-fill"></div></div>
        </div>

        <div id="firstBootCard" class="card" style="margin-bottom: 1rem; display:none;">
          <h2 class="card-title">First-boot progress</h2>
          <p id="firstBootStepLabel" class="firstboot-step-label">Step 0 of 13</p>
          <div id="firstBootProgressWrap" class="firstboot-progress-wrap" style="display:none;">
            <div id="firstBootProgressFill" class="firstboot-progress-fill" style="width:0%;"></div>
          </div>
          <div id="firstBootStatus" class="status-row" style="margin-top: 0.5rem;">
            <span id="firstBootStep" class="status-pill idle"></span>
            <span id="firstBootMsg" class="status-msg"></span>
          </div>
          <p id="firstBootIpWrap" style="margin-top: 0.5rem; font-size: 0.9rem; display:none;"><strong>Device IP:</strong> <code id="firstBootIp" class="mono"></code></p>
        </div>

        <div class="card">
          <h2 class="card-title">Deploy and Backup</h2>
          <p id="shrinkOptionWrap" style="display:none; margin-bottom: 0.5rem; font-size: 0.8rem;"><label><input type="checkbox" id="shrinkAfterBackup" /> Shrink after backup</label></p>
          <div id="pendingDevices"></div>
          <p id="noPending" class="empty-msg" style="display:none;">No device connected. Use USB boot mode.</p>
        </div>
      </div>

      <!-- Right: Golden + Log + Connect -->
      <div>
        <div class="card" style="margin-bottom: 1rem;">
          <h2 class="card-title">Current deploy image</h2>
          <p id="goldenInfo" class="golden-info">Loading…</p>
        </div>

        <details class="card" style="padding: 0;" open>
          <summary>Recent log</summary>
          <div class="inner"><pre id="log" class="log-pre"></pre></div>
        </details>

        <details class="card" style="padding: 0; margin-top: 1rem;" open>
          <summary>DHCP leases</summary>
          <div class="inner">
            <p id="leasesNote" class="help-sub" style="margin-top:0;">Provisioning LAN (dnsmasq) — when dashboard runs on LXC.</p>
            <div id="leasesContent"><table class="leases-table"><thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Expires</th></tr></thead><tbody id="leasesBody"></tbody></table></div>
            <p id="leasesError" style="display:none; font-size: 0.8rem; color: var(--text-muted);"></p>
            <p id="leasesEmpty" style="display:none; font-size: 0.85rem; color: var(--text-muted);">No leases file or no active leases.</p>
          </div>
        </details>

        <details class="card" style="padding: 0; margin-top: 1rem;">
          <summary>How to connect</summary>
          <div class="inner">
            <p class="help-sub">USB boot</p>
            <ol class="steps-list">
              <li><span class="num">1</span> Set device to <strong>boot mode</strong> (eMMC disable jumper).</li>
              <li><span class="num">2</span> Connect USB to host; choose <strong>Backup</strong> or <strong>Deploy</strong> above.</li>
              <li><span class="num">3</span> Remove jumper and power cycle when done.</li>
            </ol>
          </div>
        </details>
      </div>
    </div>
  </div>

  <script>
    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';
      document.getElementById('statusPill').className = 'status-pill ' + phase;
      document.getElementById('statusPill').textContent = phaseLabels[phase] || phase;
      document.getElementById('statusMsg').textContent = data.message || (phase === 'idle' ? 'Waiting for Device in USB boot mode.' : '');
      const err = document.getElementById('statusErr');
      if (data.error) { err.textContent = data.error; err.style.display = 'block'; } else { err.style.display = 'none'; }
      if (phase === 'done') scheduleDoneClear();
      const progress = data.progress;
      const inProgress = ['rpiboot', 'flashing', 'backup', 'eeprom_update'].includes(phase);
      const wrap = document.getElementById('progressWrap');
      const fill = document.getElementById('progressFill');
      if (inProgress || (phase === 'done' && progress != null)) {
        wrap.style.display = 'block';
        fill.classList.remove('indeterminate');
        fill.style.width = (progress != null ? progress + '%' : '35%');
        if (progress == null) fill.classList.add('indeterminate');
      } else wrap.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;
        shrinkWrap.style.display = 'block';
        const el = document.createElement('div');
        el.className = 'device-item';
        el.innerHTML = '<div class="device-desc">USB device — choose Backup or Deploy</div><div class="device-actions-row"><button type="button" class="btn btn-outline btn-sm" data-source="usb" data-action="backup">Backup</button> <button type="button" class="btn btn-primary btn-sm" data-source="usb" data-action="deploy">Deploy</button></div>';
        container.appendChild(el);
      } else shrinkWrap.style.display = 'none';
      noPending.style.display = hasAny ? 'none' : 'block';
      container.querySelectorAll('button[data-action]').forEach(function(btn) {
        btn.onclick = function() {
          const body = { source: btn.getAttribute('data-source'), action: btn.getAttribute('data-action') };
          if (body.source === 'network') body.mac = btn.getAttribute('data-mac');
          if (body.action === 'backup' && document.getElementById('shrinkAfterBackup').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 escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
    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 fetchStatus() { fetch('/api/status').then(function(r){ return r.json(); }).then(renderStatus).catch(function(){ renderStatus({ phase: 'error', message: 'Could not load status.' }); }); }
    const FIRST_BOOT_TOTAL_STEPS = 13;
    function renderFirstBootStatus(data) {
      const phase = data.phase || 'idle';
      const card = document.getElementById('firstBootCard');
      if (phase === 'idle' || phase === '') { card.style.display = 'none'; return; }
      card.style.display = 'block';
      const stepEl = document.getElementById('firstBootStep');
      const msgEl = document.getElementById('firstBootMsg');
      const stepLabelEl = document.getElementById('firstBootStepLabel');
      const progressWrap = document.getElementById('firstBootProgressWrap');
      const progressFill = document.getElementById('firstBootProgressFill');
      const ipWrap = document.getElementById('firstBootIpWrap');
      const ipEl = document.getElementById('firstBootIp');
      var stepNum = parseInt(data.step, 10) || 0;
      if (phase === 'done') stepNum = FIRST_BOOT_TOTAL_STEPS;
      var progress = stepNum > 0 ? (stepNum / FIRST_BOOT_TOTAL_STEPS * 100) : 0;
      var inProgress = (phase === 'started' || phase === 'running');
      stepLabelEl.textContent = 'Step ' + stepNum + ' of ' + FIRST_BOOT_TOTAL_STEPS + (data.step_name ? ' · ' + data.step_name : '');
      if (inProgress || phase === 'done' || phase === 'error') {
        progressWrap.style.display = 'block';
        progressFill.style.width = progress + '%';
        if (inProgress) progressFill.classList.add('animated'); else progressFill.classList.remove('animated');
      } else {
        progressWrap.style.display = 'none';
      }
      stepEl.textContent = data.step ? 'Step ' + data.step : (phase === 'done' ? 'Done' : phase);
      stepEl.className = 'status-pill ' + (phase === 'done' ? 'done' : phase === 'error' ? 'error' : 'flashing');
      msgEl.textContent = data.message || (phase === 'done' && data.ip ? 'First-boot finished. Device IP: ' + data.ip : '');
      if (phase === 'done' && data.ip) {
        ipWrap.style.display = 'block';
        ipEl.textContent = data.ip;
      } else {
        ipWrap.style.display = 'none';
      }
    }
    function fetchFirstBootStatus() {
      fetch('/api/first-boot-status').then(function(r){ return r.json(); }).then(renderFirstBootStatus).catch(function(){});
    }
    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 fetchGolden() {
      fetch('/api/golden-info').then(function(r){ return r.json(); }).then(function(d){
        const el = document.getElementById('goldenInfo');
        if (!d.present) { el.innerHTML = 'No image set. An admin must set a backup or cloud-init image as golden for Deploy.'; return; }
        let t = 'Ready for deploy: ' + fmtSize(d.size) + ', updated ' + fmtDate(d.mtime);
        if (d.name) t += ' <br><strong>' + (d.source === 'cloudinit' ? 'Cloud-init' : 'Backup') + ':</strong> ' + escapeHtml(d.name);
        el.innerHTML = t;
      }).catch(function(){ document.getElementById('goldenInfo').textContent = 'Could not load.'; });
    }
    var doneClearTimer = null;
    function scheduleDoneClear() {
      if (doneClearTimer) return;
      doneClearTimer = setTimeout(function(){ doneClearTimer = null; fetch('/api/status-clear', { method: 'POST' }).then(function(r){ return r.json(); }).then(function(d){ if(d.ok) fetchStatus(); }); }, 60000);
    }
    function fetchDhcpLeases() {
      fetch('/api/dhcp-leases').then(function(r){ return r.json(); }).then(function(d){
        const tbody = document.getElementById('leasesBody');
        const errEl = document.getElementById('leasesError');
        const emptyEl = document.getElementById('leasesEmpty');
        errEl.style.display = 'none';
        emptyEl.style.display = 'none';
        if (d.error) { errEl.textContent = d.error; errEl.style.display = 'block'; tbody.innerHTML = ''; return; }
        var rows = (d.leases || []).map(function(l){
          var expStr = l.expiry ? (new Date(l.expiry*1000).toLocaleString()) : '—';
          return '<tr><td class="mono">' + escapeHtml(l.ip) + '</td><td class="mono">' + escapeHtml(l.mac) + '</td><td>' + escapeHtml(l.hostname || '') + '</td><td>' + expStr + '</td></tr>';
        }).join('');
        tbody.innerHTML = rows || '';
        if (!rows) emptyEl.style.display = 'block';
      }).catch(function(){ document.getElementById('leasesError').textContent = 'Could not load leases.'; document.getElementById('leasesError').style.display = 'block'; });
    }
    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(); }); });
    fetchStatus(); fetchLog(); fetchPending(); fetchGolden(); fetchDhcpLeases(); fetchFirstBootStatus();
    setInterval(fetchStatus, 2000);
    setInterval(fetchLog, 4000);
    setInterval(fetchPending, 2000);
    setInterval(fetchFirstBootStatus, 3000);
    setInterval(fetchGolden, 10000);
    setInterval(fetchDhcpLeases, 10000);
  </script>
</body>
</html>
