"use strict";
const SESSION =
new URLSearchParams(location.hash.replace(/^#/, "")).get("session") || "";
const H = { "x-kovra-session": SESSION };
const $ = (sel) => document.querySelector(sel);
const el = (tag, attrs = {}, html) => {
const n = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) n.setAttribute(k, v);
if (html !== undefined) n.innerHTML = html;
return n;
};
const esc = (s) =>
String(s).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
);
const projectOf = (origin) =>
origin && origin.startsWith("project:") ? origin.slice(8) : null;
let table = null;
async function api(path, opts = {}) {
const r = await fetch(path, {
...opts,
headers: { ...H, ...(opts.headers || {}) },
});
let body = null;
try { body = await r.json(); } catch (_) { }
return { ok: r.ok, status: r.status, body };
}
function toast(msg, kind = "ok") {
const t = el("div", { class: `toast ${kind}` }, esc(msg));
$("#toasts").appendChild(t);
setTimeout(() => t.classList.add("in"), 10);
setTimeout(() => { t.classList.remove("in"); setTimeout(() => t.remove(), 250); }, 3200);
}
const svgIcon = (paths) =>
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
const ICON_EYE = svgIcon(
'<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"/><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"/>',
);
const ICON_EYE_OFF = svgIcon(
'<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828"/><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9 9 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"/><path d="M3 3l18 18"/>',
);
const ICON_COPY = svgIcon(
'<path d="M7 9.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z"/><path d="M4.012 16.737a2 2 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1"/>',
);
const ICON_PASTE = svgIcon(
'<path d="M9 5h-2a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-12a2 2 0 0 0 -2 -2h-2"/><path d="M9 3m0 2a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-2a2 2 0 0 1 -2 -2z"/><path d="M12 11l0 6"/><path d="M9 14l3 3l3 -3"/>',
);
const ICON_EDIT = svgIcon(
'<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/><path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3z"/><path d="M16 5l3 3"/>',
);
const ICON_TRASH = svgIcon(
'<path d="M4 7l16 0"/><path d="M10 11l0 6"/><path d="M14 11l0 6"/><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"/><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"/>',
);
function secretFieldHtml(id, value, editable = false) {
const action = editable
? `<button type="button" class="paste" data-target="${id}" title="Paste">${ICON_PASTE}</button>`
: `<button type="button" class="copy" data-target="${id}" title="Copy">${ICON_COPY}</button>`;
return (
`<div class="secret-field">` +
`<input id="${id}" type="password" value="${esc(value)}"${editable ? "" : " readonly"} autocomplete="off" spellcheck="false">` +
`<button type="button" class="eye" data-target="${id}" title="Reveal">${ICON_EYE}</button>` +
action +
`</div>`
);
}
function wireSecretToggles(root) {
root.querySelectorAll("button.eye").forEach((b) => {
b.onclick = () => {
const inp = root.querySelector(`#${b.dataset.target}`);
const masked = inp.type === "password";
inp.type = masked ? "text" : "password";
b.classList.toggle("on", masked);
b.title = masked ? "Hide" : "Reveal";
b.innerHTML = masked ? ICON_EYE_OFF : ICON_EYE;
};
});
root.querySelectorAll("button.copy").forEach((b) => {
b.onclick = async () => {
const inp = root.querySelector(`#${b.dataset.target}`);
try { await navigator.clipboard.writeText(inp.value); toast("copied"); }
catch (_) { toast("copy failed", "err"); }
};
});
root.querySelectorAll("button.paste").forEach((b) => {
b.onclick = async () => {
const inp = root.querySelector(`#${b.dataset.target}`);
try { inp.value = await navigator.clipboard.readText(); toast("pasted"); }
catch (_) { inp.focus(); toast("paste failed — use ⌘V in the field", "err"); }
};
});
}
function row(k, v) {
return `<div class="row"><span class="k">${esc(k)}</span> ${v}</div>`;
}
function badge(sensitivity) {
const s = esc(sensitivity || "");
return `<span class="badge ${s}">${s}</span>`;
}
function renderReveal(j) {
if (j.value !== undefined) {
return {
title: "Revealed value",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
row("Sensitivity", badge(j.sensitivity)) +
`<div class="row"><span class="k">Value</span></div>` +
secretFieldHtml("reveal-value", j.value),
};
}
if (j.masked) {
return {
title: "Masked (critical)",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
(j.sensitivity ? row("Sensitivity", badge(j.sensitivity)) : "") +
(j.fingerprint ? row("Fingerprint", `<span class="fp">${esc(j.fingerprint)}</span>`) : "") +
`<p class="note"><span class="lock">🔒 masked in the browser.</span> ${esc(j.note || "")}</p>`,
};
}
if (j.inject_only) {
return {
title: "Inject-only",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
row("Sensitivity", badge(j.sensitivity)) +
`<p class="note"><span class="lock">🔒 never revealed on any surface (I2).</span></p>`,
};
}
if (j.kind === "reference") {
return {
title: "Reference",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
row("Pointer", `<code>${esc(j.pointer)}</code>`) +
row("Status", esc(j.status || "")) +
`<p class="note">${esc(j.note || "")}</p>`,
};
}
if (j.kind === "keypair" || j.kind === "public-only") {
return {
title: j.kind === "keypair" ? "Keypair" : "Public key",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
row("Algorithm", esc(j.algorithm || "")) +
`<div class="row"><span class="k">Public key</span></div>` +
`<div class="value">${esc(j.public || "")}</div>` +
`<p class="note">${esc(j.note || "")}</p>`,
};
}
if (j.kind === "totp") {
return {
title: "TOTP enrollment",
html:
row("Coordinate", `<code>${esc(j.coordinate)}</code>`) +
row("Algorithm", esc(j.algorithm || "")) +
row("Digits", esc(j.digits)) +
row("Period", `${esc(j.period)}s`) +
`<p class="note">${esc(j.note || "")}</p>`,
};
}
return {
title: "Details",
html:
row("Coordinate", `<code>${esc(j.coordinate || "")}</code>`) +
`<p class="note">${esc(j.note || JSON.stringify(j))}</p>`,
};
}
function openReveal() {
$("#drawer").classList.add("show");
$("#scrim").classList.add("show");
}
function closeReveal() {
$("#drawer").classList.remove("show");
$("#scrim").classList.remove("show");
}
async function inspect(d) {
const q = new URLSearchParams({ coord: d.coordinate });
const p = projectOf(d.origin);
if (p) q.set("project", p);
const { ok, status, body } = await api(`/api/reveal?${q}`);
const bodyEl = $("#reveal-body");
if (!ok) {
$("#reveal-title").textContent = "Error";
bodyEl.innerHTML = `<p class="note">request failed (${status})</p>`;
} else {
const { title, html } = renderReveal(body);
$("#reveal-title").textContent = title;
bodyEl.innerHTML = html;
wireSecretToggles(bodyEl);
}
openReveal();
}
const SENS = ["low", "medium", "high", "inject-only"];
const field = (label, inner) =>
`<label class="field"><span class="lbl">${esc(label)}</span>${inner}</label>`;
const sensSelect = (id, sel = "medium") =>
`<select id="${id}">` +
SENS.map((s) => `<option value="${s}"${s === sel ? " selected" : ""}>${s}</option>`).join("") +
`</select>`;
let onSubmit = null;
function openForm(title, bodyHtml, submit, submitLabel = "Save", danger = false) {
$("#form-title").textContent = title;
const body = $("#form-body");
body.innerHTML = bodyHtml;
wireSecretToggles(body);
const btn = $("#form-submit");
btn.textContent = submitLabel;
btn.classList.toggle("danger", danger);
btn.disabled = false; onSubmit = submit;
$("#form").showModal();
const first = body.querySelector("input,select,textarea");
if (first) first.focus();
}
function openCreate() {
const html =
field("Coordinate", `<input id="f-coord" placeholder="dev/db/password" autocomplete="off">`) +
`<div class="field"><span class="lbl">Kind</span><div class="radios">` +
`<label><input type="radio" name="kind" value="literal" checked> Literal value</label>` +
`<label><input type="radio" name="kind" value="reference"> Reference</label>` +
`<label><input type="radio" name="kind" value="generate"> Generate</label>` +
`</div></div>` +
`<div id="f-literal">${field("Value", secretFieldHtml("f-value", "", true))}</div>` +
`<div id="f-reference" hidden>${field("Pointer", `<input id="f-pointer" placeholder="azure-kv://vault/name" autocomplete="off">`)}</div>` +
`<div id="f-generate" hidden>${field("Length", `<input id="f-length" type="number" value="32" min="1" max="256">`)}</div>` +
field("Sensitivity", sensSelect("f-sens")) +
field("Description", `<input id="f-desc" autocomplete="off">`) +
`<label class="field check"><input id="f-reveal" type="checkbox"> revealable over MCP (non-prod, non-high only)</label>`;
openForm("New secret", html, async () => {
const coord = $("#f-coord").value.trim();
if (!coord) { toast("coordinate is required", "err"); return false; }
const kind = $("#form-body").querySelector("input[name=kind]:checked").value;
const sensitivity = $("#f-sens").value;
const description = $("#f-desc").value.trim() || undefined;
if (kind === "generate") {
const length = parseInt($("#f-length").value, 10) || 32;
return submitJson("POST", "/api/generate", { coord, length, sensitivity, description }, "generated");
}
const revealable = $("#f-reveal").checked;
const payload = { coord, sensitivity, description, revealable };
if (kind === "reference") payload.reference = $("#f-pointer").value.trim();
else payload.value = $("#f-value").value;
return submitJson("POST", "/api/secret", payload, "created");
});
const body = $("#form-body");
body.querySelectorAll("input[name=kind]").forEach((r) => {
r.onchange = () => {
const k = body.querySelector("input[name=kind]:checked").value;
$("#f-literal").hidden = k !== "literal";
$("#f-reference").hidden = k !== "reference";
$("#f-generate").hidden = k !== "generate";
};
});
}
function openEdit(d) {
const isRef = d.mode === "reference";
const isLiteral = d.mode === "literal";
const html =
`<p class="note">Editing <code>${esc(d.coordinate)}</code></p>` +
field("Sensitivity", sensSelect("e-sens", d.sensitivity)) +
field("Description", `<input id="e-desc" autocomplete="off" value="${esc(d.description || "")}">`) +
(isRef ? field("Pointer", `<input id="e-pointer" autocomplete="off" value="${esc(d.pointer || "")}">`) : "") +
(isLiteral ? `<div class="field"><span class="lbl">New value <span class="muted">(optional)</span></span>${secretFieldHtml("e-value", "", true)}</div>` : "") +
`<label class="field check"><input id="e-reveal" type="checkbox"${d.revealable ? " checked" : ""}> revealable over MCP</label>`;
openForm(`Edit ${d.coordinate}`, html, async () => {
const p = projectOf(d.origin);
const meta = {
coord: d.coordinate,
project: p || undefined,
sensitivity: $("#e-sens").value,
description: $("#e-desc").value.trim() || undefined,
revealable: $("#e-reveal").checked,
};
if (isRef) meta.reference = $("#e-pointer").value.trim();
const r1 = await api("/api/secret", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify(meta),
});
if (!r1.ok) { toast(r1.body?.error || `edit failed (${r1.status})`, "err"); return false; }
const newVal = isLiteral ? $("#e-value").value : "";
if (newVal) {
const r2 = await api("/api/secret", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ coord: d.coordinate, project: p || undefined, value: newVal }),
});
if (!r2.ok) { toast(r2.body?.error || `value update failed (${r2.status})`, "err"); return false; }
}
toast("saved");
return true;
});
}
function openDelete(d) {
const critical = d.sensitivity === "high" || d.sensitivity === "inject-only";
const doDelete = async () => {
const p = projectOf(d.origin);
const q = new URLSearchParams({ coord: d.coordinate });
if (p) q.set("project", p);
const { ok, status, body } = await api(`/api/secret?${q}`, { method: "DELETE" });
if (!ok) { toast(body?.error || `delete failed (${status})`, "err"); return false; }
toast("deleted");
return true;
};
if (critical) {
const html =
`<p>Delete <code>${esc(d.coordinate)}</code> (<strong>${esc(d.sensitivity)}</strong>)? This cannot be undone.</p>` +
`<p class="note">You'll be asked to approve on your device (Touch ID), or via <code>kovra approve</code> in a terminal.</p>`;
openForm("Delete secret", html, doDelete, "Delete", true);
return;
}
const html =
`<p>Delete <code>${esc(d.coordinate)}</code>? This cannot be undone.</p>` +
`<label class="field"><span class="lbl">Coordinate</span>` +
`<div class="secret-field">` +
`<input id="del-name" type="text" value="${esc(d.coordinate)}" readonly autocomplete="off" spellcheck="false">` +
`<button type="button" class="copy" data-target="del-name" title="Copy">${ICON_COPY}</button>` +
`</div></label>` +
`<label class="field"><span class="lbl">Type or paste it to confirm</span>` +
`<input id="del-confirm" autocomplete="off" placeholder="${esc(d.coordinate)}"></label>`;
openForm("Delete secret", html, async () => {
if ($("#del-confirm").value.trim() !== d.coordinate) {
toast("name does not match", "err");
return false;
}
return doDelete();
}, "Delete", true);
const name = $("#del-name");
name.onfocus = () => name.select();
name.onclick = () => name.select();
const inp = $("#del-confirm");
const btn = $("#form-submit");
btn.disabled = true;
inp.oninput = () => { btn.disabled = inp.value.trim() !== d.coordinate; };
inp.focus(); }
async function submitJson(method, path, payload, okWord) {
const { ok, status, body } = await api(path, {
method,
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
if (!ok) { toast(body?.error || `${okWord} failed (${status})`, "err"); return false; }
toast(okWord);
return true;
}
let page = "home"; let view = "table"; let currentSecrets = [];
let searchTerm = ""; const colFilters = {}; let projectFilter = null;
const isNode = (d) => !!(d && d._node);
function coordCell(cell) {
const d = cell.getData();
if (isNode(d)) {
return `<strong>${esc(d._label)}</strong> <span class="muted">(${d._count})</span>`;
}
const shadow = d.shadows_global
? ' <span class="muted" title="shadows a global coordinate">*shadows global</span>'
: "";
const label = view === "tree" ? esc(`${d.component}/${d.key}`) : esc(d.coordinate);
return `<code>${label}</code>${shadow}`;
}
function modeCell(cell) {
const d = cell.getData();
if (isNode(d)) return "";
const ptr = d.pointer ? ` <span class="muted">→ ${esc(d.pointer)}</span>` : "";
return `<span class="mode-pill">${esc(d.mode || "")}</span>${ptr}`;
}
function fpCell(cell) {
const d = cell.getData();
if (isNode(d)) return "";
return d.fingerprint ? `<span class="fp">${esc(d.fingerprint)}</span>` : "";
}
function sensCell(cell) {
const d = cell.getData();
return isNode(d) ? "" : badge(cell.getValue());
}
function actionCell(cell) {
if (isNode(cell.getData())) return "";
return (
`<button class="row-act" data-act="inspect" title="Inspect / reveal">${ICON_EYE}</button>` +
`<button class="row-act" data-act="edit" title="Edit">${ICON_EDIT}</button>` +
`<button class="row-act danger" data-act="del" title="Delete">${ICON_TRASH}</button>`
);
}
function onAction(e, cell) {
const act = e.target?.closest?.("button[data-act]")?.dataset?.act;
if (!act) return; const d = cell.getData();
if (act === "inspect") inspect(d);
else if (act === "edit") openEdit(d);
else if (act === "del") openDelete(d);
}
function toTree(secrets) {
const envs = new Map();
for (const s of secrets) {
if (!envs.has(s.environment)) envs.set(s.environment, []);
envs.get(s.environment).push(s);
}
const out = [];
for (const [env, leaves] of envs) {
out.push({ _node: true, _label: env, _count: leaves.length, coordinate: env, _children: leaves });
}
return out;
}
const originLabel = (origin) =>
origin && origin.startsWith("project:") ? origin.slice(8) : "global";
const originOf = (s) => s.origin || "global";
function distinctOrigins(secrets) {
const seen = new Set();
const out = [];
for (const s of secrets) {
const o = originOf(s);
if (!seen.has(o)) { seen.add(o); out.push(o); }
}
return out;
}
function visibleSecrets() {
const origins = distinctOrigins(currentSecrets);
if (!projectFilter || !origins.includes(projectFilter)) {
projectFilter = origins.includes("global") ? "global" : origins[0] || "global";
}
return currentSecrets.filter((s) => originOf(s) === projectFilter);
}
function renderSidebarProjects() {
const list = $("#proj-list");
const origins = distinctOrigins(currentSecrets); list.innerHTML = "";
for (const o of origins) {
const n = currentSecrets.filter((s) => originOf(s) === o).length;
const label = o === "global" ? "(global)" : originLabel(o);
const item = el(
"button",
{ class: `navitem${o === projectFilter ? " on" : ""}`, "data-origin": o },
`<span class="nl">${esc(label)}</span><span class="nc">${n}</span>`,
);
item.onclick = () => selectProject(o);
list.appendChild(item);
}
syncNav();
}
function setProjectsExpanded(open) {
$("#proj-list").hidden = !open;
const t = $("#proj-toggle");
t.classList.toggle("open", open);
t.setAttribute("aria-expanded", String(open));
}
function selectProject(origin) {
projectFilter = origin;
$("#search").value = "";
searchTerm = "";
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
if (origin) setProjectsExpanded(true);
renderSidebarProjects();
render();
setPage("secrets"); }
function syncNav() {
$("#nav-home").classList.toggle("on", page === "home");
document.querySelectorAll("#proj-list .navitem").forEach((it) => {
it.classList.toggle("on", page === "secrets" && it.dataset.origin === projectFilter);
});
}
function setPage(p) {
page = p;
$("#page-home").hidden = p !== "home";
$("#page-secrets").hidden = p !== "secrets";
syncNav();
persistScope();
if (p === "home") renderHome();
else if (table) table.redraw(true);
}
function renderHome() {
const all = currentSecrets;
const count = (pred) => all.filter(pred).length;
$("#stat-total").textContent = all.length;
$("#stat-high").textContent = count((s) => s.sensitivity === "high");
$("#stat-inject").textContent = count((s) => s.sensitivity === "inject-only");
$("#stat-ref").textContent = count((s) => s.mode === "reference");
$("#home-sub").textContent = `${all.length} secret${all.length === 1 ? "" : "s"} · overview`;
renderRecent();
renderIntakes();
}
function renderRecent() {
const recent = [...currentSecrets]
.sort((a, b) => String(b.updated || "").localeCompare(String(a.updated || "")))
.slice(0, 8);
const box = $("#recent");
if (!recent.length) { box.innerHTML = `<div class="empty">no secrets yet</div>`; return; }
box.innerHTML =
`<table class="mini"><tbody>` +
recent
.map(
(s) =>
`<tr data-coord="${esc(s.coordinate)}" data-origin="${esc(s.origin || "global")}">` +
`<td class="proj muted">${esc(s.origin === "global" || !s.origin ? "global" : originLabel(s.origin))}</td>` +
`<td class="c"><code>${esc(s.coordinate)}</code></td>` +
`<td>${badge(s.sensitivity)}</td>` +
`<td><span class="mode-pill">${esc(s.mode || "")}</span></td>` +
`</tr>`,
)
.join("") +
`</tbody></table>`;
box.querySelectorAll("tr").forEach((tr) => {
tr.onclick = () => {
const d = currentSecrets.find(
(x) => x.coordinate === tr.dataset.coord && (x.origin || "global") === tr.dataset.origin,
);
if (d) inspect(d);
};
});
}
async function renderIntakes() {
const list = $("#intake-list");
const counter = $("#intake-count");
const { ok, body } = await api("/api/intakes");
if (!ok) { list.innerHTML = `<div class="empty">could not load intakes</div>`; counter.textContent = "0"; return; }
const intakes = body.intakes || [];
counter.textContent = String(intakes.length);
if (!intakes.length) { list.innerHTML = `<div class="empty">no pending requests</div>`; return; }
list.innerHTML = intakes.map(intakeRowHtml).join("");
list.querySelectorAll("[data-fulfill]").forEach((b) => {
b.onclick = () => openFulfill(intakes.find((x) => x.id === b.dataset.fulfill));
});
list.querySelectorAll("[data-dismiss]").forEach((b) => {
b.onclick = () => dismissIntake(b.dataset.dismiss);
});
}
function intakeRowHtml(i) {
const proc = i.requesting_process ? ` <span class="muted">· ${esc(i.requesting_process)}</span>` : "";
const note = i.description ? `<div class="fenced sm">${esc(i.description)}</div>` : "";
return (
`<div class="intake">` +
`<div class="ii">` +
`<div class="ic"><code>${esc(i.coordinate)}</code> ${badge(i.sensitivity)}</div>` +
`<div class="im muted">${esc(i.environment)}${proc}</div>` +
note +
`</div>` +
`<div class="ia">` +
`<button class="btn sm primary" data-fulfill="${esc(i.id)}">Fulfil</button>` +
`<button class="btn sm" data-dismiss="${esc(i.id)}">Dismiss</button>` +
`</div>` +
`</div>`
);
}
function openFulfill(i) {
if (!i) return;
const projects = distinctOrigins(currentSecrets).filter((o) => o.startsWith("project:"));
const projOpts =
`<option value="">(global)</option>` +
projects.map((o) => `<option value="${esc(originLabel(o))}">${esc(originLabel(o))}</option>`).join("");
const high = i.sensitivity === "high";
const html =
`<p class="note">Sealing <code>${esc(i.coordinate)}</code> as <strong>${esc(i.sensitivity)}</strong>${i.environment === "prod" ? " (prod is born high)" : ""}.</p>` +
(i.description ? field("Requester note (untrusted)", `<div class="fenced">${esc(i.description)}</div>`) : "") +
`<div class="field"><span class="lbl">Value</span>${secretFieldHtml("ff-value", "", true)}</div>` +
`<label class="field"><span class="lbl">Project vault</span><select id="ff-proj">${projOpts}</select></label>` +
(high ? `<p class="note">High — you'll approve on your device (Touch ID), or via <code>kovra approve</code>.</p>` : "");
openForm(`Fulfil ${i.coordinate}`, html, async () => {
const value = $("#ff-value").value;
if (!value) { toast("value is required", "err"); return false; }
const project = $("#ff-proj").value || undefined;
return submitJson("POST", "/api/intakes/fulfill", { id: i.id, value, project }, "fulfilled");
}, "Fulfil");
}
async function dismissIntake(id) {
const { ok, status, body } = await api(`/api/intakes?id=${encodeURIComponent(id)}`, { method: "DELETE" });
if (!ok) { toast(body?.error || `dismiss failed (${status})`, "err"); return; }
toast("dismissed");
load();
}
function persistScope() {
try {
localStorage.setItem("kovra-page", page);
localStorage.setItem("kovra-view", view);
if (projectFilter) localStorage.setItem("kovra-project", projectFilter);
else localStorage.removeItem("kovra-project");
} catch (_) { }
}
function restoreState() {
try {
const pg = localStorage.getItem("kovra-page");
if (pg === "home" || pg === "secrets") page = pg;
const v = localStorage.getItem("kovra-view");
if (v === "table" || v === "tree") view = v;
const p = localStorage.getItem("kovra-project");
if (p) projectFilter = p;
} catch (_) { }
$("#view-table").classList.toggle("on", view === "table");
$("#view-tree").classList.toggle("on", view === "tree");
setProjectsExpanded(true);
}
const FUNNEL_ICON =
'<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18l-7 8v5l-4 2v-7L3 5Z"/></svg>';
const TREE_EXPAND =
'<svg class="tree-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6l-6 6"/></svg>';
const TREE_COLLAPSE =
'<svg class="tree-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6l6 -6"/></svg>';
function rowMatches(data) {
if (isNode(data)) return true;
if (searchTerm) {
const hit = ["coordinate", "origin", "environment", "mode", "pointer"].some(
(f) => String(data[f] || "").toLowerCase().includes(searchTerm),
);
if (!hit) return false;
}
for (const [field, val] of Object.entries(colFilters)) {
if (!val) continue;
if (field === "sensitivity") {
if (String(data.sensitivity || "") !== val) return false;
} else if (!String(data[field] || "").toLowerCase().includes(val.toLowerCase())) {
return false;
}
}
return true;
}
function applyFilters() {
if (!table || view !== "table") return;
const active = searchTerm || Object.values(colFilters).some(Boolean);
if (active) table.setFilter(rowMatches);
else table.clearFilter(true);
}
function markActiveFilterCols() {
if (!table) return;
for (const c of table.getColumns()) {
const f = c.getField();
c.getElement().classList.toggle("has-filter", !!(f && colFilters[f]));
}
}
function filterPopup(field, kind) {
return (e, column, onRendered) => {
const wrap = el("div", { class: "col-filter-popup" });
wrap.appendChild(el("div", { class: "cfp-label" }, `Filter ${esc(column.getDefinition().title)}`));
const input =
kind === "sens"
? el("select", { class: "cfp-input" },
`<option value="">all</option>` + SENS.map((s) => `<option value="${s}">${s}</option>`).join(""))
: el("input", { class: "cfp-input", type: "text", placeholder: "contains…", autocomplete: "off" });
input.value = colFilters[field] || "";
const onChange = () => {
const v = (input.value || "").trim();
if (v) colFilters[field] = v;
else delete colFilters[field];
column.getElement().classList.toggle("has-filter", !!v);
applyFilters();
};
input.addEventListener("input", onChange);
input.addEventListener("change", onChange);
wrap.appendChild(input);
const clear = el("button", { type: "button", class: "cfp-clear" }, "Clear");
clear.addEventListener("click", () => { input.value = ""; onChange(); input.focus(); });
wrap.appendChild(clear);
onRendered(() => input.focus());
return wrap;
};
}
function columns() {
const pf = (field, kind) =>
view === "table" ? { headerPopup: filterPopup(field, kind), headerPopupIcon: FUNNEL_ICON } : {};
return [
{ title: "Coordinate", field: "coordinate", formatter: coordCell, widthGrow: 3, minWidth: 220, ...pf("coordinate", "text") },
{ title: "Env", field: "environment", width: 120, minWidth: 120, ...pf("environment", "text") },
{ title: "Sensitivity", field: "sensitivity", width: 175, minWidth: 175, formatter: sensCell, ...pf("sensitivity", "sens") },
{ title: "Mode", field: "mode", formatter: modeCell, widthGrow: 2, minWidth: 150, ...pf("mode", "text") },
{ title: "Fingerprint", field: "fingerprint", width: 150, minWidth: 130, formatter: fpCell },
{ title: "", field: "_act", width: 210, minWidth: 210, hozAlign: "right", headerSort: false, formatter: actionCell, cellClick: onAction },
];
}
function onRowDblClick(e, row) {
if (e.target?.closest && e.target.closest("button.row-act")) return;
const d = row.getData();
if (!isNode(d)) inspect(d);
}
function render() {
if (table) { table.destroy(); table = null; }
const common = {
layout: "fitColumns", height: "100%", movableColumns: true, placeholder: "no secrets",
columns: columns(), rowDblClick: onRowDblClick,
tableBuilt() { applyFilters(); markActiveFilterCols(); },
};
const data = visibleSecrets();
updateStats(data);
if (view === "tree") {
table = new Tabulator("#grid", {
...common,
data: toTree(data),
dataTree: true,
dataTreeStartExpanded: true,
dataTreeElementColumn: "coordinate",
dataTreeExpandElement: TREE_EXPAND,
dataTreeCollapseElement: TREE_COLLAPSE,
dataTreeBranchElement: false,
});
} else {
table = new Tabulator("#grid", {
...common,
data,
pagination: true,
paginationSize: 25,
paginationSizeSelector: [10, 25, 50, 100],
initialSort: [{ column: "coordinate", dir: "asc" }],
});
}
}
function setView(v) {
if (view === v) return;
view = v;
$("#view-table").classList.toggle("on", v === "table");
$("#view-tree").classList.toggle("on", v === "tree");
$("#search").value = "";
searchTerm = "";
Object.keys(colFilters).forEach((k) => delete colFilters[k]);
persistScope();
render();
}
function applySearch(term) {
searchTerm = (term || "").trim().toLowerCase();
applyFilters();
}
function updateStats(data) {
const n = data.length;
const scope = projectFilter === "global" ? "global" : projectFilter ? originLabel(projectFilter) : "";
$("#status").textContent = scope ? `${n} secret${n === 1 ? "" : "s"} in ${scope}` : `${n} secrets`;
const title = $("#secrets-title");
if (title) {
title.textContent =
projectFilter === "global" ? "Global" : projectFilter ? originLabel(projectFilter) : "Secrets";
}
}
async function load() {
const btn = $("#refresh");
btn.classList.add("spinning");
const minSpin = new Promise((r) => setTimeout(r, 500));
try {
const { ok, status: code, body } = await api("/api/secrets");
if (!ok) { $("#status").textContent = `auth error (${code})`; return; }
currentSecrets = body.secrets || [];
render();
renderSidebarProjects();
setPage(page); } finally {
await minSpin;
btn.classList.remove("spinning");
}
}
function applyTheme(t) {
document.documentElement.dataset.theme = t;
try { localStorage.setItem("kovra-theme", t); } catch (_) { }
}
function toggleTheme() {
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
}
document.addEventListener("DOMContentLoaded", () => {
try {
const saved = localStorage.getItem("kovra-theme");
if (saved) applyTheme(saved);
} catch (_) { }
restoreState();
$("#search").addEventListener("input", (e) => applySearch(e.target.value));
$("#refresh").addEventListener("click", load);
$("#theme").addEventListener("click", toggleTheme);
$("#new").addEventListener("click", openCreate);
$("#view-table").addEventListener("click", () => setView("table"));
$("#view-tree").addEventListener("click", () => setView("tree"));
$("#nav-home").addEventListener("click", (e) => { e.preventDefault(); setPage("home"); });
$("#proj-toggle").addEventListener("click", () => setProjectsExpanded($("#proj-list").hidden));
$("#home-new").addEventListener("click", openCreate);
$("#reveal-close").addEventListener("click", closeReveal);
$("#scrim").addEventListener("click", closeReveal);
$("#form-cancel").addEventListener("click", () => $("#form").close());
$("#form-cancel-2").addEventListener("click", () => $("#form").close());
$("#form-el").addEventListener("submit", async (e) => {
e.preventDefault();
if (!onSubmit) return;
const ok = await onSubmit();
if (ok) { $("#form").close(); load(); }
});
load();
});