chore: initial import
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
/* ==========================================================================
|
||||
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 <mount>/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 = `<span class="bt-dot"></span><b>${BT.escape(text)}</b>` +
|
||||
(extra ? ` <span class="bt-muted">${BT.escape(extra)}</span>` : "");
|
||||
}
|
||||
|
||||
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;
|
||||
})();
|
||||
Reference in New Issue
Block a user