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 ( -
-
-

{title}

-
-
- -
-
- ); -} - function MiniStat({ label, value, accent }: { label: string; value: ReactNode; accent?: string }) { 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 ( +
+
+
+
+
{label}
+ {badge &&
{badge}
} +
+ {children} +
+
+
+ ); +} + const WIDGETS: { type: IntegrationType; label: string }[] = [ { type: "proxmox", label: "Proxmox" }, { type: "synology", label: "Synology NAS" }, @@ -425,97 +414,93 @@ export default function Dashboard({ user }: { user: CurrentUser }) {
- - {dnsError &&
{dnsError}
} - {dnsStats && dnsStats.totalProviders === 0 ? ( -
-
- No DNS providers configured yet. Add one under DNS. -
-
- ) : ( - <> -
-
- `${p.zoneCount} on ${p.name}`).join(" · ") || undefined} - /> -
-
- -
-
- -
-
- {dnsStats?.perProvider.some((p) => p.error) && ( -
- {dnsStats.perProvider - .filter((p) => p.error) - .map((p) => `${p.name}: ${p.error}`) - .join("; ")} + +
+ Not configured + ) : ( + + {dnsStats.enabledProviders} enabled + + ) + } + > + {dnsError &&
{dnsError}
} + {!dnsStats ? ( +
Fetching status…
+ ) : dnsStats.totalProviders === 0 ? ( +
+ Configure a DNS provider from the DNS page.
+ ) : ( + <> +
+
+ +
+
+ +
+
+ +
+
+ {dnsStats.perProvider.some((p) => p.error) && ( +
+ {dnsStats.perProvider + .filter((p) => p.error) + .map((p) => `${p.name}: ${p.error}`) + .join("; ")} +
+ )} + {dnsStats.recordsByType.length > 0 && ( + ({ label: r.type, count: r.count }))} colors={DNS_TYPE_COLORS} compact /> + )} + )} - {dnsStats && ( - ({ label: r.type, count: r.count }))} - colors={DNS_TYPE_COLORS} - /> - )} - - )} +
- - {secrets && totalSecrets === 0 ? ( -
-
- No secrets tracked yet. Add one under Secrets. -
-
- ) : ( - <> -
-
- + Not configured + ) : expiredSecrets > 0 ? ( + {expiredSecrets} expired + ) : expiringSecrets > 0 ? ( + {expiringSecrets} expiring + ) : ( + All OK + ) + } + > + {!secrets ? ( +
Fetching status…
+ ) : totalSecrets === 0 ? ( +
+ Configure a secret from the Secrets page.
-
- 0 ? "#f59e0b" : undefined} - /> -
-
- 0 ? "#ef4444" : undefined} - /> -
-
- - - )} + ) : ( + <> +
+
+ +
+
+ 0 ? "#f59e0b" : undefined} /> +
+
+ 0 ? "#ef4444" : undefined} /> +
+
+ + + )} + +
-
-
-
-
{label}
-
- {isLive ? ( - Connected - ) : integration ? ( - Connecting… - ) : ( - Not connected - )} + Connected + ) : integration ? ( + Connecting… + ) : ( + Not connected + ) + } + > + {isLiveTailscale ? ( + <> +
+
+ +
+
+ 0 ? "#f59e0b" : undefined} + /> +
+
+ 0 ? "#f59e0b" : undefined} + />
- {isLiveTailscale ? ( - <> -
-
- -
-
- 0 ? "#f59e0b" : undefined} - /> -
-
- 0 ? "#f59e0b" : undefined} - /> -
-
- {tailscaleExtra && tailscaleExtra.osBreakdown.length > 0 && ( - d.label))} - compact - /> - )} - - ) : isLiveGitea ? ( - <> -
-
- -
-
- -
-
- 0 ? "#ef4444" : undefined} - /> -
-
- {giteaSummary!.statusBreakdown.length > 0 && ( - - )} - - ) : isLiveDockhand ? ( - <> -
-
- -
-
- -
-
- 0 ? "#f59e0b" : undefined} - /> -
-
- {dockhandSummary!.stateBreakdown.length > 0 && ( - - )} - - ) : isLiveSemaphore ? ( - <> -
-
- -
-
- 0 ? "#ef4444" : undefined} - /> -
-
- {semaphoreSummary!.statusBreakdown.length > 0 && ( - - )} - - ) : isLiveProxmox ? ( - <> -
-
- -
-
- -
-
- -
-
- {proxmoxSummary!.nodeBreakdown.length > 1 && ( - d.label))} - compact - /> - )} - - ) : isLiveSynology ? ( - <> -
-
- -
-
- 0 ? "#ef4444" : undefined} - /> -
-
- -
-
- {synologySummary!.memTotalBytes !== null && ( -
- RAM: {formatBytes(synologySummary!.memUsedBytes)} / {formatBytes(synologySummary!.memTotalBytes)} -
- )} - {synologySummary!.diskStatusBreakdown.length > 0 && ( - d.label))} - compact - /> - )} - - ) : ( -
- {integration ? ( - "Fetching status…" - ) : ( - <> - Configure this integration from the{" "} - Integrations page. - - )} + {tailscaleExtra && tailscaleExtra.osBreakdown.length > 0 && ( + d.label))} + compact + /> + )} + + ) : isLiveGitea ? ( + <> +
+
+ +
+
+ +
+
+ 0 ? "#ef4444" : undefined} + /> +
+
+ {giteaSummary!.statusBreakdown.length > 0 && ( + + )} + + ) : isLiveDockhand ? ( + <> +
+
+ +
+
+ +
+
+ 0 ? "#f59e0b" : undefined} + /> +
+
+ {dockhandSummary!.stateBreakdown.length > 0 && ( + + )} + + ) : isLiveSemaphore ? ( + <> +
+
+ +
+
+ 0 ? "#ef4444" : undefined} + /> +
+
+ {semaphoreSummary!.statusBreakdown.length > 0 && ( + + )} + + ) : isLiveProxmox ? ( + <> +
+
+ +
+
+ +
+
+ +
+
+ {proxmoxSummary!.nodeBreakdown.length > 1 && ( + d.label))} + compact + /> + )} + + ) : isLiveSynology ? ( + <> +
+
+ +
+
+ 0 ? "#ef4444" : undefined} + /> +
+
+ +
+
+ {synologySummary!.memTotalBytes !== null && ( +
+ RAM: {formatBytes(synologySummary!.memUsedBytes)} / {formatBytes(synologySummary!.memTotalBytes)}
)} + {synologySummary!.diskStatusBreakdown.length > 0 && ( + d.label))} + compact + /> + )} + + ) : ( +
+ {integration ? ( + "Fetching status…" + ) : ( + <> + Configure this integration from the Integrations page. + + )}
-
-
+ )} + ); })}