init
This commit is contained in:
204
web/index.html
Normal file
204
web/index.html
Normal file
@@ -0,0 +1,204 @@
|
||||
<!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}} · 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>
|
||||
Reference in New Issue
Block a user