/* ========================================================================== boehmitools · Runner Gemeinsame Oberflächenlogik für Plugins, die ein Kommandozeilen-Tool kapseln: Lauf starten, Ausgabe live streamen, Status anzeigen, abbrechen. const runner = BT.Runner({ log: el, status: el, onFinish: fn }); runner.start({ apply: false }); // POST /api/run runner.attach(jobId); // an laufenden Lauf andocken Setzt boehmi.js voraus. ========================================================================== */ (function () { "use strict"; const LABEL = { running: ["busy", "läuft …"], done: ["ok", "erfolgreich abgeschlossen"], failed: ["error", "fehlgeschlagen"], cancelled: ["warn", "abgebrochen"], }; function classify(entry) { if (entry.stream === "meta") return "l-meta"; if (entry.stream === "stderr") return "l-err"; const t = entry.text; if (/^(FEHLER|Abbruch|Fehlgeschlagen)|✗|Traceback/i.test(t)) return "l-err"; if (/^(Warnung|Hinweis)|⚠/i.test(t)) return "l-warn"; if (/(erfolgreich|✓|OK\b|angelegt|aktualisiert)/i.test(t)) return "l-ok"; return ""; } function Runner(cfg) { // Nicht const: Oberflächen, die ihre Karten neu zeichnen, hängen den // Runner per rebind() wieder an die frischen Elemente. let logEl = cfg.log; let statusEl = cfg.status; // Plugins mit mehreren Läufen (prüfen / vorschlagen / übernehmen) geben // ihren eigenen Endpunkt an. Vorgabe bleibt der bisherige. const endpoint = cfg.endpoint || "/api/run"; let source = null; let job = null; function setStatus(kind, text, extra) { if (!statusEl) return; statusEl.className = "bt-status " + (kind || ""); statusEl.innerHTML = `${BT.escape(text)}` + (extra ? ` ${BT.escape(extra)}` : ""); } function clear() { if (logEl) logEl.textContent = ""; } function append(entry) { if (!logEl) return; const atBottom = logEl.scrollHeight - logEl.scrollTop - logEl.clientHeight < 40; const span = document.createElement("span"); const cls = classify(entry); if (cls) span.className = cls; span.textContent = entry.text + "\n"; logEl.appendChild(span); if (atBottom) logEl.scrollTop = logEl.scrollHeight; } function close() { if (source) { source.close(); source = null; } } function attach(id) { close(); source = new EventSource(BT.url(`/api/jobs/${id}/events`)); setStatus("busy", "läuft …"); source.addEventListener("line", (e) => append(JSON.parse(e.data))); source.addEventListener("end", (e) => { const info = JSON.parse(e.data); job = info; const [kind, text] = LABEL[info.status] || ["", info.status]; setStatus(kind, text, `${info.duration}s · Exit-Code ${info.returncode}`); close(); if (cfg.onFinish) cfg.onFinish(info); }); source.onerror = () => { if (source && source.readyState === EventSource.CLOSED) { close(); setStatus("warn", "Verbindung zum Protokoll unterbrochen"); } }; } async function start(body) { clear(); setStatus("busy", "wird gestartet …"); try { job = await BT.api(endpoint, { method: "POST", body: JSON.stringify(body || {}) }); attach(job.id); if (cfg.onStart) cfg.onStart(job); return job; } catch (err) { setStatus("error", "Start fehlgeschlagen", err.message); BT.toast(err.message, "err"); throw err; } } async function cancel() { if (!job) return; try { await BT.api(`/api/jobs/${job.id}/cancel`, { method: "POST" }); BT.toast("Abbruch angefordert."); } catch (err) { BT.toast(err.message, "err"); } } function rebind(next) { if (next.log) logEl = next.log; if (next.status) statusEl = next.status; } return { start, attach, cancel, clear, setStatus, rebind, get job() { return job; }, get busy() { return Boolean(source); }, }; } window.BT.Runner = Runner; })();