babycam/app/templates/index.html

373 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Babycam</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0f0f0f;
color: #e0e0e0;
min-height: 100vh;
}
header {
background: #1a1a1a;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
border-bottom: 1px solid #2a2a2a;
}
header h1 { font-size: 1.2rem; font-weight: 600; }
header .dot {
width: 10px; height: 10px;
border-radius: 50%;
background: {{ '#4caf50' if status.cam_available else '#f44336' }};
}
.container { max-width: 900px; margin: 0 auto; padding: 1.5rem; }
/* Stream */
.stream-box {
background: #000;
border-radius: 12px;
overflow: hidden;
margin-bottom: 1.5rem;
aspect-ratio: 16/9;
display: flex;
align-items: center;
justify-content: center;
}
.stream-box img {
width: 100%;
height: 100%;
object-fit: cover;
touch-action: none;
user-select: none;
}
.stream-box .no-cam {
color: #555;
font-size: 0.9rem;
text-align: center;
padding: 2rem;
}
/* Status-Karten */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.card {
background: #1a1a1a;
border-radius: 10px;
padding: 1rem 1.25rem;
border: 1px solid #2a2a2a;
}
.card label {
font-size: 0.75rem;
color: #888;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.card .value {
font-size: 1rem;
font-weight: 500;
margin-top: 0.3rem;
word-break: break-all;
}
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.badge.on { background: #1b4d2e; color: #4caf50; }
.badge.off { background: #3d1a1a; color: #f44336; }
/* WLAN-Panel */
.panel {
background: #1a1a1a;
border-radius: 10px;
border: 1px solid #2a2a2a;
overflow: hidden;
}
.panel-header {
padding: 1rem 1.25rem;
border-bottom: 1px solid #2a2a2a;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-header h2 { font-size: 1rem; font-weight: 600; }
button {
background: #2a2a2a;
color: #e0e0e0;
border: 1px solid #3a3a3a;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.15s;
}
button:hover { background: #333; }
button.primary { background: #1565c0; border-color: #1976d2; }
button.primary:hover { background: #1976d2; }
#network-list { padding: 0.5rem 0; min-height: 3rem; }
.network-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.65rem 1.25rem;
cursor: pointer;
transition: background 0.1s;
border-radius: 6px;
margin: 0.1rem 0.5rem;
}
.network-item:hover { background: #252525; }
.network-item.selected { background: #1a2a3a; }
.network-name { font-size: 0.9rem; }
.network-signal { font-size: 0.75rem; color: #888; }
.connect-form {
padding: 1rem 1.25rem;
border-top: 1px solid #2a2a2a;
display: none;
gap: 0.75rem;
flex-direction: column;
}
.connect-form.visible { display: flex; }
.connect-form input {
background: #0f0f0f;
border: 1px solid #3a3a3a;
color: #e0e0e0;
padding: 0.6rem 0.9rem;
border-radius: 6px;
font-size: 0.9rem;
width: 100%;
}
.connect-form input:focus {
outline: none;
border-color: #1976d2;
}
#status-msg {
font-size: 0.85rem;
padding: 0 1.25rem 1rem;
color: #888;
}
#status-msg.ok { color: #4caf50; }
#status-msg.err { color: #f44336; }
</style>
</head>
<body>
<header>
<div class="dot"></div>
<h1>Babycam</h1>
</header>
<div class="container">
<!-- Stream -->
<a href="/live" style="text-decoration:none;">
<div class="stream-box">
{% if status.cam_available %}
<img src="/stream" alt="Kamera-Stream">
{% else %}
<div class="no-cam">
Kamera nicht verfügbar<br>
<small>Ribbon-Kabel verbunden?</small>
</div>
{% endif %}
</div>
</a>
<!-- Status-Karten -->
<div class="cards">
<div class="card">
<label>WLAN</label>
<div class="value">{{ status.wifi_ssid }}</div>
</div>
<div class="card">
<label>IP-Adressen</label>
<div class="value">{{ status.ips | join(", ") or "keine" }}</div>
</div>
<div class="card">
<label>Access Point</label>
<div class="value" style="display:flex;align-items:center;gap:.6rem;flex-wrap:wrap;">
<span class="badge {{ 'on' if status.ap_active else 'off' }}">
{{ "aktiv" if status.ap_active else "aus" }}
</span>
<span style="font-size:.75rem;color:#666;">
{{ {"on":"manuell an","off":"manuell aus","auto":"automatisch"}[status.ap_override] }}
</span>
</div>
<div style="display:flex;gap:.4rem;margin-top:.6rem;flex-wrap:wrap;">
<button onclick="setAP('on')" class="{{ 'primary' if status.ap_override == 'on' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">An</button>
<button onclick="setAP('off')" class="{{ 'primary' if status.ap_override == 'off' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">Aus</button>
<button onclick="setAP('auto')"class="{{ 'primary' if status.ap_override == 'auto' else '' }}" style="font-size:.75rem;padding:.3rem .7rem;">Auto</button>
</div>
</div>
<div class="card">
<label>Laufzeit</label>
<div class="value">{{ status.uptime }}</div>
</div>
<div class="card">
<label>Mikrofon-Lautstärke</label>
<div style="display:flex;align-items:center;gap:.75rem;margin-top:.5rem;">
<input type="range" id="gain-slider" min="0" max="500" step="10" value="200"
style="flex:1;accent-color:#1976d2;cursor:pointer;">
<span id="gain-label" style="font-size:.95rem;font-weight:500;min-width:3.5rem;text-align:right;">200%</span>
</div>
</div>
<div class="card">
<label>System</label>
<div style="display:flex;gap:.4rem;margin-top:.3rem;flex-wrap:wrap;">
<button onclick="systemAction('reboot')" style="font-size:.75rem;padding:.3rem .7rem;">↺ Neustart</button>
<button onclick="systemAction('shutdown')" style="font-size:.75rem;padding:.3rem .7rem;background:#3d1a1a;border-color:#5a2a2a;color:#f44336;">⏻ Ausschalten</button>
</div>
</div>
</div>
<!-- WLAN-Verwaltung -->
<div class="panel">
<div class="panel-header">
<h2>WLAN konfigurieren</h2>
<button onclick="scanNetworks()">Scannen</button>
</div>
<div id="network-list"><p style="padding:1rem;color:#555;font-size:.85rem;">Auf Scannen klicken…</p></div>
<div class="connect-form" id="connect-form">
<div style="font-size:.85rem;color:#aaa">Verbinden mit: <strong id="selected-ssid"></strong></div>
<input type="password" id="wifi-password" placeholder="WLAN-Passwort">
<button class="primary" onclick="connectWifi()">Verbinden</button>
</div>
<div id="status-msg"></div>
</div>
</div>
<script>
// Mikrofon-Gain
(async () => {
try {
const res = await fetch("/api/audio-gain");
const { gain } = await res.json();
const pct = Math.round(gain * 100);
document.getElementById("gain-slider").value = pct;
document.getElementById("gain-label").textContent = pct + "%";
} catch (e) {}
})();
let gainTimer;
document.getElementById("gain-slider").addEventListener("input", function () {
const pct = parseInt(this.value);
document.getElementById("gain-label").textContent = pct + "%";
clearTimeout(gainTimer);
gainTimer = setTimeout(() => {
fetch("/api/audio-gain", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ gain: pct / 100 }),
});
}, 300);
});
let selectedSSID = null;
async function scanNetworks() {
const list = document.getElementById("network-list");
list.innerHTML = '<p style="padding:1rem;color:#555;font-size:.85rem;">Scanne…</p>';
document.getElementById("connect-form").classList.remove("visible");
const res = await fetch("/wifi/scan");
const networks = await res.json();
if (!networks.length) {
list.innerHTML = '<p style="padding:1rem;color:#555;font-size:.85rem;">Keine Netzwerke gefunden.</p>';
return;
}
list.innerHTML = networks.map(n => `
<div class="network-item" onclick="selectNetwork('${n.ssid.replace(/'/g,"\\'")}', this)">
<span class="network-name">${n.ssid}</span>
<span class="network-signal">${n.signal}% &nbsp; ${n.security || "offen"}</span>
</div>
`).join("");
}
function selectNetwork(ssid, el) {
document.querySelectorAll(".network-item").forEach(i => i.classList.remove("selected"));
el.classList.add("selected");
selectedSSID = ssid;
document.getElementById("selected-ssid").textContent = ssid;
document.getElementById("connect-form").classList.add("visible");
document.getElementById("wifi-password").focus();
}
async function setAP(action) {
const res = await fetch(`/ap/${action}`, { method: "POST" });
const data = await res.json();
if (data.success) setTimeout(() => location.reload(), 800);
}
async function systemAction(action) {
const labels = { reboot: "Pi wirklich neu starten?", shutdown: "Pi wirklich ausschalten?" };
if (!confirm(labels[action])) return;
const res = await fetch(`/system/${action}`, { method: "POST" });
const data = await res.json();
alert(data.message);
}
async function connectWifi() {
if (!selectedSSID) return;
const password = document.getElementById("wifi-password").value;
const msg = document.getElementById("status-msg");
msg.className = "";
msg.textContent = "Verbinde…";
const res = await fetch("/wifi/connect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ssid: selectedSSID, password }),
});
const data = await res.json();
msg.className = data.success ? "ok" : "err";
msg.textContent = data.success ? "Verbunden!" : "Fehler: " + data.message;
if (data.success) {
setTimeout(() => location.reload(), 2000);
}
}
</script>
</body>
</html>