diff --git a/README.md b/README.md
index 471fdc7..6506680 100644
--- a/README.md
+++ b/README.md
@@ -17,14 +17,15 @@ All modules from the original plan are built:
- Monorepo scaffold, Tabler-themed app shell/navigation
- Authentik OIDC login, roles (first user to sign in becomes admin), audit log
-- **Dashboard** — an overview of every system this app tracks: DNS (domain/
- record counts per provider, cached records broken down by type), Secrets
- (monitored/expiring/expired counts, broken down by type), and a widget per
- integration with a small stat row plus its own breakdown — Tailscale by
- OS, Proxmox by node (VM/LXC counts too), Dockhand by container state (plus
- host count), Semaphore and Gitea by last-run status (plus private-repo
- count), and Synology's CPU/RAM alongside its disk-health breakdown.
- Breakdowns render as a stacked proportion bar with a legend — no charting
+- **Dashboard** — an overview of every system this app tracks, all sharing
+ one widget-card design (label + status badge, a small stat row, then its
+ own breakdown): DNS (domain/record counts per provider, cached records by
+ type), Secrets (monitored/expiring/expired, by type), and one widget per
+ integration — Tailscale by OS, Proxmox by node (VM/LXC counts too),
+ Dockhand by container state (plus host count), Semaphore and Gitea by
+ last-run status (plus private-repo count), and Synology's CPU/RAM
+ alongside its disk-health breakdown. Breakdowns render as a stacked
+ proportion bar with a legend — no charting
library, matching the rest of the app's plain-Tabler-CSS approach.
- **Diagnostic Log** (admin-only) — every call this app makes to a DNS
provider or integration (Tailscale, Proxmox, Synology, Semaphore, Gitea,
diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx
index 5c99de6..5ae9052 100644
--- a/web/src/pages/Dashboard.tsx
+++ b/web/src/pages/Dashboard.tsx
@@ -95,20 +95,6 @@ function SectionHeader({ title, hint }: { title: string; hint?: ReactNode }) {
);
}
-function StatTile({ label, value, sub, accent }: { label: string; value: ReactNode; sub?: ReactNode; accent?: string }) {
- return (
-
-
-
{label}
-
- {value}
-
- {sub &&
{sub}
}
-
-
- );
-}
-
function breakdownFrom(items: T[], keyOf: (item: T) => string): { label: string; count: number }[] {
const counts = new Map();
for (const item of items) {
@@ -156,20 +142,6 @@ function BreakdownBar({ data, colors, compact }: { data: { label: string; count:
);
}
-function TypeBreakdown({ title, data, colors }: { title: string; data: { label: string; count: number }[]; colors: Record }) {
- if (data.reduce((s, d) => s + d.count, 0) === 0) return null;
- return (
-
@@ -181,6 +153,23 @@ function MiniStat({ label, value, accent }: { label: string; value: ReactNode; a
);
}
+/** The single card shape shared by every dashboard widget — DNS, Secrets, and each integration — so they all read as one family: a label + status badge, then whatever stats/breakdown that widget has. */
+function WidgetCard({ label, badge, children }: { label: string; badge?: ReactNode; children: ReactNode }) {
+ return (
+