Files
script-runner/web/index.html
2025-10-07 19:31:41 +02:00

205 lines
7.1 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th, td { padding: .6rem .5rem; border-bottom: 1px solid #ddd; vertical-align: top; }
.btn { padding: .4rem .7rem; border: 1px solid #888; border-radius: .5rem; background:#f8f8f8; cursor:pointer; }
.pill { padding:.15rem .45rem; border-radius:999px; font-size:.85rem; display:inline-block; }
.pill.on { background:#e6ffed; border:1px solid #b7ebc6; color:#165b31; }
.pill.off { background:#fff2e5; border:1px solid #ffd4a8; color:#8a4b16; }
.pill.run { background:#e7efff; border:1px solid #b9c8ff; color:#1f3d8a; }
.btn:hover { background:#eee; }
.pill { padding:.15rem .45rem; border-radius:999px; font-size:.85rem; }
.ok { background:#e6ffed; border:1px solid #b7ebc6; }
.bad { background:#ffecec; border:1px solid #ffb3b3; }
.muted { color:#666; }
pre { white-space: pre-wrap; background:#fafafa; border:1px solid #eee; padding:.5rem; border-radius:.5rem; }
.row { display:flex; gap:.4rem; align-items:center; flex-wrap: wrap; }
.grow { flex:1 }
.small { font-size: .9rem; }
</style>
</head>
<body>
<h1>Script Runner</h1>
<p class="muted small">Gestartet: {{.Started}} &middot; Lauscht auf: {{.ListenHost}}</p>
<table>
<thead>
<tr>
<th style="width:18%">Name</th>
<th>Pfad</th>
<th style="width:10%">Status</th>
<th style="width:14%">Intervall</th>
<th style="width:10%">Nächster Lauf</th>
<th style="width:26%">Aktionen</th>
</tr>
</thead>
<tbody>
{{range .Tasks}}
<tr id="row-{{.Name}}">
<td><strong>{{.Name}}</strong></td>
<td><code>{{.Path}}</code></td>
<td>
<span
id="status-{{.Name}}"
class="pill {{if .Enabled}}on{{else}}off{{end}}">
{{if .Enabled}}Aktiv{{else}}Deaktiviert{{end}}
</span>
<span id="running-{{.Name}}" class="pill run" style="display:none;">Läuft…</span>
</td>
<td>
<div class="row">
<input class="grow" type="text" id="ival-{{.Name}}" value="{{.Interval}}" placeholder="z.B. 5m, 1h"/>
<button class="btn" onclick="setInterval('{{.Name}}', document.getElementById('ival-{{.Name}}').value, 1)">Setzen</button>
</div>
<div class="small muted">leer/0 = aus</div>
</td>
<td>
<span id="next-{{.Name}}" class="muted"></span>
</td>
<td>
<div class="row">
<button class="btn" onclick="runNow('{{.Name}}')">Jetzt starten</button>
<button class="btn" onclick="toggle('{{.Name}}', true)">Aktivieren</button>
<button class="btn" onclick="toggle('{{.Name}}', false)">Deaktivieren</button>
<button class="btn" onclick="cancelRun('{{.Name}}')">Abbrechen</button>
<button class="btn" onclick="openLogs('{{.Name}}')">Logs</button>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
<dialog id="logdlg">
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;">
<h3 id="logtitle">Logs</h3>
<button class="btn" onclick="document.getElementById('logdlg').close()">Schließen</button>
</div>
<div id="logcontent"></div>
</dialog>
<script>
const CSRF = "{{.CSRFToken}}";
async function api(path, form) {
const body = new URLSearchParams(form || {});
const res = await fetch(path, {
method: "POST",
headers: {"X-CSRF-Token": CSRF},
body
});
if (!res.ok) throw new Error(await res.text());
return res.text();
}
async function runNow(name) {
try { await api("/api/run", {name, csrf_token: CSRF}); alert("Gestartet."); }
catch(e){ alert(e.message); }
}
async function setInterval(name, interval, enable) {
try { await api("/api/set-interval", {name, interval, enable: enable?1:0, csrf_token: CSRF}); alert("Gespeichert."); }
catch(e){ alert(e.message); }
await refreshRow(name);
}
async function toggle(name, enable) {
try { await api("/api/toggle", {name, enable: enable?1:0, csrf_token: CSRF}); }
catch(e){ alert(e.message); }
await refreshRow(name);
}
async function cancelRun(name) {
try { await api("/api/cancel", {name, csrf_token: CSRF}); }
catch(e){ alert(e.message); }
}
async function refreshRow(name) {
const res = await fetch("/api/logs?name="+encodeURIComponent(name));
if (!res.ok) return;
const data = await res.json();
// Next run
document.getElementById("next-"+name).textContent =
data.next_run ? new Date(data.next_run).toLocaleString() : "";
// Status-Badge (Aktiv/Deaktiviert)
const st = document.getElementById("status-"+name);
st.textContent = data.enabled ? "Aktiv" : "Deaktiviert";
st.classList.remove("on","off");
st.classList.add(data.enabled ? "on" : "off");
// Running-Badge
const run = document.getElementById("running-"+name);
if (data.running) {
run.style.display = "inline-block";
} else {
run.style.display = "none";
}
}
async function openLogs(name) {
const res = await fetch("/api/logs?name="+encodeURIComponent(name));
if (!res.ok) { alert(await res.text()); return; }
const data = await res.json();
document.getElementById("logtitle").textContent = "Logs " + name;
const box = document.getElementById("logcontent");
box.innerHTML = "";
if (!data.history || !data.history.length) {
box.textContent = "Keine Einträge.";
} else {
data.history.forEach(e => {
const wrap = document.createElement("div");
wrap.style.margin = "1rem 0";
const badge = document.createElement("span");
badge.className = "pill " + (e.success ? "ok" : "bad");
badge.textContent = e.success ? "OK" : "Fehler";
const head = document.createElement("div");
head.className = "row";
const meta = document.createElement("div");
meta.textContent = new Date(e.time).toLocaleString() + " · " + (e.manual ? "manuell" : "geplant") + " · " + ms(e.duration);
head.appendChild(badge);
head.appendChild(meta);
const pre = document.createElement("pre");
pre.textContent = e.output || "(kein Output)";
wrap.appendChild(head);
wrap.appendChild(pre);
box.appendChild(wrap);
});
}
document.getElementById("logdlg").showModal();
}
function ms(ns) {
// ns ist eigentlich Duration in ns; Browser interpretiert ggf. anders zur Sicherheit:
if (typeof ns === "number") {
const ms = ns / 1e6;
if (ms < 1000) return ms.toFixed(0) + " ms";
const s = ms/1000;
if (s < 60) return s.toFixed(2) + " s";
const m = Math.floor(s/60);
const rest = (s%60).toFixed(0);
return m + " m " + rest + " s";
}
return String(ns);
}
async function init() {
// erste NextRun-Anzeige laden
const rows = document.querySelectorAll("tbody tr[id^='row-']");
for (const tr of rows) {
const name = tr.id.replace("row-","");
await refreshRow(name);
}
}
init();
</script>
</body>
</html>