338 lines
9.5 KiB
HTML
338 lines
9.5 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;
|
|
}
|
|
|
|
.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>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>
|
|
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}% ${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>
|