<!DOCTYPE html>
<!-- Revision: 2 -->
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Portal files · 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; --danger: #f87171; --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: 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; }
    .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; display: flex; align-items: center; flex-wrap: wrap; gap: 0.5rem; }
    .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; }
    .btn-danger { background: rgba(248,113,113,0.2); color: var(--danger); }
    .btn-danger:hover { background: rgba(248,113,113,0.3); }
    table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
    th { text-align: left; font-weight: 600; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim); padding: 0.4rem 0; border-bottom: 1px solid var(--border); }
    td { padding: 0.5rem 0; border-bottom: 1px solid var(--border); vertical-align: middle; }
    tr:last-child td { border-bottom: none; }
    .mono { font-family: 'JetBrains Mono', monospace; font-size: 0.8em; color: var(--text-muted); }
    .empty-msg { text-align: center; padding: 1.5rem; font-size: 0.9rem; color: var(--text-muted); }
    .breadcrumb { margin-bottom: 0.75rem; font-size: 0.85rem; }
    .breadcrumb a { color: var(--text-dim); text-decoration: none; }
    .breadcrumb a:hover { color: var(--accent); }
    .breadcrumb span { color: var(--text-muted); margin: 0 0.25rem; }
    .desc-input { width: 100%; max-width: 280px; font-size: 0.8rem; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.3rem 0.5rem; border-radius: 4px; }
    .folder-name { font-weight: 500; }
    input[type="text"] { background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text); padding: 0.35rem 0.5rem; border-radius: 4px; font: inherit; }
    /* Editor modal */
    .editor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 1000; display: none; align-items: center; justify-content: center; padding: 1rem; }
    .editor-overlay.visible { display: flex; }
    .editor-modal { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); width: 100%; max-width: 900px; max-height: 90vh; display: flex; flex-direction: column; box-shadow: 0 8px 32px rgba(0,0,0,0.4); }
    .editor-header { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
    .editor-title { font-family: 'JetBrains Mono', monospace; font-size: 0.9rem; color: var(--text); }
    .editor-actions { display: flex; gap: 0.5rem; }
    .editor-body { flex: 1; min-height: 400px; overflow: hidden; }
    #editorHost { width: 100%; height: 100%; min-height: 420px; }
  </style>
</head>
<body>
  <div class="wrap">
    <header class="header">
      <div>
        <h1>Portal files</h1>
        <nav class="nav" style="margin-top:0.35rem;">
          <a href="/admin">Admin</a>
          <a href="/admin/portal-files" class="active">Portal files</a>
          <a href="/admin/cloudinit-build">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">
      <div class="section-title">
        <span id="breadcrumb">Portal files</span>
        <button type="button" class="btn btn-outline btn-sm" id="newFolderBtn">New folder</button>
        <button type="button" class="btn btn-outline btn-sm" id="uploadBtn">Upload file</button>
        <input type="file" id="uploadInput" style="display:none;" />
      </div>
      <p class="mono" style="font-size:0.8rem; margin-bottom:0.5rem;">Served at <strong id="baseUrl">/files/</strong> — use in cloud-init e.g. <code>curl -fsSL "http://SERVER/files/first-boot/splash.png" -o /tmp/splash.png</code></p>
      <p class="mono" style="font-size:0.75rem; color:var(--text-muted); margin-bottom:0.75rem;">Directory on server: <strong id="portalDir">—</strong></p>
      <table>
        <thead><tr><th>Name</th><th>Type</th><th>Size</th><th>Description</th><th>Actions</th></tr></thead>
        <tbody id="portalBody"></tbody>
      </table>
      <p id="portalEmpty" class="empty-msg" style="display:none;">No files or folders. Create a folder or upload a file.</p>
    </div>

    <div id="editorOverlay" class="editor-overlay">
      <div class="editor-modal">
        <div class="editor-header">
          <span id="editorTitle" class="editor-title">—</span>
          <div class="editor-actions">
            <button type="button" class="btn btn-primary btn-sm" id="editorSaveBtn">Save</button>
            <button type="button" class="btn btn-outline btn-sm" id="editorCancelBtn">Cancel</button>
          </div>
        </div>
        <div class="editor-body">
          <div id="editorHost"></div>
        </div>
      </div>
    </div>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.0/ace.min.js" crossorigin="anonymous"></script>
  <script>
    function authFetch(url, opts) {
      opts = opts || {};
      opts.credentials = opts.credentials || 'same-origin';
      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; }
    function fmtSize(n) { if (!n) return '—'; 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 ts ? new Date(ts*1000).toLocaleString() : '—'; }

    var currentPath = '';
    var descriptions = {};

    function buildBreadcrumb() {
      var el = document.getElementById('breadcrumb');
      var parts = currentPath ? currentPath.split('/') : [];
      var html = '<a href="#" data-path="">Portal files</a>';
      parts.forEach(function(p, i) {
        var path = parts.slice(0, i+1).join('/');
        html += ' <span>/</span> <a href="#" data-path="' + escapeHtml(path) + '">' + escapeHtml(p) + '</a>';
      });
      el.innerHTML = html;
      el.querySelectorAll('a[data-path]').forEach(function(a) {
        a.onclick = function(e) { e.preventDefault(); currentPath = a.getAttribute('data-path') || ''; fetchPortal(); };
      });
    }

    function saveDescription(path, desc) {
      descriptions[path] = desc;
      authFetch('/api/portal-files/descriptions', { method: 'PATCH', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ descriptions: descriptions }) }).then(function(r) { return r.json(); }).catch(function() {});
    }

    function renderPortal(data) {
      var items = data.items || [];
      var baseUrl = data.base_url || '';
      descriptions = data.descriptions || {};
      currentPath = data.current_path || '';
      document.getElementById('baseUrl').textContent = baseUrl + (currentPath ? currentPath + '/' : '');
      document.getElementById('portalDir').textContent = data.portal_files_dir || '—';
      buildBreadcrumb();

      var tbody = document.getElementById('portalBody');
      var empty = document.getElementById('portalEmpty');
      tbody.innerHTML = '';
      if (items.length === 0) { empty.style.display = 'block'; return; }
      empty.style.display = 'none';

      items.forEach(function(it) {
        var tr = document.createElement('tr');
        var desc = descriptions[it.path] || '';
        var typeLabel = it.type === 'folder' ? 'Folder' : 'File';
        var sizeCell = it.type === 'file' ? '<td class="mono">' + fmtSize(it.size) + '</td>' : '<td class="mono">—</td>';
        var descCell = '<td><input type="text" class="desc-input" data-path="' + escapeHtml(it.path) + '" value="' + escapeHtml(desc) + '" placeholder="Comment…" /></td>';
        var actions = '';
        if (it.type === 'folder') {
          actions = '<button type="button" class="btn btn-outline btn-sm open-folder" data-path="' + escapeHtml(it.path) + '">Open</button> <button type="button" class="btn btn-danger btn-sm delete-item" data-path="' + escapeHtml(it.path) + '" data-type="folder">Delete</button>';
        } else {
          actions = '<button type="button" class="btn btn-outline btn-sm edit-file" data-path="' + escapeHtml(it.path) + '">Edit</button> <a href="' + escapeHtml(baseUrl + it.path) + '" target="_blank" rel="noopener" class="btn btn-outline btn-sm">Open</a> <button type="button" class="btn btn-danger btn-sm delete-item" data-path="' + escapeHtml(it.path) + '" data-type="file">Delete</button>';
        }
        tr.innerHTML = '<td class="' + (it.type === 'folder' ? 'folder-name' : '') + '">' + escapeHtml(it.name) + (it.type === 'folder' ? ' /' : '') + '</td><td>' + typeLabel + '</td>' + sizeCell + descCell + '<td class="actions-cell">' + actions + '</td>';
        tbody.appendChild(tr);
      });

      tbody.querySelectorAll('.desc-input').forEach(function(inp) {
        inp.onblur = function() { saveDescription(inp.getAttribute('data-path'), inp.value.trim()); };
      });
      tbody.querySelectorAll('.open-folder').forEach(function(btn) {
        btn.onclick = function() { currentPath = btn.getAttribute('data-path'); fetchPortal(); };
      });
      tbody.querySelectorAll('.edit-file').forEach(function(btn) {
        btn.onclick = function() { openEditor(btn.getAttribute('data-path')); };
      });
      tbody.querySelectorAll('.delete-item').forEach(function(btn) {
        btn.onclick = function() {
          var path = btn.getAttribute('data-path');
          var type = btn.getAttribute('data-type');
          if (!confirm('Delete ' + type + ' "' + path + '"?')) return;
          authFetch('/api/portal-files/' + encodeURIComponent(path), { method: 'DELETE' }).then(function(r) { return r.json(); }).then(function(d) {
            if (d.ok) fetchPortal(); else alert(d.error || 'Delete failed');
          });
        };
      });
    }

    function fetchPortal() {
      var url = '/api/portal-files';
      if (currentPath) url += (url.indexOf('?') >= 0 ? '&' : '?') + 'path=' + encodeURIComponent(currentPath);
      authFetch(url).then(function(r) { return r.json(); }).then(renderPortal).catch(function(err) {
        document.getElementById('portalEmpty').style.display = 'block';
        document.getElementById('portalEmpty').textContent = 'Could not load list (session may have expired). Trying read-only list…';
        var fallbackUrl = '/api/portal-files?debug=1' + (currentPath ? '&path=' + encodeURIComponent(currentPath) : '');
        fetch(fallbackUrl).then(function(r) { return r.json(); }).then(function(data) {
          if (data.items && data.items.length) {
            document.getElementById('portalEmpty').style.display = 'none';
            renderPortal(data);
            document.getElementById('portalDir').textContent = (data.portal_files_dir || '—') + ' (read-only; log in to edit)';
          } else {
            document.getElementById('portalEmpty').textContent = 'Server sees ' + (data.items ? data.items.length : 0) + ' item(s) at ' + (data.portal_files_dir || '?') + '. Log in to see and edit.';
          }
        }).catch(function() {
          document.getElementById('portalEmpty').textContent = 'Could not load list. Log out and log in again, then refresh.';
        });
      });
    }

    document.getElementById('newFolderBtn').onclick = function() {
      var name = prompt('Folder name (no slashes)');
      if (!name || !name.trim()) return;
      name = name.trim().replace(/[/\\]/g, '');
      var path = currentPath ? currentPath + '/' + name : name;
      authFetch('/api/portal-files/folder', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ path: path }) }).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) fetchPortal(); else alert(d.error || 'Failed');
      });
    };
    document.getElementById('uploadBtn').onclick = function() { document.getElementById('uploadInput').click(); };
    document.getElementById('uploadInput').onchange = function() {
      var f = this.files && this.files[0];
      if (!f) return;
      var fd = new FormData();
      fd.append('file', f);
      if (currentPath) fd.append('path', currentPath);
      authFetch('/api/portal-files/upload', { method: 'POST', body: fd }).then(function(r) { return r.json(); }).then(function(d) {
        if (d.ok) { fetchPortal(); alert('Uploaded. URL: ' + d.url); } else alert(d.error || 'Upload failed');
      });
      this.value = '';
    };

    var editorInstance = null;
    var editorCurrentPath = null;
    var aceModeMap = { sh: 'sh', json: 'json', python: 'python', yaml: 'yaml', markdown: 'markdown', ini: 'ini', plain_text: 'plain_text' };

    function initAce() {
      if (editorInstance) return editorInstance;
      var host = document.getElementById('editorHost');
      editorInstance = ace.edit(host);
      editorInstance.setTheme('ace/theme/tomorrow_night_eighties');
      editorInstance.setOptions({ fontSize: '13px', showPrintMargin: false, wrap: true, useSoftTabs: true, tabSize: 2 });
      editorInstance.session.setUseWorker(false);
      return editorInstance;
    }

    function openEditor(path) {
      editorCurrentPath = path;
      document.getElementById('editorTitle').textContent = path;
      document.getElementById('editorOverlay').classList.add('visible');
      authFetch('/api/portal-files/content?path=' + encodeURIComponent(path)).then(function(r) { return r.json(); }).then(function(d) {
        if (!d.ok) { alert(d.error || 'Could not load file'); closeEditor(); return; }
        var ace = initAce();
        ace.session.setValue(d.content || '');
        var mode = aceModeMap[d.language] || 'plain_text';
        ace.session.setMode('ace/mode/' + mode);
        ace.focus();
      }).catch(function() { alert('Could not load file'); closeEditor(); });
    }

    function closeEditor() {
      editorCurrentPath = null;
      document.getElementById('editorOverlay').classList.remove('visible');
    }

    document.getElementById('editorSaveBtn').onclick = function() {
      if (!editorCurrentPath) return;
      var content = editorInstance.getSession().getValue();
      authFetch('/api/portal-files/content', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: editorCurrentPath, content: content }) })
        .then(function(r) { return r.json(); })
        .then(function(d) { if (d.ok) { closeEditor(); fetchPortal(); } else alert(d.error || 'Save failed'); })
        .catch(function() { alert('Save failed'); });
    };
    document.getElementById('editorCancelBtn').onclick = closeEditor;
    document.getElementById('editorOverlay').onclick = function(e) { if (e.target === document.getElementById('editorOverlay')) closeEditor(); };

    fetchPortal();
  </script>
</body>
</html>
