Publish curated LumaOps source from c85ee9ef8f1454f5c305398902afbb9e6738de40
Managed validation / full (push) Failing after 8s

This commit is contained in:
Public Source Publisher committed 2026-09-30 00:26:45 +00:00
1 parent 91f8b3e3c1
commit def7920856
2271 files changed
+501225 -501178

No files matched your search

+97 -97
View File
@@ -1,97 +1,97 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Activity, Cable, CirclePower, Cpu, Lightbulb, ShieldAlert, Sparkles, Wifi } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { api, mutationId } from "../api/client";
import type { Device, Page, Scene, SystemStatus } from "../api/types";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { useToast } from "../components/Toast";
import { Badge, Button, Card, CardHeader, ErrorPanel, LoadingGrid, PageHeader, Stat, StatusBadge } from "../components/ui";
import { useI18n } from "../lib/i18n";
import { formatDate, hexToColor } from "../lib/utils";
export function OverviewPage() {
const { text } = useI18n();
const queryClient = useQueryClient();
const { notify } = useToast();
const [color, setColor] = useState("#5660ff");
const [confirmEmergency, setConfirmEmergency] = useState(false);
const system = useQuery({ queryKey: ["system"], queryFn: () => api<SystemStatus>("/api/v1/system") });
const devices = useQuery({ queryKey: ["devices", "online"], queryFn: () => api<Page<Device>>("/api/v1/devices?online=true&limit=500") });
const scenes = useQuery({ queryKey: ["scenes"], queryFn: () => api<Page<Scene>>("/api/v1/scenes?limit=6") });
const quickColor = useMutation({
mutationFn: async () => {
const targets = devices.data?.items.filter((device) => device.capabilities.rgb && !device.read_only && !device.blocked) ?? [];
return Promise.allSettled(targets.map((device) => api(`/api/v1/devices/${device.id}/state`, {
method: "POST",
headers: { "Idempotency-Key": mutationId() },
body: JSON.stringify({ state: { power: true, colors: [hexToColor(color)] } }),
})));
},
onSuccess: (results) => {
const failed = results.filter((result) => result.status === "rejected").length;
notify(failed ? `${results.length - failed} apparaten bijgewerkt; ${failed} mislukt.` : "Kleur toegepast op alle geschikte apparaten.", failed ? "danger" : "success");
void queryClient.invalidateQueries({ queryKey: ["system"] });
},
});
const allOff = useMutation({
mutationFn: (emergency: boolean) => api(`/api/v1/commands/${emergency ? "emergency-stop" : "all-off"}`, { method: "POST" }),
onSuccess: (_, emergency) => { notify(emergency ? "Noodstop geactiveerd." : "Alles-uitopdracht voltooid."); void queryClient.invalidateQueries(); },
});
const applyScene = useMutation({
mutationFn: (id: string) => api(`/api/v1/scenes/${id}/apply`, { method: "POST", body: JSON.stringify({ rollback_on_failure: true }) }),
onSuccess: () => { notify("Scène toegepast."); void queryClient.invalidateQueries(); },
onError: (error) => notify(error instanceof Error ? error.message : "Scène mislukt.", "danger"),
});
if (system.isLoading) return <><PageHeader title={text("Overzicht", "Overview")} /><LoadingGrid /></>;
if (system.error || !system.data) return <ErrorPanel error={system.error} retry={() => void system.refetch()} />;
const status = system.data;
const openrgb = status.health.components["openrgb-local"] ?? status.health.components["openrgb-mock"];
return (
<>
<PageHeader eyebrow={text("Lokaal controlecentrum", "Local control center")} title={text("Goedenavond, Jens", "Good evening, Jens")} description={text("Je verlichting en OpenRGB-hardware in één rustig overzicht.", "Your lighting and OpenRGB hardware in one calm overview.")} actions={<><Button variant="secondary" onClick={() => allOff.mutate(false)} busy={allOff.isPending}><CirclePower size={16} /> {text("Alles uit", "All off")}</Button><Button variant="danger" onClick={() => setConfirmEmergency(true)}><ShieldAlert size={16} /> {text("Noodstop", "Emergency stop")}</Button></>} />
{status.emergency_stop ? <div className="alert alert--danger"><ShieldAlert size={20} /><div><strong>Globale noodstop actief</strong><p>Nieuwe hardwareopdrachten worden geblokkeerd totdat je de noodstop in Diagnostiek vrijgeeft.</p></div></div> : null}
<section className="grid grid--stats" aria-label="Systeemstatistieken">
<Stat label="Systeemstatus" value={<StatusBadge status={status.health.status} />} detail={`Gecontroleerd ${formatDate(status.health.checked_at)}`} icon={Activity} />
<Stat label="Apparaten online" value={`${status.devices.online} / ${status.devices.total}`} detail={`${status.devices.offline} offline`} icon={Cpu} />
<Stat label="OpenRGB Core" value={openrgb ? <StatusBadge status={openrgb.status} /> : "—"} detail={`1.0rc3 · SDK ${status.sdk_protocol}`} icon={Lightbulb} />
<Stat label="Connectoren" value={Object.keys(status.health.components).filter((key) => key.startsWith("openrgb")).length} detail="Actieve beheerkanalen" icon={Cable} />
</section>
<section className="dashboard-grid">
<Card className="quick-control">
<CardHeader title="Snelle bediening" description="Veilige statische kleur op alle geschikte apparaten." />
<div className="color-control">
<input aria-label="Snelle kleur" type="color" value={color} onChange={(event) => setColor(event.target.value)} />
<div><strong>{color.toUpperCase()}</strong><span>{devices.data?.items.length ?? 0} online doelen</span></div>
<Button onClick={() => quickColor.mutate()} busy={quickColor.isPending}><Sparkles size={16} /> Kleur toepassen</Button>
</div>
<div className="color-swatches" aria-label="Kleursuggesties">
{["#5660ff", "#00c2a8", "#ff7a59", "#f5c451", "#f7f7ff"].map((item) => <button key={item} aria-label={`Kies ${item}`} className={color === item ? "selected" : ""} style={{ background: item }} onClick={() => setColor(item)} />)}
</div>
</Card>
<Card>
<CardHeader title="Actieve scène" description="De laatst succesvol toegepaste LumaOps-scène." />
{status.active_scene ? <div className="active-scene"><span className="scene-orb" /><div><strong>{status.active_scene.name}</strong><span>{formatDate(status.active_scene.last_applied_at)}</span></div><Badge tone="accent">Actief</Badge></div> : <p className="muted">Nog geen scène toegepast.</p>}
</Card>
</section>
<section className="section-block">
<div className="section-heading"><div><h2>Laatst gebruikte scènes</h2><p>Start een sfeer zonder de pagina te verlaten.</p></div><Link className="text-link" to="/scenes">Alle scènes</Link></div>
<div className="grid grid--cards">
{scenes.data?.items.map((scene, index) => <Card key={scene.id} className="scene-card"><div className={`scene-gradient scene-gradient--${index % 4}`}><Sparkles size={20} /></div><div className="scene-card__body"><div><h3>{scene.name}</h3><p>{scene.item_count ?? 0} onderdelen · v{scene.version}</p></div><Button variant="secondary" onClick={() => applyScene.mutate(scene.id)} busy={applyScene.isPending}>Start</Button></div></Card>)}
{!scenes.isLoading && !scenes.data?.items.length ? <Card><p className="muted">Leg je huidige toestand vast als eerste scène.</p></Card> : null}
</div>
</section>
<section className="dashboard-grid">
<Card><CardHeader title="Recente opdrachten" description="Laatste wijzigingen via LumaOps." action={<Link to="/activity" className="text-link">Alles bekijken</Link>} />
<div className="timeline">{status.recent_commands.slice(0, 5).map((command) => <div className="timeline__item" key={command.id}><span className={`timeline__dot timeline__dot--${command.status}`} /><div><strong>{command.action === "set_state" ? "Apparaattoestand gewijzigd" : command.action}</strong><span>{formatDate(command.created_at)}</span></div><Badge tone={command.status === "succeeded" ? "success" : command.status === "failed" ? "danger" : "neutral"}>{command.status}</Badge></div>)}{!status.recent_commands.length ? <p className="muted">Nog geen opdrachten uitgevoerd.</p> : null}</div>
</Card>
<Card><CardHeader title="Waarschuwingen" description="Aandachtspunten met concrete herstelacties." action={<Wifi size={18} />} />
{status.warnings.length ? status.warnings.map((warning) => <div className="warning-row" key={warning.id}><span /><div><strong>{warning.title}</strong><p>{warning.message}</p></div></div>) : <div className="all-clear"><span><Lightbulb size={20} /></span><div><strong>Alles rustig</strong><p>Er zijn geen open waarschuwingen.</p></div></div>}
</Card>
</section>
<ConfirmDialog open={confirmEmergency} title="Globale noodstop activeren?" description="LumaOps probeert alle geschikte apparaten één keer uit te schakelen en blokkeert daarna nieuwe hardwareopdrachten." confirmLabel="Noodstop activeren" danger onClose={() => setConfirmEmergency(false)} onConfirm={() => { setConfirmEmergency(false); allOff.mutate(true); }} />
</>
);
}
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Activity, Cable, CirclePower, Cpu, Lightbulb, ShieldAlert, Sparkles, Wifi } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { api, mutationId } from "../api/client";
import type { Device, Page, Scene, SystemStatus } from "../api/types";
import { ConfirmDialog } from "../components/ConfirmDialog";
import { useToast } from "../components/Toast";
import { Badge, Button, Card, CardHeader, ErrorPanel, LoadingGrid, PageHeader, Stat, StatusBadge } from "../components/ui";
import { useI18n } from "../lib/i18n";
import { formatDate, hexToColor } from "../lib/utils";
export function OverviewPage() {
const { text } = useI18n();
const queryClient = useQueryClient();
const { notify } = useToast();
const [color, setColor] = useState("#5660ff");
const [confirmEmergency, setConfirmEmergency] = useState(false);
const system = useQuery({ queryKey: ["system"], queryFn: () => api<SystemStatus>("/api/v1/system") });
const devices = useQuery({ queryKey: ["devices", "online"], queryFn: () => api<Page<Device>>("/api/v1/devices?online=true&limit=500") });
const scenes = useQuery({ queryKey: ["scenes"], queryFn: () => api<Page<Scene>>("/api/v1/scenes?limit=6") });
const quickColor = useMutation({
mutationFn: async () => {
const targets = devices.data?.items.filter((device) => device.capabilities.rgb && !device.read_only && !device.blocked) ?? [];
return Promise.allSettled(targets.map((device) => api(`/api/v1/devices/${device.id}/state`, {
method: "POST",
headers: { "Idempotency-Key": mutationId() },
body: JSON.stringify({ state: { power: true, colors: [hexToColor(color)] } }),
})));
},
onSuccess: (results) => {
const failed = results.filter((result) => result.status === "rejected").length;
notify(failed ? `${results.length - failed} apparaten bijgewerkt; ${failed} mislukt.` : "Kleur toegepast op alle geschikte apparaten.", failed ? "danger" : "success");
void queryClient.invalidateQueries({ queryKey: ["system"] });
},
});
const allOff = useMutation({
mutationFn: (emergency: boolean) => api(`/api/v1/commands/${emergency ? "emergency-stop" : "all-off"}`, { method: "POST" }),
onSuccess: (_, emergency) => { notify(emergency ? "Noodstop geactiveerd." : "Alles-uitopdracht voltooid."); void queryClient.invalidateQueries(); },
});
const applyScene = useMutation({
mutationFn: (id: string) => api(`/api/v1/scenes/${id}/apply`, { method: "POST", body: JSON.stringify({ rollback_on_failure: true }) }),
onSuccess: () => { notify("Scène toegepast."); void queryClient.invalidateQueries(); },
onError: (error) => notify(error instanceof Error ? error.message : "Scène mislukt.", "danger"),
});
if (system.isLoading) return <><PageHeader title={text("Overzicht", "Overview")} /><LoadingGrid /></>;
if (system.error || !system.data) return <ErrorPanel error={system.error} retry={() => void system.refetch()} />;
const status = system.data;
const openrgb = status.health.components["openrgb-local"] ?? status.health.components["openrgb-mock"];
return (
<>
<PageHeader eyebrow={text("Lokaal controlecentrum", "Local control center")} title={text("Goedenavond, Jens", "Good evening, Jens")} description={text("Je verlichting en OpenRGB-hardware in één rustig overzicht.", "Your lighting and OpenRGB hardware in one calm overview.")} actions={<><Button variant="secondary" onClick={() => allOff.mutate(false)} busy={allOff.isPending}><CirclePower size={16} /> {text("Alles uit", "All off")}</Button><Button variant="danger" onClick={() => setConfirmEmergency(true)}><ShieldAlert size={16} /> {text("Noodstop", "Emergency stop")}</Button></>} />
{status.emergency_stop ? <div className="alert alert--danger"><ShieldAlert size={20} /><div><strong>Globale noodstop actief</strong><p>Nieuwe hardwareopdrachten worden geblokkeerd totdat je de noodstop in Diagnostiek vrijgeeft.</p></div></div> : null}
<section className="grid grid--stats" aria-label="Systeemstatistieken">
<Stat label="Systeemstatus" value={<StatusBadge status={status.health.status} />} detail={`Gecontroleerd ${formatDate(status.health.checked_at)}`} icon={Activity} />
<Stat label="Apparaten online" value={`${status.devices.online} / ${status.devices.total}`} detail={`${status.devices.offline} offline`} icon={Cpu} />
<Stat label="OpenRGB Core" value={openrgb ? <StatusBadge status={openrgb.status} /> : "—"} detail={`1.0rc3 · SDK ${status.sdk_protocol}`} icon={Lightbulb} />
<Stat label="Connectoren" value={Object.keys(status.health.components).filter((key) => key.startsWith("openrgb")).length} detail="Actieve beheerkanalen" icon={Cable} />
</section>
<section className="dashboard-grid">
<Card className="quick-control">
<CardHeader title="Snelle bediening" description="Veilige statische kleur op alle geschikte apparaten." />
<div className="color-control">
<input aria-label="Snelle kleur" type="color" value={color} onChange={(event) => setColor(event.target.value)} />
<div><strong>{color.toUpperCase()}</strong><span>{devices.data?.items.length ?? 0} online doelen</span></div>
<Button onClick={() => quickColor.mutate()} busy={quickColor.isPending}><Sparkles size={16} /> Kleur toepassen</Button>
</div>
<div className="color-swatches" aria-label="Kleursuggesties">
{["#5660ff", "#00c2a8", "#ff7a59", "#f5c451", "#f7f7ff"].map((item) => <button key={item} aria-label={`Kies ${item}`} className={color === item ? "selected" : ""} style={{ background: item }} onClick={() => setColor(item)} />)}
</div>
</Card>
<Card>
<CardHeader title="Actieve scène" description="De laatst succesvol toegepaste LumaOps-scène." />
{status.active_scene ? <div className="active-scene"><span className="scene-orb" /><div><strong>{status.active_scene.name}</strong><span>{formatDate(status.active_scene.last_applied_at)}</span></div><Badge tone="accent">Actief</Badge></div> : <p className="muted">Nog geen scène toegepast.</p>}
</Card>
</section>
<section className="section-block">
<div className="section-heading"><div><h2>Laatst gebruikte scènes</h2><p>Start een sfeer zonder de pagina te verlaten.</p></div><Link className="text-link" to="/scenes">Alle scènes</Link></div>
<div className="grid grid--cards">
{scenes.data?.items.map((scene, index) => <Card key={scene.id} className="scene-card"><div className={`scene-gradient scene-gradient--${index % 4}`}><Sparkles size={20} /></div><div className="scene-card__body"><div><h3>{scene.name}</h3><p>{scene.item_count ?? 0} onderdelen · v{scene.version}</p></div><Button variant="secondary" onClick={() => applyScene.mutate(scene.id)} busy={applyScene.isPending}>Start</Button></div></Card>)}
{!scenes.isLoading && !scenes.data?.items.length ? <Card><p className="muted">Leg je huidige toestand vast als eerste scène.</p></Card> : null}
</div>
</section>
<section className="dashboard-grid">
<Card><CardHeader title="Recente opdrachten" description="Laatste wijzigingen via LumaOps." action={<Link to="/activity" className="text-link">Alles bekijken</Link>} />
<div className="timeline">{status.recent_commands.slice(0, 5).map((command) => <div className="timeline__item" key={command.id}><span className={`timeline__dot timeline__dot--${command.status}`} /><div><strong>{command.action === "set_state" ? "Apparaattoestand gewijzigd" : command.action}</strong><span>{formatDate(command.created_at)}</span></div><Badge tone={command.status === "succeeded" ? "success" : command.status === "failed" ? "danger" : "neutral"}>{command.status}</Badge></div>)}{!status.recent_commands.length ? <p className="muted">Nog geen opdrachten uitgevoerd.</p> : null}</div>
</Card>
<Card><CardHeader title="Waarschuwingen" description="Aandachtspunten met concrete herstelacties." action={<Wifi size={18} />} />
{status.warnings.length ? status.warnings.map((warning) => <div className="warning-row" key={warning.id}><span /><div><strong>{warning.title}</strong><p>{warning.message}</p></div></div>) : <div className="all-clear"><span><Lightbulb size={20} /></span><div><strong>Alles rustig</strong><p>Er zijn geen open waarschuwingen.</p></div></div>}
</Card>
</section>
<ConfirmDialog open={confirmEmergency} title="Globale noodstop activeren?" description="LumaOps probeert alle geschikte apparaten één keer uit te schakelen en blokkeert daarna nieuwe hardwareopdrachten." confirmLabel="Noodstop activeren" danger onClose={() => setConfirmEmergency(false)} onConfirm={() => { setConfirmEmergency(false); allOff.mutate(true); }} />
</>
);
}