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

+3 -2
View File
@@ -1,5 +1,6 @@
import { ChevronDown, GripVertical, Pencil, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
import { useItemDnd, useSectionDnd } from "@/board/dnd";
import { IconImg, ItemAnchor, StatusChip, hostOf } from "@/board/primitives";
import { WidgetContent } from "@/widgets";
@@ -28,7 +29,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
<div className="group/sec flex cursor-pointer select-none items-center gap-3 px-0.5 py-1">
<button
type="button"
aria-label="Drag section"
aria-label={t("renderer.dragSection")}
className="cursor-grab text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
{...dnd.attributes}
{...dnd.listeners}
@@ -54,7 +55,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
</button>
<button
type="button"
aria-label="Delete section"
aria-label={t("renderer.deleteSection")}
onClick={onDelete}
className="text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
>
+3 -2
View File
@@ -1,5 +1,6 @@
import { ChevronDown, GripVertical, Pencil, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
import { useItemDnd, useSectionDnd } from "@/board/dnd";
import { IconImg, ItemAnchor, StatusChip, hostOf } from "@/board/primitives";
import { WidgetContent } from "@/widgets";
@@ -32,7 +33,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
</button>
<button
type="button"
aria-label="Drag section"
aria-label={t("renderer.dragSection")}
className="cursor-grab text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
{...dnd.attributes}
{...dnd.listeners}
@@ -41,7 +42,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
</button>
<button
type="button"
aria-label="Delete section"
aria-label={t("renderer.deleteSection")}
onClick={onDelete}
className="text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
>
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { ChevronDown, GripVertical, Pencil, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
import { useItemDnd, useSectionDnd } from "@/board/dnd";
import { useBoard } from "@/board/store";
import { IconImg, ItemAnchor, hostOf } from "@/board/primitives";
@@ -37,10 +38,10 @@ function BoardView({ children }: BoardViewProps) {
}).format(now);
const cells: [string, string, string][] = [
["Local time", time, date.split(",")[0] ?? ""],
[t("renderer.localTime"), time, date.split(",")[0] ?? ""],
["Services", String(services.length), `${board.sections.length} sections`],
["Up", String(up), "responding"],
["Down", String(down), down === 0 ? "all green" : "offline"],
[t("renderer.down"), String(down), down === 0 ? t("renderer.allGreen") : t("renderer.offline")],
["Widgets", String(widgets.length), "registered"],
];
@@ -77,7 +78,7 @@ function SectionView({ section, index, onToggle, onDelete, children }: SectionVi
<div className="group/sec mb-0.5 flex cursor-pointer select-none items-baseline gap-3.5 border-b border-border-strong pb-2.5">
<button
type="button"
aria-label="Drag section"
aria-label={t("renderer.dragSection")}
className="cursor-grab self-center text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
{...dnd.attributes}
{...dnd.listeners}
@@ -96,7 +97,7 @@ function SectionView({ section, index, onToggle, onDelete, children }: SectionVi
{section.name}
</h2>
<span className="font-mono text-[11px] text-text-faint">
{section.items.length} {section.items.length === 1 ? "entry" : "entries"}
{section.items.length} {t(section.items.length === 1 ? "renderer.entry" : "renderer.entries")}
</span>
<ChevronDown
size={14}
@@ -105,7 +106,7 @@ function SectionView({ section, index, onToggle, onDelete, children }: SectionVi
</button>
<button
type="button"
aria-label="Delete section"
aria-label={t("renderer.deleteSection")}
onClick={onDelete}
className="self-center text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
>
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react";
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { t } from "@/i18n";
import { useItemDnd, useSectionDnd } from "@/board/dnd";
import { useBoard } from "@/board/store";
import { IconImg, ItemAnchor, hostOf } from "@/board/primitives";
@@ -42,7 +43,7 @@ function BoardView({ children }: BoardViewProps) {
const panels: { head: string; v: string; s: string; live?: boolean; wide?: boolean }[] = [
{ head: "clock --local", v: time, s: date, live: true, wide: true },
{ head: "services --up", v: String(up), s: `of ${probed} probed` },
{ head: "services --down", v: String(down), s: down === 0 ? "all green" : "needs attention" },
{ head: "services --down", v: String(down), s: down === 0 ? t("renderer.allGreen") : t("renderer.needsAttention") },
{ head: "widgets", v: String(widgets.length), s: "registered" },
{ head: "endpoints", v: String(endpoints), s: "urls indexed" },
{ head: "uptime --probes", v: probed > 0 ? `${Math.round((up / probed) * 100)}%` : "—", s: `${up}/${probed} responding` },
@@ -56,7 +57,7 @@ function BoardView({ children }: BoardViewProps) {
<span className="font-medium text-text-dim">{services.length} services</span>
{` indexed · `}
<span className="text-up">{up} up</span>
{down > 0 ? <span>{` · `}<span className="text-down">{down} down</span></span> : ` · all green`}
{down > 0 ? <span>{` · `}<span className="text-down">{t("renderer.downCount", { n: down })}</span></span> : ` · ${t("renderer.allGreen")}`}
{` · `}
<span className="font-medium text-text-dim">{widgets.length} widgets</span>
{`\ndrag rows to reorder · click rows with multiple endpoints to choose`}
@@ -70,7 +71,7 @@ function BoardView({ children }: BoardViewProps) {
>
<div className="flex items-center gap-1.5 border-b border-border px-3 py-2 text-[10px] uppercase tracking-[0.1em] text-text-faint">
{p.head}
{p.live ? <span className="ml-auto text-[9px] text-up">● live</span> : null}
{p.live ? <span className="ml-auto text-[9px] text-up">● {t("renderer.live")}</span> : null}
</div>
<div className="px-3 py-3">
<div className={cn("font-medium leading-none tracking-tight tabular-nums", p.wide ? "text-[26px]" : "text-[22px]")}>{p.v}</div>
@@ -108,7 +109,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
</button>
<button
type="button"
aria-label="Drag section"
aria-label={t("renderer.dragSection")}
className="cursor-grab text-text-faint opacity-0 transition-opacity group-hover/sec:opacity-100 active:cursor-grabbing"
{...dnd.attributes}
{...dnd.listeners}
@@ -117,7 +118,7 @@ function SectionView({ section, onToggle, onDelete, children }: SectionViewProps
</button>
<button
type="button"
aria-label="Delete section"
aria-label={t("renderer.deleteSection")}
onClick={onDelete}
className="text-text-faint opacity-0 transition-opacity hover:text-destructive group-hover/sec:opacity-100"
>