161 lines
6.5 KiB
JavaScript
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 };
|
|
};
|
|
})();
|