<!DOCTYPE html>
<!-- Revision: 2 -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Cloud-init build · Admin · 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; --text: #e6e8eb; --text-dim: #8b949e; --text-muted: #5c6370;
      --border: #2d333b; --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.5; }
    .wrap { max-width: 1000px; 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; }
    .header span { color: var(--text-dim); font-size: 0.9rem; }
    .nav a { color: var(--text-dim); text-decoration: none; margin-right: 1rem; }
    .nav a:hover { color: var(--accent); }
    .nav a.active { color: var(--accent); font-weight: 600; }
    .header a { color: var(--text-dim); text-decoration: none; margin-left: 0.5rem; }
    .header a:hover { color: var(--accent); }
    .section { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.25rem; margin-bottom: 1rem; }
    .section-title { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-dim); margin-bottom: 0.75rem; }
    .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; }
    .tabs { display: flex; gap: 0.25rem; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
    .tabs button { padding: 0.5rem 1rem; background: none; border: none; color: var(--text-dim); cursor: pointer; font: inherit; border-bottom: 2px solid transparent; margin-bottom: -1px; }
    .tabs button:hover { color: var(--text); }
    .tabs button.active { color: var(--accent); border-bottom-color: var(--accent); }
    .tab-pane { display: none; }
    .tab-pane.active { display: block; }
    textarea { width: 100%; min-height: 220px; resize: vertical; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.5rem; border-radius: 4px; }
    .mono { font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--text-muted); }
    input[type="text"], select { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.35rem 0.5rem; border-radius: 4px; font: inherit; }
    label { display: inline-block; margin-right: 0.5rem; }
    ul { list-style: none; padding: 0; }
    ul li { margin-bottom: 0.35rem; }
  </style>
