Skip to content

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

KeyGridDescription
tiny1×1, 1×2Icon + single number only. No labels, no controls
compact2×2Hero KPI + status indicator
strip4×2KPI left panel | detail / list right panel
detail2×4KPI top | scrollable detail list below
expanded4×4, 8×4Full panel — all data, all metrics
banner8×2Full-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): WidgetSizeKey

Using 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

WidgetComponentAPI routeData source
DockerDockerWidget/api/widgets/dockerDocker socket
TailscaleTailscaleWidget/api/widgets/tailscaleTailscale local API
Cloudflare TunnelCloudflaredWidget/api/widgets/cloudflaredcloudflared metrics
Host ResourcesVpsServersWidget/api/widgets/node-exporternode_exporter
InfrastructurePrometheusHostsWidget/api/widgets/prometheus-hostsPrometheus / node_exporter
SecuritySecurityWidget/api/widgets/securityFail2ban via node_exporter textfile
PortainerPortainerWidget/api/widgets/portainerPortainer REST
Uptime KumaUptimeKumaWidget/api/widgets/uptime-kumaUptime Kuma status page
System SummarySystemSummaryCards/api/widgets/system-summaryAggregated system metrics

Default sizes

WidgetDefaultRationale
Tailscale4×2 (strip)Device list needs horizontal space
Cloudflared2×2 (compact)Single status word is the key signal
Host Resources2×2 (compact)Memory + disk bars fit stacked
Portainer4×2 (strip)Three stats work side-by-side
Docker2×2 (compact)Running count is the key signal
Uptime Kuma8×2 (banner)Status bar wants full width
Infrastructure8×4 (expanded)Multi-host metric rows need width + height
Security4×2 (strip)Ban count + jail table need width

Adding a widget

  1. Create src/components/widgets/<name>.tsx — implement each sizeKey variant
  2. Create src/app/api/widgets/<name>/route.ts — always verify the NextAuth session
  3. Add a default size to DEFAULTS in src/hooks/use-widget-size.ts
  4. 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:

SectionWidgets
NetworkTailscale, Cloudflared, Host Resources
ContainersDocker, Portainer
MonitoringUptime Kuma
InfrastructurePrometheus Hosts
SecuritySecurity (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