Web frontend

This commit is contained in:
2026-08-27 12:32:20 +02:00
parent d44c24ec97
commit edb6e5e027
97 changed files with 9535 additions and 195 deletions

View File

@@ -0,0 +1,167 @@
/** Parent mode: reachable only via `?parentMode=1`, never from the child-facing UI.
*
* This hides the settings, it does not protect them - there is no login, and the
* endpoints behind it are as open as the rest of the API. That matches the device's
* threat model (a box on a home network) and is stated in the README rather than
* implied.
*/
import { useEffect, useState } from "react";
import { api } from "../api/client";
import type { Settings } from "../api/types";
const FIELDS: Array<[keyof Settings, string, string, number, number, number]> = [
["min_volume", "Minimale Lautstärke", "Untergrenze am Gerät (0-200)", 0, 200, 1],
["max_volume", "Maximale Lautstärke", "Was 100 % in der Kinder-Ansicht bedeutet", 0, 200, 1],
["initial_volume", "Start-Lautstärke", "Beim Einschalten", 0, 200, 1],
["volume_increment", "Schritt am Drehknopf", "Pro Rasterung", 1, 100, 1],
["button_leds_brightness", "Tastenbeleuchtung", "0 bis 1", 0, 1, 0.05],
];
export function ParentPanel({ onClose }: { onClose: () => void }) {
const [settings, setSettings] = useState<Settings | null>(null);
const [status, setStatus] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
api
.settings()
.then(setSettings)
.catch((cause: unknown) => setStatus(String(cause)));
}, []);
const save = async () => {
if (!settings) return;
setBusy(true);
setStatus(null);
try {
setSettings(await api.saveSettings(settings));
setStatus("Gespeichert.");
} catch {
// The backend rejects an inverted range or an out-of-band start volume.
setStatus("Nicht gespeichert — bitte die Werte prüfen (min ≤ Start ≤ max).");
} finally {
setBusy(false);
}
};
const rescan = async () => {
setStatus("Bibliothek wird eingelesen …");
await api.refreshLibrary();
setStatus("Einlesen gestartet. Neue Titel erscheinen gleich von selbst.");
};
return (
<div className="overlay" style={{ zIndex: 6, background: "oklch(15% 0.03 210 / .75)" }}>
<div className="sheet" style={{ padding: 28, width: 460, maxHeight: "100%", overflow: "auto" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ fontSize: 20, fontWeight: 900, color: "var(--ink)", flex: 1 }}>
🔧 Eltern-Einstellungen
</div>
<button
onClick={onClose}
aria-label="Schließen"
style={{
border: "none",
cursor: "pointer",
background: "oklch(88% 0.02 210)",
color: "var(--ink)",
fontSize: 18,
fontWeight: 900,
width: 36,
height: 36,
borderRadius: 999,
}}
>
×
</button>
</div>
{settings === null ? (
<div style={{ marginTop: 18, color: "oklch(45% 0.03 210)" }}>Wird geladen </div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 14, marginTop: 18 }}>
{FIELDS.map(([key, label, hint, min, max, step]) => (
<label key={key} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
<span style={{ fontSize: 14, fontWeight: 800, color: "var(--ink)" }}>{label}</span>
<span style={{ fontSize: 12, fontWeight: 600, color: "oklch(50% 0.03 210)" }}>
{hint}
</span>
<input
type="number"
min={min}
max={max}
step={step}
value={settings[key]}
onChange={(event) =>
setSettings({ ...settings, [key]: Number(event.target.value) })
}
style={{
padding: "8px 12px",
borderRadius: 10,
border: "2px solid oklch(85% 0.02 210)",
fontSize: 16,
fontWeight: 700,
}}
/>
</label>
))}
<div style={{ display: "flex", gap: 10, marginTop: 6 }}>
<button
onClick={() => void save()}
disabled={busy}
style={{
border: "none",
cursor: "pointer",
background: "var(--accent)",
color: "#fff",
fontSize: 15,
fontWeight: 800,
padding: "11px 20px",
borderRadius: 999,
}}
>
Speichern
</button>
<button
onClick={() => void rescan()}
style={{
border: "none",
cursor: "pointer",
background: "oklch(88% 0.02 210)",
color: "var(--ink)",
fontSize: 15,
fontWeight: 800,
padding: "11px 20px",
borderRadius: 999,
}}
>
Bibliothek neu einlesen
</button>
</div>
<div
style={{
fontSize: 12,
fontWeight: 600,
color: "oklch(50% 0.03 210)",
lineHeight: 1.5,
}}
>
Speichern schreibt in die <code>config.yml</code> auf dem Gerät. Die neue
Obergrenze gilt sofort, ohne Neustart.
</div>
</div>
)}
{status && (
<div style={{ marginTop: 14, fontSize: 14, fontWeight: 700, color: "var(--ink)" }}>
{status}
</div>
)}
</div>
</div>
);
}