Files
Dash/apps/frontend/src/pages/StatusPublicPage.tsx
T
Tomas DvorakandDevin e0ef60eca6 feat(ui): phase 5 — graphs, widgets, theming, PWA, boards, i18n
P5a: 24h uptime bar strip + latency chart on monitor detail, iframe
embed widget, generic JSON-path widget, accent/wallpaper/custom CSS
settings, PWA manifest + service worker, demo GIF.

P5b: transport picker + per-transport fields + test button in
settings; per-monitor alert-rule fields in dialogs.

P5c: container cpu/mem columns, SMART/ZFS/GPU sections on system
detail, subdomains section on domain detail.

P5d: login/setup page + auth middleware client handling, board
switcher + /b/:slug routes, full string extraction to typed t() keys
with {param} interpolation, complete cs locale, language select.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 12:49:51 +02:00

130 lines
5.6 KiB
TypeScript

import { useCallback, useEffect, useState } from "react";
import { api } from "@/api";
import type { PublicStatus } from "@/types";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
// Public status page — clean read-only view; refreshes every 30s.
// Route: /status/:slug -> GET /api/status-pages/:slug/public.
// status strings arrive as plain `string` — map with functions, not
// open dictionaries, so the lookup type stays honest.
function overallBanner(overall?: string) {
if (overall === "down") return { label: t("statusPublic.outage"), cls: "bg-down" };
if (overall === "maintenance") return { label: t("statusPublic.underMaintenance"), cls: "bg-text-faint/60" };
if (overall === "up") return { label: t("statusPublic.operational"), cls: "bg-up" };
return { label: t("statusPublic.checking"), cls: "bg-text-faint/40" };
}
function dotCls(status?: string) {
if (status === "up") return "bg-up";
if (status === "down") return "bg-down";
if (status === "maintenance") return "bg-text-faint/60";
return "bg-text-faint/40";
}
export function StatusPublicPage({ slug }: { slug: string }) {
const [st, setSt] = useState<PublicStatus | null>(null);
const [missing, setMissing] = useState(false);
const load = useCallback(async () => {
const { data, response } = await api.GET("/api/status-pages/{slug}/public", { params: { path: { slug } } });
if (response.status === 404) setMissing(true);
if (data) setSt(data);
}, [slug]);
useEffect(() => {
void load();
const t = setInterval(() => void load(), 30_000);
return () => clearInterval(t);
}, [load]);
if (missing) return <main className="mx-auto max-w-2xl px-7 py-16 text-center text-[13px] text-text-faint">{t("statusPublic.notFound")}</main>;
if (!st) return <main className="mx-auto max-w-2xl px-7 py-16 text-center text-[13px] text-text-faint">{t("common.loading")}</main>;
const banner = overallBanner(st.overall);
return (
<main className="mx-auto w-full max-w-2xl px-7 py-10">
<div className="mb-6 text-center">
<h1 className="text-[17px] font-semibold tracking-tight">{st.title}</h1>
{st.description ? <p className="mt-1 text-[12px] text-text-faint">{st.description}</p> : null}
</div>
<div className={cn("mb-8 rounded-[10px] px-5 py-3.5 text-center text-[13px] font-medium text-white", banner.cls)}>
{banner.label}
</div>
{(st.monitors ?? []).length > 0 && (
<section className="mb-6">
<h2 className="mb-2 font-mono text-[10.5px] uppercase tracking-[0.12em] text-text-faint">{t("statusPublic.services")}</h2>
<div className="overflow-hidden rounded-[10px] border border-border">
{(st.monitors ?? []).map((m) => (
<div key={m.id} className="flex items-center gap-3 border-b border-border/60 px-4 py-2.5 last:border-0">
<span className={cn("size-1.5 shrink-0 rounded-full", dotCls(m.status))} />
<span className="min-w-0 flex-1 truncate text-[13px]">{m.name}</span>
<span className="font-mono text-[10.5px] text-text-faint">{m.uptime24h?.toFixed(1)}%</span>
<span className="w-20 text-right font-mono text-[10.5px] uppercase text-text-faint">{m.status}</span>
</div>
))}
</div>
</section>
)}
{(st.systems ?? []).length > 0 && (
<section className="mb-6">
<h2 className="mb-2 font-mono text-[10.5px] uppercase tracking-[0.12em] text-text-faint">{t("statusPublic.systems")}</h2>
<div className="overflow-hidden rounded-[10px] border border-border">
{(st.systems ?? []).map((s) => (
<div key={s.id} className="flex items-center gap-3 border-b border-border/60 px-4 py-2.5 last:border-0">
<span className={cn("size-1.5 shrink-0 rounded-full", dotCls(s.status))} />
<span className="min-w-0 flex-1 truncate text-[13px]">{s.name}</span>
<span className="font-mono text-[10.5px] uppercase text-text-faint">{s.status}</span>
</div>
))}
</div>
</section>
)}
{(st.maintenance ?? []).length > 0 && (
<section className="mb-6">
<h2 className="mb-2 font-mono text-[10.5px] uppercase tracking-[0.12em] text-text-faint">{t("statusPublic.maintenance")}</h2>
<div className="overflow-hidden rounded-[10px] border border-border">
{(st.maintenance ?? []).map((w) => (
<div key={w.id} className="flex items-center gap-3 border-b border-border/60 px-4 py-2.5 last:border-0">
<span className="min-w-0 flex-1 truncate text-[13px]">{w.title}</span>
<span className="font-mono text-[10.5px] text-text-faint">
{new Date(w.startsAt ?? "").toLocaleString()} → {new Date(w.endsAt ?? "").toLocaleString()}
</span>
</div>
))}
</div>
</section>
)}
{(st.incidents ?? []).length > 0 && (
<section className="mb-6">
<h2 className="mb-2 font-mono text-[10.5px] uppercase tracking-[0.12em] text-text-faint">{t("statusPublic.incidents")}</h2>
<div className="flex flex-col gap-2">
{(st.incidents ?? []).map((inc) => (
<div key={inc.id} className="rounded-[10px] border border-border px-4 py-3">
<div className="flex items-center gap-3">
<span className="min-w-0 flex-1 truncate text-[13px] font-medium">{inc.title}</span>
<span className="font-mono text-[10.5px] uppercase text-text-faint">{inc.status}</span>
</div>
{(inc.updates ?? []).slice(-3).map((u) => (
<p key={u.id} className="mt-1.5 text-[11.5px] text-text-faint">
<span className="font-mono text-[10px] uppercase">{u.status}</span> — {u.message}
</p>
))}
</div>
))}
</div>
</section>
)}
<p className="mt-10 text-center font-mono text-[10px] text-text-faint">{t("statusPublic.footer")}</p>
</main>
);
}