Files
boehmitools-main/plugins/tandoor-lint/static/index.html
T
2026-07-24 21:37:03 +02:00

201 lines
8.5 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rezept-Inventur</title>
<link rel="stylesheet" href="/shared/boehmi.css">
<script src="/shared/boehmi.js"></script>
<script src="/shared/boehmi-runner.js"></script>
<style>
.facts { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 14px; }
.fact { background: var(--bt-surface); border: 1px solid var(--bt-line); border-radius: var(--bt-r-lg); padding: 13px 14px; box-shadow: var(--bt-shadow); }
.fact b { display: block; font-size: 24px; font-weight: 850; letter-spacing: -.03em; }
.fact span { font-size: 10.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--bt-muted); }
.fact.err b { color: var(--bt-err); }
.fact.warn b { color: var(--bt-warn); }
.fact.ok b { color: var(--bt-ok); }
.checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 7px; }
.ck { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center;
padding: 8px 10px; border: 1px solid var(--bt-line); border-radius: var(--bt-r-sm);
background: var(--bt-surface); cursor: pointer; }
.ck:hover { background: var(--bt-surface-2); }
.ck .t { font-size: 12px; font-weight: 600; line-height: 1.25; }
.ck .c { font-family: var(--bt-mono); font-size: 11px; color: var(--bt-muted); }
.ck.off { opacity: .45; }
.flist { border: 1px solid var(--bt-line); border-radius: var(--bt-r); overflow: hidden; }
.fitem { display: grid; grid-template-columns: 56px 1fr auto; gap: 10px; align-items: baseline;
padding: 9px 11px; border-bottom: 1px solid var(--bt-line-soft); font-size: 13px; }
.fitem:last-child { border-bottom: none; }
.fitem:hover { background: var(--bt-surface-2); }
.fitem .who { font-weight: 650; }
.fitem .det { color: var(--bt-muted); font-size: 12px; }
.fitem .id { font-family: var(--bt-mono); font-size: 11px; color: var(--bt-muted); }
.sev { font-size: 9.5px; font-weight: 850; letter-spacing: .06em; text-transform: uppercase;
padding: 2px 6px; border-radius: 4px; text-align: center; }
.sev.err { color: var(--bt-err); background: var(--bt-err-soft); }
.sev.warn { color: var(--bt-warn); background: var(--bt-warn-soft); }
.sev.info { color: var(--bt-muted); background: var(--bt-bg-2); }
.grp { padding: 9px 11px; background: var(--bt-surface-2); border-bottom: 1px solid var(--bt-line);
font-size: 11px; font-weight: 800; letter-spacing: .05em; text-transform: uppercase; color: var(--bt-muted); }
.grp .h { text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--bt-muted); font-size: 11.5px; }
@media (max-width: 900px) { .facts { grid-template-columns: repeat(2, 1fr); } .fitem { grid-template-columns: 52px 1fr; } .fitem .id { display: none; } }
</style>
</head>
<body data-bt-title="Rezept-Inventur" data-bt-icon="🔍">
<main class="bt-main wide">
<div class="bt-pagehead">
<h1>Rezept-Inventur</h1>
<p>Geht die Sammlung durch und meldet, was fehlt oder schief steht.
Dieses Werkzeug liest ausschließlich — es verändert in Tandoor nichts.</p>
</div>
<div id="warn" class="bt-notice err bt-hidden"></div>
<div class="bt-card">
<div class="bt-row" style="align-items:center">
<div style="flex:1">
<h2 style="margin:0">Durchsehen</h2>
<p class="hint" style="margin:4px 0 0">
Je Rezept eine Abfrage — bei vielen Rezepten dauert das einen Moment.</p>
</div>
<button class="primary" id="scan">Inventur starten</button>
<button class="ghost mini" id="cancel" disabled>Abbrechen</button>
</div>
<div id="status" class="bt-status bt-hidden">Bereit.</div>
<div id="log" class="bt-log bt-hidden"></div>
</div>
<div id="box" class="bt-empty">Noch keine Inventur gelaufen.</div>
</main>
<script>
"use strict";
const $ = (id) => document.getElementById(id);
let state = {};
let report = null;
const hidden = new Set();
let search = "";
const runner = BT.Runner({
endpoint: "/api/run/scan",
log: $("log"), status: $("status"),
onStart: () => {
$("log").classList.remove("bt-hidden");
$("status").classList.remove("bt-hidden");
$("cancel").disabled = false;
},
onFinish: (info) => {
$("cancel").disabled = true;
BT.toast(info.status === "done" ? "Inventur fertig." : "Beendet: " + info.status,
info.status === "done" ? "ok" : "err");
load();
},
});
$("scan").addEventListener("click", () => runner.start({ only: [], skip: [] }));
$("cancel").addEventListener("click", () => runner.cancel());
function render() {
if (!report) return;
const s = report.summary;
const aktive = report.checks.filter((c) => c.count > 0);
$("box").className = "";
$("box").innerHTML = `
<div class="facts">
<div class="fact"><b>${s.recipes_checked}</b><span>Rezepte geprüft</span></div>
<div class="fact ok"><b>${s.clean_recipes}</b><span>ohne Befund</span></div>
<div class="fact err"><b>${s.err}</b><span>Fehler</span></div>
<div class="fact warn"><b>${s.warn}</b><span>Warnungen</span></div>
<div class="fact"><b>${s.info}</b><span>Hinweise</span></div>
</div>
${s.unreadable?.length ? `<div class="bt-notice warn">
${s.unreadable.length} Rezepte konnten nicht gelesen werden und fehlen im Bericht.
</div>` : ""}
<section class="bt-card">
<h2>Was wurde gefunden</h2>
<p class="hint">Zum Ein- und Ausblenden anklicken.</p>
<div class="checks">${aktive.map((c) => `
<div class="ck ${hidden.has(c.key) ? "off" : ""}" data-ck="${c.key}" title="${BT.escape(c.hint)}">
<span class="sev ${c.severity}">${c.severity}</span>
<span class="t">${BT.escape(c.title)}</span>
<span class="c">${c.count}</span>
</div>`).join("") || `<span class="hint">Nichts gefunden — sauber.</span>`}</div>
</section>
<section class="bt-card">
<div class="bt-row" style="align-items:center;margin-bottom:9px">
<h2 style="margin:0;flex:1">Befunde</h2>
<input type="search" id="q" placeholder="Rezept oder Zutat …" style="max-width:230px" value="${BT.escape(search)}">
<a class="bt-btn mini ghost" href="${BT.url("/api/export.csv")}" download>CSV</a>
</div>
<div class="flist" id="list"></div>
<p class="hint" id="more" style="margin-top:8px"></p>
</section>`;
$("q").addEventListener("input", (e) => { search = e.target.value; draw(); });
document.querySelectorAll("[data-ck]").forEach((el) => el.addEventListener("click", () => {
const k = el.dataset.ck;
hidden.has(k) ? hidden.delete(k) : hidden.add(k);
el.classList.toggle("off", hidden.has(k));
draw();
}));
draw();
}
function draw() {
const q = search.toLowerCase();
const rows = report.findings.filter((f) =>
!hidden.has(f.check) &&
(!q || String(f.target).toLowerCase().includes(q) || f.detail.toLowerCase().includes(q)));
const byCheck = {};
rows.forEach((f) => (byCheck[f.check] ||= []).push(f));
let html = "";
let shown = 0;
for (const [key, items] of Object.entries(byCheck)) {
const meta = report.checks.find((c) => c.key === key);
html += `<div class="grp">${BT.escape(meta.title)} · ${items.length}
<span class="h">— ${BT.escape(meta.hint)}</span></div>`;
for (const f of items.slice(0, 60)) {
shown++;
html += `<div class="fitem">
<span class="sev ${f.severity}">${f.severity}</span>
<span><span class="who">${BT.escape(f.target)}</span>
<span class="det"> — ${BT.escape(f.detail)}</span></span>
<span class="id">${f.scope} ${f.target_id}</span>
</div>`;
}
if (items.length > 60) {
html += `<div class="fitem"><span></span>
<span class="det">… und ${items.length - 60} weitere. Die vollständige Liste steht in der CSV.</span>
<span></span></div>`;
}
}
$("list").innerHTML = html || `<div class="bt-empty" style="border:none">Nichts übrig — Filter zurücksetzen?</div>`;
$("more").textContent = `${rows.length} Befunde angezeigt, ${report.findings.length} insgesamt.`;
}
async function load() {
state = await BT.api("/api/state");
report = state.report;
$("warn").classList.toggle("bt-hidden", state.tandoor);
if (!state.tandoor) {
$("warn").innerHTML = `Tandoor-URL und Token fehlen. Bitte in den
<a href="/settings">Einstellungen</a> hinterlegen.`;
}
$("scan").disabled = !state.tandoor;
if (report) render();
if (state.running) runner.attach(state.running.id);
}
load();
</script>
</body>
</html>