diff --git a/apps/admin-ui/src/app/android-tv/page.tsx b/apps/admin-ui/src/app/android-tv/page.tsx new file mode 100644 index 0000000..02fa1cf --- /dev/null +++ b/apps/admin-ui/src/app/android-tv/page.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import Link from "next/link"; +import { Nav, useAuth } from "@/components/Nav"; + +type ApkMeta = { + available?: boolean; + flavor?: string; + versionCode?: number | null; + versionName?: string | null; + changelog?: string | null; + filename?: string; + sizeBytes?: number; + builtAt?: string | null; + productName?: string; + note?: string; +}; + +function formatBytes(n: number): string { + if (!n || n <= 0) return "—"; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(0)} KB`; + return `${(n / (1024 * 1024)).toFixed(1)} MB`; +} + +export default function AndroidTvOtaPage() { + useAuth(); + const fileRef = useRef(null); + const [meta, setMeta] = useState(null); + const [err, setErr] = useState(null); + const [msg, setMsg] = useState(null); + const [loading, setLoading] = useState(true); + const [versionCode, setVersionCode] = useState("167"); + const [versionName, setVersionName] = useState("0.15.4-box"); + const [changelog, setChangelog] = useState(""); + const [uploading, setUploading] = useState(false); + const [progress, setProgress] = useState(null); + const [deleting, setDeleting] = useState(false); + + const load = useCallback(async () => { + setLoading(true); + setErr(null); + try { + const res = await fetch("/api/v1/admin/install/android-tv", { + credentials: "include", + cache: "no-store", + }); + const data = (await res.json()) as ApkMeta & { + error?: { message?: string }; + }; + if (!res.ok) { + throw new Error(data.error?.message || `Meta ophalen mislukt (${res.status})`); + } + setMeta(data); + if (data.versionCode != null) setVersionCode(String(data.versionCode)); + if (data.versionName) setVersionName(String(data.versionName)); + if (data.changelog) setChangelog(String(data.changelog)); + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)); + setMeta(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + void load(); + }, [load]); + + function uploadFile(file: File | null) { + if (!file) return; + if (!file.name.toLowerCase().endsWith(".apk")) { + setErr("Kies een .apk (appliance release)"); + return; + } + const code = Number(versionCode); + if (!Number.isFinite(code) || code < 1) { + setErr("versionCode moet een positief getal zijn"); + return; + } + if (!versionName.trim()) { + setErr("versionName is verplicht"); + return; + } + setErr(null); + setMsg(null); + setUploading(true); + setProgress(0); + + const fd = new FormData(); + fd.append("file", file, file.name); + fd.append("versionCode", String(Math.floor(code))); + fd.append("versionName", versionName.trim()); + fd.append("flavor", "appliance"); + if (changelog.trim()) fd.append("changelog", changelog.trim()); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/v1/admin/install/android-tv"); + xhr.withCredentials = true; + xhr.upload.onprogress = (ev) => { + if (!ev.lengthComputable) return; + setProgress(Math.round((ev.loaded / ev.total) * 100)); + }; + xhr.onload = () => { + setUploading(false); + setProgress(null); + if (fileRef.current) fileRef.current.value = ""; + try { + const data = JSON.parse(xhr.responseText) as ApkMeta & { + error?: { message?: string }; + }; + if (xhr.status < 200 || xhr.status >= 300) { + setErr(data.error?.message || `Upload mislukt (${xhr.status})`); + return; + } + setMeta(data); + if (data.versionCode != null) setVersionCode(String(data.versionCode)); + if (data.versionName) setVersionName(String(data.versionName)); + setMsg( + `Live voor boxes: v${data.versionName} (${data.versionCode}) · ${formatBytes(data.sizeBytes ?? 0)}` + ); + } catch { + setErr(`Upload mislukt (${xhr.status})`); + } + }; + xhr.onerror = () => { + setUploading(false); + setProgress(null); + setErr("Netwerkfout tijdens upload — proxy/timeout?"); + }; + xhr.send(fd); + } + + async function removeApk() { + if (!confirm("OTA-APK van Master verwijderen?")) return; + setDeleting(true); + setErr(null); + setMsg(null); + try { + const res = await fetch("/api/v1/admin/install/android-tv", { + method: "DELETE", + credentials: "include", + }); + const data = (await res.json()) as ApkMeta & { error?: { message?: string } }; + if (!res.ok) throw new Error(data.error?.message || `Verwijderen mislukt (${res.status})`); + setMeta(data); + setMsg("APK verwijderd"); + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)); + } finally { + setDeleting(false); + } + } + + return ( + <> +