Files
ki 21b4128f19 STYLE+FIX: power.html Redesign + Auth wiederhergestellt
- power.html komplett neu gestaltet im Design-System von index.html
  (CSS-Variablen, gleiche Typografie, Farben, Abstände)
- Status-Banner, Card-Layout, Toggle-Styles, Buttons alle einheitlich
- fetch('/api/power') mit credentials:'include' für Browser-Auth
- fetch('/api/wake') mit credentials:'include' + Fehlerbehandlung
- Auth auf /api/power wieder aktiviert (POST-Schutz) mit
  credentials:'include' Browser-Auth
2026-08-31 03:20:08 +04:00

273 lines
9.9 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Energie — ePaper Dashboard</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%230a0a0f'/%3E%3Crect x='10' y='10' width='80' height='80' fill='%23fafafa'/%3E%3Crect x='15' y='15' width='35' height='35' fill='%230080ff'/%3E%3Crect x='50' y='15' width='35' height='35' fill='%23ff0080'/%3E%3Crect x='15' y='50' width='35' height='35' fill='%2300cc00'/%3E%3Crect x='50' y='50' width='35' height='35' fill='%23ff8000'/%3E%3C/svg%3E">
<style>
:root {
--bg: #0a0a0f;
--surface: #15151c;
--surface-2: #1f1f28;
--surface-3: #2a2a35;
--border: #2e2e3a;
--border-light: #3a3a48;
--fg: #f0f0f5;
--fg-muted: #9090a0;
--fg-dim: #6a6a78;
--accent: #6d8eff;
--accent-bright: #8da9ff;
--accent-glow: rgba(109, 142, 255, 0.15);
--ok: #4ade80;
--warn: #fbbf24;
--alert: #f87171;
--info: #60a5fa;
--success: #22c55e;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
background: var(--bg); color: var(--fg);
margin: 0; line-height: 1.5;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.container { max-width: 1400px; margin: 0 auto; padding: 24px 32px 80px; }
/* Header */
.page-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 32px; padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.page-header h1 { font-size: 22px; font-weight: 700; margin: 0; color: var(--fg); }
.back-btn {
display: inline-flex; align-items: center; gap: 8px;
background: var(--surface-2); color: var(--fg-muted);
border: 1px solid var(--border); border-radius: 8px;
padding: 8px 16px; font-size: 14px; font-weight: 500;
text-decoration: none; cursor: pointer; transition: all 0.15s;
}
.back-btn:hover { background: var(--surface-3); color: var(--fg); }
/* Status Banner */
.status-banner {
display: flex; align-items: center; gap: 16px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 12px; padding: 16px 20px; margin-bottom: 24px;
}
.status-pill {
display: inline-flex; align-items: center; justify-content: center;
min-width: 70px; padding: 6px 14px; border-radius: 20px;
font-size: 13px; font-weight: 600; letter-spacing: 0.3px;
}
.status-pill.wach { background: rgba(74, 222, 128, 0.15); color: var(--ok); }
.status-pill.schlaft { background: rgba(251, 191, 36, 0.15); color: var(--warn); animation: pulse 2s ease-in-out infinite; }
.status-text { font-size: 14px; color: var(--fg-muted); flex: 1; }
.status-text strong { color: var(--fg); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 16px;
}
.card-title {
font-size: 13px; font-weight: 600; color: var(--fg-muted);
text-transform: uppercase; letter-spacing: 0.5px;
margin: 0 0 16px;
}
.form-row {
display: flex; align-items: center; gap: 12px;
padding: 14px 0; border-bottom: 1px solid var(--border);
}
.form-row:last-child { border-bottom: none; padding-bottom: 0; }
.form-row label { flex: 1; font-size: 14px; color: var(--fg); }
.form-row .hint { font-size: 12px; color: var(--fg-dim); display: block; margin-top: 2px; }
/* Toggle */
.toggle { position: relative; width: 44px; height: 24px; flex-shrink: 0; }
.toggle input { opacity: 0; width: 0; height: 0; }
.slider {
position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
background: var(--surface-3); border-radius: 12px; transition: 0.2s;
border: 1px solid var(--border-light);
}
.slider::before {
content: ""; position: absolute; height: 18px; width: 18px;
left: 3px; bottom: 2px; background: var(--fg-dim);
border-radius: 50%; transition: 0.2s;
}
.toggle input:checked + .slider { background: rgba(74, 222, 128, 0.2); border-color: var(--ok); }
.toggle input:checked + .slider::before { transform: translateX(20px); background: var(--ok); }
/* Time inputs */
input[type="time"] {
padding: 8px 12px; border: 1px solid var(--border);
border-radius: 8px; font-size: 14px; background: var(--surface-2);
color: var(--fg); outline: none; transition: border-color 0.15s;
}
input[type="time"]:focus { border-color: var(--accent); }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 8px;
background: var(--accent); color: #fff;
border: none; padding: 10px 20px; border-radius: 8px;
font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s;
}
.btn:hover { background: var(--accent-bright); }
.btn.secondary { background: var(--surface-3); color: var(--fg-muted); border: 1px solid var(--border); }
.btn.secondary:hover { background: var(--surface-3); color: var(--fg); }
.btn-group { display: flex; gap: 12px; margin-top: 20px; }
/* Info text */
.info-box {
background: var(--accent-glow); border: 1px solid rgba(109, 142, 255, 0.3);
border-radius: 8px; padding: 14px 16px; margin-top: 20px;
}
.info-box p { margin: 0; font-size: 13px; color: var(--fg-muted); line-height: 1.6; }
/* Error */
.error-msg { color: var(--alert); font-size: 13px; margin-top: 8px; display: none; }
.error-msg.visible { display: block; }
</style>
</head>
<body>
<div class="container">
<div class="page-header">
<h1>⚡ Energie</h1>
<a class="back-btn" href="/">← Dashboard</a>
</div>
<div class="status-banner">
<span id="status-pill" class="status-pill schlaft">—</span>
<span id="status-text" class="status-text">Lädt...</span>
</div>
<form method="post" action="/power" id="power-form">
<div class="card">
<div class="form-row">
<label for="deepsleep_enabled">
DeepSleep aktiv
<span class="hint">Display schaltet sich nachts aus, nur noch Netzwerk aktiv</span>
</label>
<label class="toggle">
<input type="checkbox" name="deepsleep_enabled" id="deepsleep_enabled"
{% if power.deepsleep_enabled %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
<div class="form-row">
<label for="deepsleep_start">Schlafenszeit (von)</label>
<input type="time" name="deepsleep_start" id="deepsleep_start"
value="{{ power.deepsleep_start or '23:00' }}">
</div>
<div class="form-row">
<label for="deepsleep_end">Aufwachzeit (bis)</label>
<input type="time" name="deepsleep_end" id="deepsleep_end"
value="{{ power.deepsleep_end or '06:00' }}">
</div>
<div class="form-row">
<label for="cpu_throttle">
CPU drosseln
<span class="hint">Reduziert Dauerstromverbrauch um ~0.5–1W</span>
</label>
<label class="toggle">
<input type="checkbox" name="cpu_throttle" id="cpu_throttle"
{% if power.cpu_throttle %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
<div class="form-row">
<label for="wifi_off">
WLAN nachts aus
<span class="hint">Schaltet WLAN nach Schlafenszeit ab (wenn implementiert)</span>
</label>
<label class="toggle">
<input type="checkbox" name="wifi_off" id="wifi_off"
{% if power.wifi_off %}checked{% endif %}>
<span class="slider"></span>
</label>
</div>
</div>
<div id="error-msg" class="error-msg"></div>
<div class="btn-group">
<button type="submit" class="btn">💾 Speichern</button>
<button type="button" class="btn secondary" id="wake-btn">⚡ Aufwecken</button>
</div>
</form>
<div class="info-box">
<p><strong>Z-Indikator:</strong> Auf dem Display erscheint oben rechts ein "Zz" + Aufwach-Zeit wenn DeepSleep aktiv ist.</p>
<p style="margin-top:8px">Geschätzte Ersparnis: bei 7h DeepSleep/Tag + 3min Refresh-Intervall ca. 30% weniger Display-Refresh-Strom. Plus Pi-CPU-Throttle ~0.5–1W Dauerersparnis.</p>
</div>
</div>
<script>
async function refreshStatus() {
try {
const r = await fetch('/api/power', { credentials: 'include' });
if (!r.ok) throw new Error('Status nicht abrufbar');
const d = await r.json();
const pill = document.getElementById('status-pill');
const text = document.getElementById('status-text');
if (d.is_deepsleep) {
pill.className = 'status-pill schlaft';
pill.textContent = 'schläft';
text.innerHTML = `Display schläft bis <strong>${d.next_wake}</strong> `
+ `(${formatDuration(d.seconds_until_wake)} verbleibend)`;
} else {
pill.className = 'status-pill wach';
pill.textContent = 'wach';
const next = d.deepsleep_enabled ? ` Nächster DeepSleep um ${d.deepsleep_start}.` : '';
text.innerHTML = `Display ist wach. Normaler Refresh-Modus.${next}`;
}
} catch (e) {
document.getElementById('status-text').textContent = 'Status nicht abrufbar (' + e.message + ')';
}
}
function formatDuration(secs) {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
document.getElementById('wake-btn').addEventListener('click', async () => {
const btn = document.getElementById('wake-btn');
btn.disabled = true;
try {
const r = await fetch('/api/wake', { method: 'POST', credentials: 'include' });
const d = await r.json();
if (d.ok) {
btn.textContent = '✓ Aufgeweckt!';
setTimeout(() => { btn.textContent = '⚡ Aufwecken'; btn.disabled = false; }, 2000);
} else {
throw new Error(d.message || 'Fehler');
}
} catch (e) {
document.getElementById('error-msg').textContent = 'Aufwecken fehlgeschlagen: ' + e.message;
document.getElementById('error-msg').classList.add('visible');
btn.disabled = false;
}
});
// Init
refreshStatus();
setInterval(refreshStatus, 30000);
</script>
</body>
</html>