</head>
<body>
  <div class="wrap">
    <header class="header">
      <div>
        <h1>Cloud-init build</h1>
        <nav class="nav" style="margin-top:0.35rem;">
          <a href="/admin">Admin</a>
          <a href="/admin/portal-files">Portal files</a>
          <a href="/admin/cloudinit-build" class="active">Cloud-init build</a>
        </nav>
      </div>
      <span>Logged in as <strong>{{ username }}</strong></span>
      <a href="/">Deploy</a>
      <a href="/logout">Log out</a>
    </header>

    <div class="section">
      <h2 class="section-title">Download &amp; build cloud-init image</h2>
      <p style="font-size:0.9rem; color:var(--text-dim); margin-bottom:0.75rem;">Download latest Raspberry Pi OS (arm64), inject cloud-init. Output goes to <strong>Cloud-init images</strong> on the Admin page.</p>
      <div style="margin-bottom:0.5rem;">
        <label>Variant:</label>
        <select id="buildVariant"><option value="desktop" selected>Desktop (recommended)</option><option value="lite">Lite</option><option value="full">Full</option></select>
        <span id="buildRaspiosUrl" class="mono" style="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; margin-left:0.25rem;" />
        <span class="mono" style="font-size:0.85rem; margin-left:0.25rem;">+ date suffix</span>
      </div>
      <div style="margin-bottom:0.5rem;"><label><input type="checkbox" id="buildSetGolden" /> Set as golden after build</label></div>
      <div style="margin-bottom:0.75rem;">
        <button type="button" id="buildCloudInitBtn" class="btn btn-primary">Download &amp; build</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="mono" style="min-height:1.2em;"></div>
      <a href="#" id="buildCloudInitDismiss" style="display:none;">Dismiss</a>
    </div>

    <div class="section">
      <h2 class="section-title">Edit cloud-init files (templates)</h2>
      <p style="font-size:0.85rem; color:var(--text-dim); margin-bottom:0.75rem;">Choose a file to edit below. These contents are injected into the image when you run the build.</p>
      <div class="tabs">
        <button type="button" class="tab-btn active" data-tab="user-data">user-data</button>
        <button type="button" class="tab-btn" data-tab="meta-data">meta-data</button>
        <button type="button" class="tab-btn" data-tab="network-config">network-config</button>
      </div>
      <div id="pane-user-data" class="tab-pane active">
        <label>user-data (YAML, #cloud-config)</label>
        <textarea id="buildUserData" rows="12" placeholder="#cloud-config..."></textarea>
      </div>
      <div id="pane-meta-data" class="tab-pane">
        <label>meta-data (optional)</label>
        <textarea id="buildMetaData" rows="8" placeholder="instance-id: ..."></textarea>
      </div>
      <div id="pane-network-config" class="tab-pane">
        <label>network-config (optional)</label>
        <textarea id="buildNetworkConfig" rows="8" placeholder="version: 2..."></textarea>
      </div>
      <div style="margin-top:0.75rem;">
        <span>Templates:</span>
        <select id="buildTemplateSelect"><option value="">— Load —</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 as template…</button>
      </div>
      <ul id="buildTemplateList" style="margin-top:0.5rem; font-size:0.85rem;"></ul>
    </div>
  </div>

  <script>
    function authFetch(url, opts) {
      opts = opts || {};
      return fetch(url, opts).then(function(r) {
        if (r.status === 401) { window.location = '/login?next=' + encodeURIComponent(window.location.pathname); return Promise.reject(new Error('Login required')); }
        return r;
      });
    }
    function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; }

    document.querySelectorAll('.tab-btn').forEach(function(btn) {
      btn.onclick = function() {
        document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
        document.querySelectorAll('.tab-pane').forEach(function(p) { p.classList.remove('active'); });
        btn.classList.add('active');
        var tab = btn.getAttribute('data-tab');
        var pane = document.getElementById('pane-' + tab);
        if (pane) pane.classList.add('active');
      };
    });

    function fetchBuildStatus() {
      authFetch('/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');
        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 Admin page Cloud-init images.'; }
        else if (d.phase === 'error') el.textContent = 'Error: ' + (d.error || '');
        else if (d.phase === 'cancelled') {
          el.textContent = 'Build cancelled.';
          if (btn) btn.disabled = false;
          if (!window._buildClearScheduled) {
            window._buildClearScheduled = true;
            setTimeout(function() {
              authFetch('/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 cancelBuild() {
      var cancelBtn = document.getElementById('buildCloudInitCancelBtn');
      if (cancelBtn) cancelBtn.disabled = true;
      document.getElementById('buildCloudInitStatus').textContent = 'Cancelling…';
      authFetch('/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 failed'); if (cancelBtn) cancelBtn.disabled = false; }
      }).catch(function() { if (cancelBtn) cancelBtn.disabled = false; });
    }
    function startBuild() {
      var btn = document.getElementById('buildCloudInitBtn');
      if (btn) btn.disabled = true;
      var nameEl = document.getElementById('buildImageName');
      var body = { variant: document.getElementById('buildVariant').value, set_as_golden_after: document.getElementById('buildSetGolden').checked };
      if (nameEl && nameEl.value.trim()) body.image_name = nameEl.value.trim();
      body.user_data = document.getElementById('buildUserData').value.trim();
      body.meta_data = document.getElementById('buildMetaData').value.trim();
      body.network_config = document.getElementById('buildNetworkConfig').value.trim();
      authFetch('/api/build-cloudinit', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) { document.getElementById('buildCloudInitStatus').textContent = 'Build started…'; setTimeout(fetchBuildStatus, 2000); }
        else { alert(d.error); if (btn) btn.disabled = false; }
      }).catch(function() { if (btn) btn.disabled = false; });
    }
    function fetchTemplates() {
      authFetch('/api/cloudinit-templates').then(function(r) { return r.json(); }).then(function(d) {
        var list = d.templates || [];
        var sel = document.getElementById('buildTemplateSelect');
        sel.innerHTML = '<option value="">— Load —</option>';
        list.forEach(function(t) { var o = document.createElement('option'); o.value = t.id; o.textContent = t.name; sel.appendChild(o); });
        var ul = document.getElementById('buildTemplateList');
        ul.innerHTML = list.map(function(t) {
          return '<li>' + escapeHtml(t.name) + ' <button type="button" class="btn btn-outline btn-sm load-tpl" data-id="' + t.id + '">Load</button> <button type="button" class="btn btn-outline btn-sm upd-tpl" data-id="' + t.id + '" title="Save current content into this template">Update</button> <button type="button" class="btn btn-outline btn-sm del-tpl" data-id="' + t.id + '">Delete</button></li>';
        }).join('') || '<li>No templates</li>';
        ul.querySelectorAll('.load-tpl').forEach(function(b) {
          b.onclick = function() {
            authFetch('/api/cloudinit-templates/' + b.getAttribute('data-id')).then(function(r) { return r.json(); }).then(function(t) {
              document.getElementById('buildUserData').value = t.user_data || '';
              document.getElementById('buildMetaData').value = t.meta_data || '';
              document.getElementById('buildNetworkConfig').value = t.network_config || '';
            });
          };
        });
        ul.querySelectorAll('.upd-tpl').forEach(function(b) {
          b.onclick = function() { updateTemplate(b.getAttribute('data-id')); };
        });
        ul.querySelectorAll('.del-tpl').forEach(function(b) {
          b.onclick = function() {
            if (!confirm('Delete template?')) return;
            authFetch('/api/cloudinit-templates/' + b.getAttribute('data-id'), { method: 'DELETE' }).then(function(r) { return r.json(); }).then(function(d) { if (d.ok) fetchTemplates(); });
          };
        });
      }).catch(function() {});
    }
    function updateTemplate(tid) {
      var body = { user_data: document.getElementById('buildUserData').value, meta_data: document.getElementById('buildMetaData').value, network_config: document.getElementById('buildNetworkConfig').value };
      authFetch('/api/cloudinit-templates/' + encodeURIComponent(tid), { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) { fetchTemplates(); alert('Template updated'); } else alert(d.error || 'Update failed');
      }).catch(function() { alert('Update failed'); });
    }
    function loadTemplateFromSelect() {
      var s = document.getElementById('buildTemplateSelect');
      if (s && s.value) authFetch('/api/cloudinit-templates/' + s.value).then(function(r) { return r.json(); }).then(function(t) {
        document.getElementById('buildUserData').value = t.user_data || '';
        document.getElementById('buildMetaData').value = t.meta_data || '';
        document.getElementById('buildNetworkConfig').value = t.network_config || '';
      });
    }
    function saveTemplate() {
      var name = prompt('Template name');
      if (!name || !name.trim()) return;
      var body = { name: name.trim(), user_data: document.getElementById('buildUserData').value, meta_data: document.getElementById('buildMetaData').value, network_config: document.getElementById('buildNetworkConfig').value };
      authFetch('/api/cloudinit-templates', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }).then(function(r) { return r.json(); }).then(function(d) { if (d.ok) { fetchTemplates(); alert('Saved'); } else alert(d.error); });
    }

    document.getElementById('buildCloudInitBtn').onclick = startBuild;
    var cancelBuildBtn = document.getElementById('buildCloudInitCancelBtn');
    if (cancelBuildBtn) cancelBuildBtn.onclick = cancelBuild;
    var dismissBuildBtn = document.getElementById('buildCloudInitDismiss');
    if (dismissBuildBtn) dismissBuildBtn.onclick = function(e) { e.preventDefault(); authFetch('/api/build-cloudinit-status-clear?force=1', { method: 'POST', headers: {'Content-Type':'application/json'} }).then(function() { fetchBuildStatus(); }); };
    document.getElementById('buildVariant').onchange = function() {
      authFetch('/api/raspios-latest-url?variant=' + encodeURIComponent(document.getElementById('buildVariant').value)).then(function(r) { return r.json(); }).then(function(d) {
        document.getElementById('buildRaspiosUrl').textContent = (d.ok && d.filename) ? d.filename : (d.error || '');
      });
    };
    document.getElementById('buildTemplateLoad').onclick = loadTemplateFromSelect;
    document.getElementById('buildTemplateUpdate').onclick = function() {
      var s = document.getElementById('buildTemplateSelect');
      if (!s || !s.value) { alert('Select a template to update'); return; }
      updateTemplate(s.value);
    };
    document.getElementById('buildTemplateSave').onclick = saveTemplate;

    fetchBuildStatus();
    fetchTemplates();
    authFetch('/api/raspios-latest-url').then(function(r) { return r.json(); }).then(function(d) {
      document.getElementById('buildRaspiosUrl').textContent = (d.ok && d.filename) ? d.filename : (d.error || '');
    });
    setInterval(fetchBuildStatus, 15000);
  </script>
</body>
</html>
