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>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-22 12:49:51 +02:00
1 parent f3143af351
commit e0ef60eca6
49 files changed
+2131 -426

No files matched your search

+5 -4
View File
@@ -1,5 +1,6 @@
import type { Item } from "@/types";
import type { WidgetData } from "@/widgets/index";
import { t } from "@/i18n";
// Shared DNS-blocker layout: big block-rate, queries + blocked counts.
// Pi-hole and AdGuard return the same fields from their fetchers.
@@ -23,12 +24,12 @@ function BlockerWidget({ item, data, error }: { item: Item; data: WidgetData | u
<span className="font-mono text-[22px] font-medium leading-none tracking-tight tabular-nums">
{data ? `${pct.toFixed(1)}%` : "…"}
</span>
<span className="text-[10.5px] uppercase tracking-[0.1em] text-text-faint">blocked</span>
<span className="text-[10.5px] uppercase tracking-[0.1em] text-text-faint">{t("widget.blockedLbl")}</span>
</div>
<div className="ml-auto flex flex-col items-end gap-1 font-mono text-[10.5px] text-text-faint">
<span>{fmt(num(data, "queriesToday"))} queries</span>
<span>{fmt(num(data, "blockedToday"))} blocked</span>
{typeof data?.avgProcessMs === "number" && <span>{num(data, "avgProcessMs").toFixed(0)} ms avg</span>}
<span>{t("widget.queries", { n: fmt(num(data, "queriesToday")) })}</span>
<span>{t("widget.blocked", { n: fmt(num(data, "blockedToday")) })}</span>
{typeof data?.avgProcessMs === "number" && <span>{t("widget.msAvg", { n: num(data, "avgProcessMs").toFixed(0) })}</span>}
</div>
</div>
);