Private
Public Access
Initiales Halloween-Projekt
This commit is contained in:
commit
e071fbad2a
36 files changed
+2220
No files matched your search
@@ -0,0 +1,123 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Halloween-Grab · Steuerung</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; font-family: system-ui, sans-serif; background: #111718; color: #eef0e9; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; }
|
||||
header { padding: 1.1rem max(1rem, calc((100vw - 1120px)/2)); background: #182122; border-bottom: 1px solid #3a4644; }
|
||||
h1 { margin: 0; font-size: 1.5rem; }
|
||||
header p { margin: .35rem 0 0; color: #c1cbc2; }
|
||||
main { max-width: 1120px; margin: auto; padding: 1rem; display: grid; gap: 1rem; grid-template-columns: minmax(0, 2fr) minmax(290px, 1fr); }
|
||||
section { background: #1a2424; border: 1px solid #394846; border-radius: 12px; padding: 1rem; }
|
||||
h2 { margin: 0 0 .7rem; font-size: 1.1rem; }
|
||||
.camera { width: 100%; aspect-ratio: 4/3; object-fit: contain; background: #050808; border-radius: 8px; }
|
||||
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }
|
||||
button, select, a.download { border: 1px solid #5c6c67; border-radius: 7px; background: #293938; color: #f4f5ed; font: inherit; padding: .65rem .8rem; cursor: pointer; }
|
||||
button:hover, a.download:hover { background: #36504a; }
|
||||
button:focus-visible, select:focus-visible, a:focus-visible { outline: 3px solid #ffb44f; outline-offset: 2px; }
|
||||
button.danger { background: #782c26; border-color: #c76652; font-weight: 700; }
|
||||
button.danger:hover { background: #9c352c; }
|
||||
button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
a.download { display: inline-block; text-decoration: none; }
|
||||
.value { font-weight: 650; color: #ffca78; }
|
||||
.notice { border-left: 4px solid #ffb44f; padding: .55rem .8rem; background: #302b21; margin: .8rem 0; }
|
||||
.muted { color: #bfcbc6; }
|
||||
#message { min-height: 1.5rem; color: #ffca78; }
|
||||
dl { display: grid; grid-template-columns: auto 1fr; gap: .4rem .8rem; margin: .5rem 0 1rem; }
|
||||
dt { color: #adbfba; } dd { margin: 0; }
|
||||
@media (max-width: 760px) { main { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Halloween-Grab</h1>
|
||||
<p>Lokale Kameraansicht und Steuerung</p>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2>Livebild</h2>
|
||||
<img class="camera" src="/stream.mjpg" alt="Livebild der Kamera">
|
||||
<p class="muted" id="camera-status">Kamera wird gestartet …</p>
|
||||
<a class="download" href="/snapshot.jpg" download="halloween-snapshot.jpg">Aktuelles Bild herunterladen</a>
|
||||
<p class="notice">Aufnahmen und Personenerkennung sind noch nicht aktiviert. Der Aufnahmebereich muss vor dem Außeneinsatz festgelegt werden.</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Betrieb</h2>
|
||||
<dl>
|
||||
<dt>Modus</dt><dd class="value" id="mode-label">Lädt …</dd>
|
||||
<dt>Zustand</dt><dd id="phase">Lädt …</dd>
|
||||
<dt>Sperrzeit</dt><dd id="cooldown">–</dd>
|
||||
<dt>Letzter Test</dt><dd id="last-event">–</dd>
|
||||
</dl>
|
||||
<label for="mode">Modus wählen</label>
|
||||
<div class="row" style="margin: .5rem 0 1rem">
|
||||
<select id="mode">
|
||||
<option value="voll">Voller Schreck</option>
|
||||
<option value="sanft">Sanft</option>
|
||||
<option value="atmosphaere">Atmosphäre</option>
|
||||
<option value="aus">Aus</option>
|
||||
</select>
|
||||
<button id="save-mode">Übernehmen</button>
|
||||
</div>
|
||||
<p class="notice"><strong>Aktoren nur simuliert.</strong> Kein Motor, Licht oder Ton ist mit dieser Version verbunden. Es gibt noch keine automatische Personenauslösung.</p>
|
||||
<div class="row">
|
||||
<button id="zone-clear">Bereich frei (Test)</button>
|
||||
<button id="test-trigger">Testauslösung</button>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1rem">
|
||||
<button class="danger" id="stop">Stopp</button>
|
||||
<button id="reset">Nach Stopp zurücksetzen</button>
|
||||
</div>
|
||||
<p id="message" role="status" aria-live="polite"></p>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const $ = id => document.getElementById(id);
|
||||
const names = { voll: 'Voller Schreck', sanft: 'Sanft', atmosphaere: 'Atmosphäre', aus: 'Aus' };
|
||||
async function api(path, body = {}) {
|
||||
const response = await fetch(path, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Halloween-UI': '1' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.detail || 'Anfrage fehlgeschlagen');
|
||||
return data;
|
||||
}
|
||||
async function refresh() {
|
||||
try {
|
||||
const response = await fetch('/api/status', { cache: 'no-store' });
|
||||
const data = await response.json();
|
||||
const c = data.controller, cam = data.camera;
|
||||
$('mode-label').textContent = names[c.mode] || c.mode;
|
||||
if (document.activeElement !== $('mode')) $('mode').value = c.mode;
|
||||
$('phase').textContent = c.phase;
|
||||
$('cooldown').textContent = c.cooldown_seconds ? c.cooldown_seconds + ' s' : '–';
|
||||
$('last-event').textContent = c.last_event ? c.last_event.time + ' · ' + names[c.last_event.mode] : '–';
|
||||
$('camera-status').textContent = cam.error || (cam.ready ? cam.source + ' · Bild aktiv' : cam.source + ' · wartet auf Bild');
|
||||
if (c.config_error) $('message').textContent = c.config_error;
|
||||
} catch (error) {
|
||||
$('camera-status').textContent = 'Server nicht erreichbar';
|
||||
$('message').textContent = error.message;
|
||||
}
|
||||
}
|
||||
async function action(path, body) {
|
||||
try {
|
||||
const data = await api(path, body);
|
||||
$('message').textContent = data.reason || 'Einstellung übernommen';
|
||||
await refresh();
|
||||
} catch (error) { $('message').textContent = error.message; }
|
||||
}
|
||||
$('save-mode').onclick = () => action('/api/mode', { mode: $('mode').value });
|
||||
$('zone-clear').onclick = () => action('/api/zone-clear');
|
||||
$('test-trigger').onclick = () => action('/api/test-trigger');
|
||||
$('stop').onclick = () => action('/api/stop');
|
||||
$('reset').onclick = () => action('/api/reset');
|
||||
refresh();
|
||||
setInterval(refresh, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user