init
This commit is contained in:
+138
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user