Publish curated LumaOps source from c85ee9ef8f1454f5c305398902afbb9e6738de40
Managed validation / full (push) Failing after 8s
Managed validation / full (push) Failing after 8s
This commit is contained in:
1 parent
91f8b3e3c1
commit
def7920856
2271 files changed
+501225
-501178
No files matched your search
@@ -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); }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user