Widgets
Each widget follows the same data chain:
React component → React Query hook → Next.js API route (/api/widgets/<name>) → external service (Docker socket, HTTP API, etc.)Widgets never call external services directly from the browser.
Adaptive size system
Every widget renders semantically at each size rather than just scaling.
The grid is 8-column (xl) / 4-column (default), with rows of 160 px.
Size keys
| Key | Grid | Description |
|---|---|---|
tiny | 1×1, 1×2 | Icon + single number only. No labels, no controls |
compact | 2×2 | Hero KPI + status indicator |
strip | 4×2 | KPI left panel | detail / list right panel |
detail | 2×4 | KPI top | scrollable detail list below |
expanded | 4×4, 8×4 | Full panel — all data, all metrics |
banner | 8×2 | Full-width strip — headline + supporting data |
The mapping lives in src/lib/widgets/types.ts:
export type WidgetSizeKey = "tiny" | "compact" | "strip" | "detail" | "expanded" | "banner"
export function deriveSize(cols: number, rows: number): WidgetSizeKeyUsing size in a widget
import { useCardSize, SizedContent } from "@/components/dashboard/widget-wrapper"
export function MyWidget() { const { sizeKey, isTiny } = useCardSize()
return ( <Card ...> <CardContent ...> <SizedContent sizeKey={sizeKey}> {sizeKey === "tiny" && <TinyView />} {sizeKey === "compact" && <CompactView />} {sizeKey === "strip" && <StripView />} {sizeKey === "detail" && <DetailView />} {sizeKey === "expanded" && <ExpandedView />} {sizeKey === "banner" && <BannerView />} </SizedContent> </CardContent> </Card> )}SizedContent fades in new content on resize via key-based remount.
useCardSize() also returns cols, rows, isTiny, isCompact, isWide, isTall for
cases where raw dimensions are more useful than the semantic key.
Edit mode
The pencil icon (top-right of the dashboard) enters edit mode. Each widget shows:
- Drag handle — reorder within its section
- Size picker — buttons for every available size (1×1, 1×2, 2×2, 4×2, 2×4, 4×4, 8×2)
Sizes are persisted per-widget in localStorage under widget-size:<id>.
Some widgets pass a custom sizeOptions prop to restrict which sizes are available
(e.g. uptime-kuma and prometheus-hosts only offer wide/tall variants).
Available widgets
| Widget | Component | API route | Data source |
|---|---|---|---|
| Docker | DockerWidget | /api/widgets/docker | Docker socket |
| Tailscale | TailscaleWidget | /api/widgets/tailscale | Tailscale local API |
| Cloudflare Tunnel | CloudflaredWidget | /api/widgets/cloudflared | cloudflared metrics |
| Host Resources | VpsServersWidget | /api/widgets/node-exporter | node_exporter |
| Infrastructure | PrometheusHostsWidget | /api/widgets/prometheus-hosts | Prometheus / node_exporter |
| Security | SecurityWidget | /api/widgets/security | Fail2ban via node_exporter textfile |
| Portainer | PortainerWidget | /api/widgets/portainer | Portainer REST |
| Uptime Kuma | UptimeKumaWidget | /api/widgets/uptime-kuma | Uptime Kuma status page |
| System Summary | SystemSummaryCards | /api/widgets/system-summary | Aggregated system metrics |
Default sizes
| Widget | Default | Rationale |
|---|---|---|
| Tailscale | 4×2 (strip) | Device list needs horizontal space |
| Cloudflared | 2×2 (compact) | Single status word is the key signal |
| Host Resources | 2×2 (compact) | Memory + disk bars fit stacked |
| Portainer | 4×2 (strip) | Three stats work side-by-side |
| Docker | 2×2 (compact) | Running count is the key signal |
| Uptime Kuma | 8×2 (banner) | Status bar wants full width |
| Infrastructure | 8×4 (expanded) | Multi-host metric rows need width + height |
| Security | 4×2 (strip) | Ban count + jail table need width |
Adding a widget
- Create
src/components/widgets/<name>.tsx— implement eachsizeKeyvariant - Create
src/app/api/widgets/<name>/route.ts— always verify the NextAuth session - Add a default size to
DEFAULTSinsrc/hooks/use-widget-size.ts - Add the widget to the dashboard grid in
src/app/page.tsx
Layout sections
The dashboard is divided into named sections, each with its own drag-reorder context:
| Section | Widgets |
|---|---|
| Network | Tailscale, Cloudflared, Host Resources |
| Containers | Docker, Portainer |
| Monitoring | Uptime Kuma |
| Infrastructure | Prometheus Hosts |
| Security | Security (Fail2ban) |
Each section uses DndContext + SortableContext from @dnd-kit. Order is persisted
in localStorage under widget-order:<section>.
v0.1.0 · d0d7a20 · 2026-06-26