package handler

import (
	"html/template"
	"net/http"

	"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
	"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)

// profileView is the data passed to the profile template's HTML shell.
//
// Only cheap-to-compute fields appear here — Email comes from the request
// context, IsSuperAdmin reads the root .zddc only (single file + ACL chain
// cache hit), and HasCustomCSS is a single stat call. Everything else
// (visible projects, admin subtrees, editable scaffolds) is fetched lazily
// by the page's JS via /.profile/access after first paint, so the slow
// .zddc tree walk doesn't block the initial render. See AccessView and
// enumerateAccess for the JSON contract the client renders against.
type profileView struct {
	Email             string
	EmailHeader       string
	IsSuperAdmin      bool
	ProfilePathPrefix string
	AssetsPathPrefix  string
	HasCustomCSS      bool
}

// serveProfilePage renders the universal profile page at GET /.profile/.
// Reachable to anyone (anonymous included). The shell is intentionally
// minimal: identity card + theme + localStorage + super-admin diagnostic
// scaffolds (gated by the cheap IsSuperAdmin check) + a hidden
// <template id="tmpl-subtree-admin"> block. The client IIFE fetches
// /.profile/access on load and reveals the subtree-admin block when the
// caller has any subtree-admin scope. Pure non-admins receive no live
// admin form, button handler, or fetch URL — admin functionality only
// ever activates after enumerateAccess has confirmed the caller's scope.
func serveProfilePage(cfg config.Config, w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		w.Header().Set("Allow", "GET")
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}
	email := EmailFromContext(r)
	view := profileView{
		Email:             email,
		EmailHeader:       cfg.EmailHeader,
		IsSuperAdmin:      zddc.IsAdmin(cfg.Root, email),
		ProfilePathPrefix: ProfilePathPrefix,
		AssetsPathPrefix:  zddcAssetsPathPrefix,
		HasCustomCSS:      hasCustomProfileCSS(cfg.Root),
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-store")
	if err := profileTemplate.Execute(w, view); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

// profileTemplate is the html/template for the profile page. The shell is
// rendered server-side from cheap-only data (identity + IsSuperAdmin); the
// expensive bits (visible projects, admin subtrees, editable .zddc files,
// create-project parent choices) are populated by the IIFE below after a
// single fetch to /.profile/access. Subtree-admin scaffolds live inside a
// <template id="tmpl-subtree-admin"> so non-admins never receive the live
// form markup. Inline styles use the same custom-property naming as the
// editor so a future merge with shared/base.css stays trivial.
var profileTemplate = template.Must(template.New("profile").Parse(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>zddc-server — profile</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root {
    --bg: #fff; --bg-alt: #f7f7f8; --text: #222; --muted: #666;
    --border: #d0d4dc; --primary: #2563eb; --primary-bg: #eff6ff;
    --danger: #b00020; --warn: #b15c00; --ok: #0a7d2c;
    --radius: 4px;
  }
  @media (prefers-color-scheme: dark) {
    :root {
      --bg: #1a1c1f; --bg-alt: #23262b; --text: #e8e8ea; --muted: #a0a4ad;
      --border: #353941; --primary: #60a5fa; --primary-bg: #1e293b;
      --danger: #ff7080; --warn: #f5b056; --ok: #4ad27c;
    }
  }
  [data-theme="light"] {
    --bg: #fff; --bg-alt: #f7f7f8; --text: #222; --muted: #666;
    --border: #d0d4dc; --primary: #2563eb; --primary-bg: #eff6ff;
    --danger: #b00020; --warn: #b15c00; --ok: #0a7d2c;
  }
  [data-theme="dark"] {
    --bg: #1a1c1f; --bg-alt: #23262b; --text: #e8e8ea; --muted: #a0a4ad;
    --border: #353941; --primary: #60a5fa; --primary-bg: #1e293b;
    --danger: #ff7080; --warn: #f5b056; --ok: #4ad27c;
  }
  body { font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; margin: 1.5rem; color: var(--text); background: var(--bg); max-width: 980px; }
  h1 { margin: 0 0 .25rem; font-size: 1.4rem; }
  h2 { margin: 1.5rem 0 .25rem; font-size: 1.05rem; }
  .muted { color: var(--muted); }
  .breadcrumb { color: var(--muted); margin-bottom: 1rem; }
  .breadcrumb a { color: var(--primary); text-decoration: none; }
  .breadcrumb a:hover { text-decoration: underline; }
  .card { background: var(--bg-alt); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1rem; }
  .card h2 { margin-top: 0; }
  .card .help { color: var(--muted); font-size: .9em; margin: .3rem 0 .6rem; }
  label { display: block; margin-bottom: .5rem; }
  input[type="text"], select { width: 100%; max-width: 32rem; padding: .35rem .5rem; font: inherit; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); box-sizing: border-box; }
  input[type="text"]:focus, select:focus { outline: 2px solid var(--primary); outline-offset: -1px; }
  .row { display: flex; gap: .5rem; align-items: center; margin-bottom: .35rem; }
  .row input[type="text"] { flex: 1; max-width: none; }
  .row .err { color: var(--danger); font-size: .85em; margin-left: .5rem; }
  button { font: inherit; padding: .35rem .85rem; cursor: pointer; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: var(--radius); }
  button:hover { background: var(--primary-bg); }
  button.primary { background: var(--primary); color: white; border-color: var(--primary); }
  button.primary:hover { filter: brightness(1.1); }
  button.danger { color: var(--danger); border-color: var(--danger); }
  button.danger:hover { background: rgba(176, 0, 32, 0.06); }
  table { width: 100%; border-collapse: collapse; font-size: .95em; }
  th, td { text-align: left; padding: .35rem .5rem; border-bottom: 1px solid var(--border); }
  th { color: var(--muted); font-weight: 500; }
  td.value { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; word-break: break-all; }
  td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
  ul.bare { list-style: none; padding: 0; margin: 0; }
  ul.bare li { padding: .2rem 0; }
  .badge { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: .8em; border: 1px solid var(--border); background: var(--bg); }
  .badge.yes { background: var(--primary-bg); border-color: var(--primary); color: var(--primary); }
  .ok-banner { background: var(--primary-bg); border: 1px solid var(--primary); border-radius: var(--radius); padding: .55rem .85rem; margin-bottom: 1rem; color: var(--text); }
  pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); padding: .5rem .7rem; margin: .35rem 0; overflow-x: auto; font-size: 12px; max-height: 20rem; }
  pre.err { border-color: var(--danger); color: var(--danger); }
  code { background: var(--bg); padding: 0 .25rem; border-radius: 2px; font-size: 12px; border: 1px solid var(--border); }
  .theme-pick label { display: inline-flex; align-items: center; gap: .25rem; margin-right: 1rem; }
  .ls-actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
</style>
{{ if .HasCustomCSS }}<link rel="stylesheet" href="{{ .AssetsPathPrefix }}/custom.css">{{ end }}
</head>
<body>
<div class="breadcrumb"><a href="/">&larr; home</a> &nbsp;/&nbsp; <span>profile</span></div>

<h1>Your profile</h1>

<section class="card">
  <h2>Identity</h2>
  {{ if .Email }}
    <p>Signed in as <code>{{ .Email }}</code>.</p>
  {{ else }}
    <p>Not signed in. The server reads identity from the <code>{{ .EmailHeader }}</code> header. If you expected to be authenticated, your reverse proxy or SSO gateway is not forwarding it.</p>
  {{ end }}
  <p class="muted">Configured email header: <code>{{ .EmailHeader }}</code></p>
</section>

<section class="card">
  <h2>Effective access</h2>
  <p>
    Super-admin: {{ if .IsSuperAdmin }}<span class="badge yes">yes</span>{{ else }}<span class="badge">no</span>{{ end }}
  </p>
  <h3 style="font-size: 1em; margin: .8rem 0 .3rem;">Visible projects</h3>
  <div id="projects-list"><p class="muted" id="projects-loading">loading…</p></div>
  <div id="admin-subtrees-block" hidden>
    <h3 style="font-size: 1em; margin: .8rem 0 .3rem;">Subtrees you administer</h3>
    <div id="admin-subtrees-list"></div>
  </div>
</section>

<section class="card">
  <h2>Theme</h2>
  <p class="help">Applies to every ZDDC tool you open in this browser. Stored in <code>localStorage["zddc-theme"]</code>.</p>
  <div class="theme-pick">
    <label><input type="radio" name="theme" value="auto"> auto (system)</label>
    <label><input type="radio" name="theme" value="light"> light</label>
    <label><input type="radio" name="theme" value="dark"> dark</label>
  </div>
</section>

<section class="card">
  <h2>Local storage</h2>
  <p class="help">Browser-side state used by the ZDDC tools at this origin. The profile page can read and write it for you.</p>
  <table id="ls-table"><thead><tr><th>Key</th><th>Value</th><th class="numeric">Bytes</th></tr></thead><tbody></tbody></table>
  <div class="ls-actions">
    <button type="button" id="ls-export">Export all (JSON)</button>
    <button type="button" id="ls-import">Import from JSON…</button>
    <input type="file" id="ls-import-file" accept="application/json,.json" hidden>
    <button type="button" id="ls-clear" class="danger">Clear all</button>
  </div>
</section>

<div id="subtree-admin-slot"></div>

<template id="tmpl-subtree-admin">
<section class="card">
  <h2>Editable .zddc files</h2>
  <p class="help">Open the form-based editor for any subtree you administer.</p>
  <div id="editable-list"></div>
</section>

<section class="card">
  <h2>Create new project folder</h2>
  <p class="help">Creates a directory under the chosen parent. If you fill in any of title / allow / deny / admins, a starter <code>.zddc</code> is also written; otherwise the directory is empty and inherits ACL from its ancestors.</p>
  <div id="cp-ok" class="ok-banner" hidden>Created.</div>
  <form id="cp-form" autocomplete="off">
    <label>Parent
      <select name="parent" id="cp-parent"></select>
    </label>
    <label>Name
      <input type="text" name="name" id="cp-name" maxlength="64" placeholder="e.g. Site-3" required>
      <span class="err" id="cp-name-err"></span>
    </label>
    <label>Title (optional)
      <input type="text" name="title" id="cp-title" maxlength="200">
    </label>
    <h3 style="font-size: 1em; margin: .8rem 0 .3rem;">ACL — Allow (optional)</h3>
    <div class="list" data-field="acl.allow"></div>
    <button type="button" class="add" data-target="acl.allow">+ Add allow rule</button>
    <h3 style="font-size: 1em; margin: .8rem 0 .3rem;">ACL — Deny (optional)</h3>
    <div class="list" data-field="acl.deny"></div>
    <button type="button" class="add" data-target="acl.deny">+ Add deny rule</button>
    <h3 style="font-size: 1em; margin: .8rem 0 .3rem;">Admins (optional)</h3>
    <div class="list" data-field="admins"></div>
    <button type="button" class="add" data-target="admins">+ Add admin</button>
    <div style="margin-top: 1rem;">
      <button type="submit" class="primary">Create</button>
    </div>
  </form>
</section>
</template>

{{ if .IsSuperAdmin }}
<section class="card">
  <h2>Server config <button type="button" data-diag="config">refresh</button></h2>
  <p class="help">Effective config from environment variables. Read-only.</p>
  <pre id="diag-config">loading…</pre>
</section>

<section class="card">
  <h2>Logs <button type="button" data-diag="logs">refresh</button>
    <span class="muted">level: <select id="diag-level"><option value="">all</option><option value="debug">debug+</option><option value="info" selected>info+</option><option value="warn">warn+</option><option value="error">error</option></select></span>
  </h2>
  <pre id="diag-logs">loading…</pre>
</section>

<section class="card">
  <h2>whoami <button type="button" data-diag="whoami">refresh</button></h2>
  <p class="help">Headers as they arrived at the binary. Useful for debugging SSO header passthrough.</p>
  <pre id="diag-whoami">loading…</pre>
</section>
{{ end }}

<script>
(function() {
  var prefix = {{ .ProfilePathPrefix }};
  var isSuper = {{ .IsSuperAdmin }};

  function escText(s) {
    var d = document.createElement("div");
    d.textContent = s == null ? "" : String(s);
    return d.innerHTML;
  }

  // ── Theme ──────────────────────────────────────────────────────────────
  var THEME_KEY = "zddc-theme";
  function applyTheme(v) {
    if (v === "light" || v === "dark") {
      document.documentElement.setAttribute("data-theme", v);
    } else {
      document.documentElement.removeAttribute("data-theme");
    }
  }
  var current = (function() { try { return localStorage.getItem(THEME_KEY) || "auto"; } catch (e) { return "auto"; } })();
  applyTheme(current);
  document.querySelectorAll('input[name="theme"]').forEach(function(r) {
    if (r.value === current) r.checked = true;
    r.addEventListener("change", function() {
      try { localStorage.setItem(THEME_KEY, r.value); } catch (e) {}
      applyTheme(r.value);
    });
  });

  // ── Local storage panel ────────────────────────────────────────────────
  function bytes(s) { return new TextEncoder().encode(s).length; }
  function renderLS() {
    var tbody = document.querySelector("#ls-table tbody");
    tbody.textContent = "";
    var keys = [];
    for (var i = 0; i < localStorage.length; i++) keys.push(localStorage.key(i));
    keys.sort();
    if (keys.length === 0) {
      var tr = document.createElement("tr");
      var td = document.createElement("td");
      td.colSpan = 3; td.className = "muted"; td.textContent = "(empty)";
      tr.appendChild(td); tbody.appendChild(tr);
      return;
    }
    keys.forEach(function(k) {
      var v = localStorage.getItem(k) || "";
      var tr = document.createElement("tr");
      var tdK = document.createElement("td"); tdK.textContent = k;
      var tdV = document.createElement("td"); tdV.className = "value";
      tdV.textContent = v.length > 200 ? v.slice(0, 200) + "…" : v;
      var tdB = document.createElement("td"); tdB.className = "numeric"; tdB.textContent = bytes(v);
      tr.appendChild(tdK); tr.appendChild(tdV); tr.appendChild(tdB);
      tbody.appendChild(tr);
    });
  }
  renderLS();

  document.getElementById("ls-export").addEventListener("click", function() {
    var dump = {};
    for (var i = 0; i < localStorage.length; i++) {
      var k = localStorage.key(i);
      dump[k] = localStorage.getItem(k);
    }
    var blob = new Blob([JSON.stringify(dump, null, 2)], { type: "application/json" });
    var a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "zddc-localstorage.json";
    a.click();
    URL.revokeObjectURL(a.href);
  });

  document.getElementById("ls-import").addEventListener("click", function() {
    document.getElementById("ls-import-file").click();
  });
  document.getElementById("ls-import-file").addEventListener("change", function(ev) {
    var file = ev.target.files && ev.target.files[0];
    if (!file) return;
    var reader = new FileReader();
    reader.onload = function() {
      try {
        var parsed = JSON.parse(String(reader.result || ""));
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
          alert("Import file must be a JSON object of string→string."); return;
        }
        Object.keys(parsed).forEach(function(k) {
          var v = parsed[k];
          if (typeof v === "string") localStorage.setItem(k, v);
        });
        renderLS();
        alert("Imported " + Object.keys(parsed).length + " keys.");
      } catch (e) {
        alert("Import failed: " + e);
      }
    };
    reader.readAsText(file);
    ev.target.value = "";
  });

  document.getElementById("ls-clear").addEventListener("click", function() {
    if (!confirm("Clear ALL localStorage at this origin? This includes theme and any saved presets.")) return;
    localStorage.clear();
    renderLS();
    applyTheme("auto");
    document.querySelectorAll('input[name="theme"]').forEach(function(r) { r.checked = (r.value === "auto"); });
  });

  // ── Lazy access view ──────────────────────────────────────────────────
  // Fetch /.profile/access and populate the projects + admin-subtree
  // sections after first paint. The slow .zddc tree walk happens here, off
  // the request hot path. Subtree-admin scaffolds are cloned from the
  // <template> only if the response shows the caller has any admin scope —
  // pure non-admins never see live admin form markup.
  function renderProjects(projects) {
    var host = document.getElementById("projects-list");
    if (!projects || projects.length === 0) {
      host.innerHTML = '<p class="muted">No projects accessible.</p>';
      return;
    }
    var html = '<ul class="bare">';
    projects.forEach(function(p) {
      var label = p.title ? escText(p.title) : escText(p.name);
      html += '<li><a href="' + escText(p.url) + '">' + label + '</a> '
            + '<span class="muted">(' + escText(p.url) + ')</span></li>';
    });
    html += '</ul>';
    host.innerHTML = html;
  }

  function renderAdminSubtrees(subtrees) {
    if (!subtrees || subtrees.length === 0) return;
    document.getElementById("admin-subtrees-block").hidden = false;
    var host = document.getElementById("admin-subtrees-list");
    var html = '<ul class="bare">';
    subtrees.forEach(function(s) {
      html += '<li><code>' + escText(s.path) + '</code>';
      if (s.title) html += ' — ' + escText(s.title);
      if (s.can_edit) {
        html += ' <span class="muted">(editable)</span>';
      } else {
        html += ' <span class="muted">(read-only — you cannot edit the file granting your own authority)</span>';
      }
      html += '</li>';
    });
    html += '</ul>';
    host.innerHTML = html;
  }

  function renderEditableList(parents, hasAnyAdminScope) {
    var host = document.getElementById("editable-list");
    if (!host) return;
    if (!parents || parents.length === 0) {
      host.innerHTML = '<p class="muted">No <code>.zddc</code> files within your edit authority. Subtree admins cannot edit the file that grants their own authority — only an admin from a higher level can.</p>';
      return;
    }
    var html = '<ul class="bare">';
    parents.forEach(function(p) {
      var path = escText(p.path);
      html += '<li><a href="' + escText(prefix) + '/zddc/edit?path=' + path + '">'
            + '<code>' + path + '/.zddc</code></a>';
      if (p.title) html += ' — ' + escText(p.title);
      html += '</li>';
    });
    html += '</ul>';
    host.innerHTML = html;
  }

  function populateParentChoices(adminSubtrees) {
    var sel = document.getElementById("cp-parent");
    if (!sel) return;
    sel.innerHTML = "";
    if (isSuper) {
      var optRoot = document.createElement("option");
      optRoot.value = "/"; optRoot.textContent = "/ (root)";
      sel.appendChild(optRoot);
    }
    (adminSubtrees || []).forEach(function(s) {
      var opt = document.createElement("option");
      opt.value = s.path; opt.textContent = s.path;
      sel.appendChild(opt);
    });
  }

  function rowFor(field) {
    var div = document.createElement("div"); div.className = "row";
    var input = document.createElement("input");
    input.type = "text"; input.dataset.field = field;
    var del = document.createElement("button");
    del.type = "button"; del.textContent = "−"; del.className = "del";
    div.appendChild(input); div.appendChild(del);
    return div;
  }
  function collectList(field) {
    var out = [];
    document.querySelectorAll('#cp-form .list[data-field="' + field + '"] input').forEach(function(i) {
      if (i.value.trim()) out.push(i.value.trim());
    });
    return out;
  }
  function wireCreateProjectForm() {
    document.querySelectorAll("#cp-form button.add").forEach(function(btn) {
      btn.addEventListener("click", function() {
        var field = btn.dataset.target;
        document.querySelector('#cp-form .list[data-field="' + field + '"]').appendChild(rowFor(field));
      });
    });
    document.getElementById("cp-form").addEventListener("click", function(e) {
      if (e.target && e.target.classList && e.target.classList.contains("del")) {
        e.target.closest(".row").remove();
      }
    });
    document.getElementById("cp-form").addEventListener("submit", function(ev) {
      ev.preventDefault();
      document.getElementById("cp-name-err").textContent = "";
      document.getElementById("cp-ok").hidden = true;
      var allow = collectList("acl.allow");
      var deny  = collectList("acl.deny");
      var admins = collectList("admins");
      var title = document.getElementById("cp-title").value.trim();
      var body = {
        parent: document.getElementById("cp-parent").value,
        name:   document.getElementById("cp-name").value.trim()
      };
      if (title)        body.title = title;
      if (allow.length || deny.length) body.acl = { allow: allow, deny: deny };
      if (admins.length) body.admins = admins;
      fetch(prefix + "/projects", {
        method: "POST",
        headers: { "Content-Type": "application/json", "Accept": "application/json" },
        credentials: "same-origin",
        body: JSON.stringify(body)
      }).then(function(r) {
        return r.text().then(function(t) { return { ok: r.ok, status: r.status, text: t }; });
      }).then(function(res) {
        if (res.ok) {
          var parsed = {};
          try { parsed = JSON.parse(res.text); } catch (e) {}
          var ok = document.getElementById("cp-ok");
          ok.hidden = false;
          ok.textContent = "Created " + (parsed.path || "(unknown path)") + ". Reload to see it in the lists above.";
          return;
        }
        try {
          var p = JSON.parse(res.text);
          if (p && p.errors && p.errors.length) {
            document.getElementById("cp-name-err").textContent = p.errors.map(function(e) { return e.field + ": " + e.message; }).join("; ");
            return;
          }
        } catch (e) {}
        document.getElementById("cp-name-err").textContent = "HTTP " + res.status + ": " + res.text;
      });
    });
  }

  function instantiateAdminScaffold(view) {
    if (!view.has_any_admin_scope) return;
    var tmpl = document.getElementById("tmpl-subtree-admin");
    if (!tmpl) return;
    var slot = document.getElementById("subtree-admin-slot");
    slot.appendChild(tmpl.content.cloneNode(true));
    renderEditableList(view.editable_parent_choices, view.has_any_admin_scope);
    populateParentChoices(view.admin_subtrees);
    wireCreateProjectForm();
  }

  fetch(prefix + "/access", { headers: { Accept: "application/json" }, credentials: "same-origin" })
    .then(function(r) { return r.ok ? r.json() : null; })
    .then(function(view) {
      if (!view) {
        document.getElementById("projects-loading").textContent = "Could not load access view.";
        return;
      }
      renderProjects(view.projects);
      renderAdminSubtrees(view.admin_subtrees);
      instantiateAdminScaffold(view);
    })
    .catch(function() {
      document.getElementById("projects-loading").textContent = "Could not load access view.";
    });

  // ── Super-admin diagnostics ───────────────────────────────────────────
  if (isSuper) {
    function loadDiag(target, qs) {
      var el = document.getElementById("diag-" + target);
      el.classList.remove("err");
      el.textContent = "loading…";
      var url = prefix + "/" + target + (qs ? ("?" + qs) : "");
      fetch(url, { headers: { Accept: "application/json" }, credentials: "same-origin" })
        .then(function(r) { return r.text().then(function(t) { return { ok: r.ok, status: r.status, text: t }; }); })
        .then(function(res) {
          if (!res.ok) {
            el.classList.add("err");
            el.textContent = "HTTP " + res.status + "\n\n" + res.text;
            return;
          }
          try { el.textContent = JSON.stringify(JSON.parse(res.text), null, 2); }
          catch (e) { el.textContent = res.text; }
        }).catch(function(err) { el.classList.add("err"); el.textContent = String(err); });
    }
    document.querySelectorAll("button[data-diag]").forEach(function(b) {
      b.addEventListener("click", function() {
        var t = b.dataset.diag;
        var qs = t === "logs" ? ("level=" + (document.getElementById("diag-level").value || "")) : "";
        loadDiag(t, qs);
      });
    });
    var lvl = document.getElementById("diag-level");
    if (lvl) lvl.addEventListener("change", function() { loadDiag("logs", "level=" + (lvl.value || "")); });
    loadDiag("config");
    loadDiag("logs", "level=info");
    loadDiag("whoami");
  }
})();
</script>
</body>
</html>
`))
