chore: initial import
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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 };
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user