Files
2026-07-24 21:37:03 +02:00

161 lines
6.5 KiB
JavaScript

/*
* Gemeinsamer Baustein „Sicherungen“.
*
* Jedes schreibende Plugin bekommt damit dieselbe Ansicht: welche Läufe gab
* es, was steckt drin, wie weit lässt sich das zurückdrehen. Der Text zur
* Rückspielbarkeit kommt aus dem Manifest — die Oberfläche behauptet also
* nichts, was das Werkzeug nicht auch kann.
*/
(function () {
"use strict";
const LEVEL = {
voll: { css: "ok", text: "vollständig rückspielbar" },
neue_id: { css: "info", text: "rückspielbar, neue ID" },
teilweise: { css: "warn", text: "teilweise rückspielbar" },
nein: { css: "err", text: "nicht rückspielbar" },
};
function renderRuns(runs, selected) {
if (!runs.length) {
return `<div class="bt-empty" style="border:none">
Noch keine Sicherungen. Es entsteht bei jedem Lauf eine, der wirklich
etwas schreibt — Trockenübungen zählen nicht.</div>`;
}
return runs.map((r) => {
const lv = LEVEL[r.restore_level] || LEVEL.nein;
const when = r.created_at ? new Date(r.created_at) : null;
return `
<label class="bt-run ${r.id === selected ? "sel" : ""}">
<input type="radio" name="btrun" value="${BT.escape(r.id)}"
${r.id === selected ? "checked" : ""}>
<span class="bt-run-main">
<b>${BT.escape(r.label || r.id)}</b>
<span class="bt-run-sub">${when && !isNaN(when) ? when.toLocaleString("de-DE") : BT.escape(r.id)}
· ${r.done} von ${r.steps} Schritten geschrieben</span>
</span>
<span class="bt-run-tags">
<span class="bt-badge ${lv.css}">${lv.text}</span>
${r.restored ? `<span class="bt-badge">bereits zurückgespielt</span>` : ""}
</span>
</label>`;
}).join("");
}
function renderDetail(detail) {
const steps = (detail.manifest?.steps || []).filter((s) => s.status === "done");
if (!steps.length) return `<p class="hint">In diesem Lauf wurde nichts geschrieben.</p>`;
return `<div class="bt-steps">${steps.map((s) => {
const lv = LEVEL[s.restore_level] || LEVEL.nein;
let was = "";
if (s.action === "properties") {
was = `${Object.keys(s.written || {}).length} Werte gesetzt`;
} else if (s.action === "merge") {
was = `zusammengeführt auf ${BT.escape(s.target_name)}`
+ (s.references?.length ? ` · ${s.references.length} Verweise aufgezeichnet` : "")
+ (s.rescued?.length ? ` · ${s.rescued.length} Werte gerettet` : "");
} else if (s.action === "delete") {
was = "gelöscht";
} else if (s.action === "recipe") {
was = `${s.step_count} Schritte gesichert`;
}
return `<div class="bt-step">
<span class="bt-badge ${lv.css}">${lv.text}</span>
<span><b>${BT.escape(s.food_name || s.source_name || "?")}</b>
<span class="bt-muted"> ${was}</span></span>
</div>`;
}).join("")}</div>`;
}
/**
* Baut den Reiter. `mount` ist das Zielelement, `runner` ein BT.Runner,
* der auf /api/run/restore zeigt.
*/
BT.Backups = function (options) {
const mount = options.mount;
const runner = options.runner;
let runs = [];
let selected = null;
async function load() {
const data = await BT.api("/api/backups");
runs = data.runs || [];
if (!runs.some((r) => r.id === selected)) selected = runs[0]?.id || null;
draw(data.dir);
if (selected) await detail();
}
async function detail() {
if (!selected || !options.detail) { paintDetail(""); return; }
try {
paintDetail(renderDetail(await BT.api(`/api/backups/${encodeURIComponent(selected)}`)));
} catch { paintDetail(""); }
}
function paintDetail(html) {
const box = mount.querySelector("#btDetail");
if (box) box.innerHTML = html;
}
function draw(dir) {
const current = runs.find((r) => r.id === selected);
const lv = LEVEL[current?.restore_level] || LEVEL.nein;
mount.innerHTML = `
<div class="bt-card">
<div class="bt-row" style="align-items:center;margin-bottom:8px">
<h2 style="margin:0;flex:1">Sicherungen</h2>
<button class="mini ghost" id="btReload">Neu laden</button>
</div>
<p class="hint">Jeder Lauf, der wirklich geschrieben hat, liegt unter
<code>${BT.escape(dir || "")}</code>. Der Zustand vorher ist dort
vollständig festgehalten.</p>
<div class="bt-runs">${renderRuns(runs, selected)}</div>
</div>
${runs.length ? `
<div class="bt-card">
<h2>Was passierte in diesem Lauf</h2>
<div id="btDetail"><p class="hint">wird geladen …</p></div>
<div class="bt-notice ${lv.css === "ok" ? "" : "warn"}" style="margin-top:12px">
${options.note || ""}
</div>
<div class="bt-row" style="margin-top:12px;align-items:center">
<button id="btDry">Trockenübung</button>
<button class="danger" id="btApply">Zurückspielen</button>
<span class="bt-spacer"></span>
<label class="bt-check">
<input type="checkbox" id="btForce">
auch wenn seither von Hand geändert wurde
</label>
</div>
<div id="btStatus" class="bt-status">Bereit.</div>
<div id="btLog" class="bt-log"></div>
</div>` : ""}`;
mount.querySelectorAll('input[name="btrun"]').forEach((el) =>
el.addEventListener("change", () => { selected = el.value; draw(dir); detail(); }));
const byId = (id) => mount.querySelector("#" + id);
byId("btReload")?.addEventListener("click", load);
byId("btDry")?.addEventListener("click", () => go(false));
byId("btApply")?.addEventListener("click", () => go(true));
if (runner) runner.rebind({ log: byId("btLog"), status: byId("btStatus") });
}
function go(apply) {
const current = runs.find((r) => r.id === selected);
if (!current) return;
if (apply && !confirm(
`Lauf „${current.label || current.id}“ zurückspielen?\n\n` +
`${current.done} Schritte · ${(LEVEL[current.restore_level] || LEVEL.nein).text}\n\n` +
(current.restored ? "Achtung: Dieser Lauf wurde bereits zurückgespielt.\n\n" : "") +
`Erst die Trockenübung ansehen, wenn du unsicher bist.`)) return;
runner.start({ run: selected, apply, force: mount.querySelector("#btForce")?.checked || false });
}
return { load, reload: load };
};
})();