This commit is contained in:
2026-08-27 08:57:14 +02:00
commit dca69c04ad
9 changed files with 1392 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
const form = document.getElementById("command-form");
const nameInput = document.getElementById("name");
const commandInput = document.getElementById("command");
const list = document.getElementById("command-list");
const emptyNote = document.getElementById("empty-note");
const statusEl = document.getElementById("status");
const outputEl = document.getElementById("output");
function setStatus(message, kind) {
statusEl.textContent = message;
statusEl.className = "status" + (kind ? ` ${kind}` : "");
}
function showOutput(text) {
outputEl.hidden = !text;
outputEl.textContent = text || "";
}
function formatTerminal(command, output) {
const lines = [];
if (command) lines.push(`$ ${command}`);
if (output) lines.push(output);
return lines.join("\n");
}
async function api(url, options) {
const res = await fetch(url, options);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const err = new Error(data.error || `Fehler ${res.status}`);
err.data = data;
throw err;
}
return data;
}
function renderCommands(commands) {
list.replaceChildren();
emptyNote.hidden = commands.length > 0;
for (const cmd of commands) {
const li = document.createElement("li");
const info = document.createElement("div");
info.className = "info";
const name = document.createElement("span");
name.className = "name";
name.textContent = cmd.name;
const code = document.createElement("code");
code.textContent = cmd.command;
info.append(name, code);
const actions = document.createElement("div");
actions.className = "actions";
const runBtn = document.createElement("button");
runBtn.type = "button";
runBtn.textContent = "Ausführen";
runBtn.addEventListener("click", () => runCommand(cmd, runBtn));
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.className = "delete";
delBtn.textContent = "Löschen";
delBtn.addEventListener("click", () => deleteCommand(cmd, delBtn));
actions.append(runBtn, delBtn);
li.append(info, actions);
list.append(li);
}
}
async function loadCommands() {
try {
renderCommands(await api("/api/commands"));
} catch (err) {
setStatus(`Befehle konnten nicht geladen werden: ${err.message}`, "error");
}
}
async function runCommand(cmd, button) {
button.disabled = true;
setStatus(`${cmd.name}" wird gesendet …`);
showOutput("");
try {
const result = await api(`/api/commands/${cmd.id}/run`, { method: "POST" });
setStatus(
result.ok
? `${cmd.name}" wurde gesendet.`
: `cec-client hat sich mit Exit-Code ${result.exitCode} beendet.`,
result.ok ? "ok" : "error"
);
showOutput(formatTerminal(result.command, result.output));
} catch (err) {
setStatus(`Ausführen fehlgeschlagen: ${err.message}`, "error");
showOutput(formatTerminal(err.data?.command, err.data?.output));
} finally {
button.disabled = false;
}
}
async function deleteCommand(cmd, button) {
button.disabled = true;
try {
await api(`/api/commands/${cmd.id}`, { method: "DELETE" });
setStatus(`${cmd.name}" wurde gelöscht.`, "ok");
await loadCommands();
} catch (err) {
setStatus(`Löschen fehlgeschlagen: ${err.message}`, "error");
button.disabled = false;
}
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const submitBtn = form.querySelector("button[type=submit]");
submitBtn.disabled = true;
try {
await api("/api/commands", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: nameInput.value.trim(),
command: commandInput.value.trim(),
}),
});
setStatus(`${nameInput.value.trim()}" wurde gespeichert.`, "ok");
form.reset();
nameInput.focus();
await loadCommands();
} catch (err) {
setStatus(`Speichern fehlgeschlagen: ${err.message}`, "error");
} finally {
submitBtn.disabled = false;
}
});
loadCommands();
+49
View File
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CEC-Tool</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>📺 CEC-Tool</h1>
<p class="subtitle">HDMI-CEC-Befehle speichern und über <code>cec-client</code> senden.</p>
<form id="command-form">
<fieldset>
<legend>Neuen Befehl anlegen</legend>
<div class="field">
<label for="name">Name</label>
<input type="text" id="name" name="name" required maxlength="60"
placeholder="z. B. TV einschalten" autocomplete="off">
</div>
<div class="field">
<label for="command">Command</label>
<input type="text" id="command" name="command" required maxlength="200"
placeholder="z. B. echo 'on 0' | cec-client -s -d 1" autocomplete="off" aria-describedby="command-help">
<span id="command-help" class="hint">
Vollständiger Shell-Befehl, z. B. <code>echo 'on 0' | cec-client -s -d 1</code> oder <code>echo 'scan' | cec-client -s -d 1</code>
</span>
</div>
<button type="submit">Speichern</button>
</fieldset>
</form>
<section aria-labelledby="list-heading">
<h2 id="list-heading">Gespeicherte Befehle</h2>
<ul id="command-list" class="command-list"></ul>
<p id="empty-note" class="hint" hidden>Noch keine Befehle gespeichert.</p>
</section>
<section aria-labelledby="output-heading">
<h2 id="output-heading">Ausgabe</h2>
<p id="status" class="status" role="status" aria-live="polite"></p>
<pre id="output" class="output" hidden></pre>
</section>
</main>
<script src="app.js"></script>
</body>
</html>
+176
View File
@@ -0,0 +1,176 @@
:root {
color-scheme: light dark;
--bg: #f6f7f9;
--surface: #ffffff;
--text: #1c1f23;
--muted: #5c6570;
--border: #d6dbe1;
--accent: #0b57d0;
--accent-text: #ffffff;
--danger: #b3261e;
--ok: #146c2e;
--output-bg: #14181d;
--output-text: #d7e0ea;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14181d;
--surface: #1d2229;
--text: #e6eaef;
--muted: #9aa4af;
--border: #39414b;
--accent: #6ea8ff;
--accent-text: #0b1526;
--danger: #ff8a80;
--ok: #7bd88f;
--output-bg: #0c0f13;
--output-text: #c8d2dc;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, sans-serif;
line-height: 1.5;
background: var(--bg);
color: var(--text);
}
main {
max-width: 42rem;
margin: 0 auto;
padding: 1.5rem 1rem 3rem;
}
h1 { margin-bottom: 0.25rem; }
.subtitle { margin-top: 0; color: var(--muted); }
code {
font-family: ui-monospace, monospace;
font-size: 0.9em;
background: color-mix(in srgb, var(--border) 40%, transparent);
padding: 0.1em 0.35em;
border-radius: 4px;
}
fieldset {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1rem 1.25rem 1.25rem;
}
legend {
font-weight: 600;
padding: 0 0.4rem;
}
.field { margin-bottom: 1rem; }
label {
display: block;
font-weight: 600;
margin-bottom: 0.2rem;
}
input[type="text"] {
width: 100%;
padding: 0.5rem 0.65rem;
font: inherit;
color: inherit;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
}
input:focus-visible,
button:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
input:user-invalid {
border-color: var(--danger);
}
.hint {
display: block;
margin-top: 0.25rem;
font-size: 0.85rem;
color: var(--muted);
}
button {
font: inherit;
font-weight: 600;
padding: 0.45rem 1rem;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
background: var(--accent);
color: var(--accent-text);
}
button:hover { filter: brightness(1.1); }
button:disabled { opacity: 0.55; cursor: wait; }
.command-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.command-list li {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.6rem 0.9rem;
}
.command-list .info {
flex: 1;
min-width: 10rem;
}
.command-list .name {
font-weight: 600;
display: block;
}
.command-list .actions {
display: flex;
gap: 0.5rem;
}
button.delete {
background: transparent;
color: var(--danger);
border-color: var(--danger);
}
.status { min-height: 1.5em; margin: 0.25rem 0 0.5rem; }
.status.ok { color: var(--ok); }
.status.error { color: var(--danger); }
.output {
background: var(--output-bg);
color: var(--output-text);
border-radius: 10px;
padding: 0.9rem 1rem;
font-family: ui-monospace, monospace;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-word;
max-height: 20rem;
overflow-y: auto;
margin: 0;
}