fix(gauge): render single value arc with correct Tailwind v4 colors

The gauge rendered as multiple black rings. Two causes:

1. recharts RadialBarChart draws each data entry as a CONCENTRIC RING, not an
   arc segment, so the 3 "track band" entries + value produced 4 nested rings.
   Render a single value arc over a neutral background track instead, colored
   by status, with the readout absolutely centered (replacing the -mt-12 hack).

2. The fills used hsl(var(--primary)) / hsl(var(--chart-1)) etc., but this
   project's Tailwind v4 theme (index.css) defines colors as --color-* holding
   full hex values (--color-primary: #4f8cff). So the references were doubly
   invalid (wrong name + hsl() wrapping a hex) -> invalid SVG fill defaults to
   black. Use var(--color-*) directly, with the semantically correct chart
   colors: ok=--color-chart-2 (green), warn=--color-chart-3 (amber),
   crit=--color-chart-4 (red).

Also fix the same hsl(var(--x)) -> var(--color-x) bug in LineSeriesChart's
tooltip contentStyle (popover/border/popover-foreground). The line stroke
palette already used the correct var(--color-chart-N) form.

166/166 frontend tests pass; typecheck + ESLint clean.
This commit is contained in:
Developer
2026-07-11 13:05:42 +00:00
parent b011d2421b
commit 50c0c9b548
6 changed files with 43 additions and 52 deletions
+2 -1
View File
@@ -2,7 +2,7 @@
dir: frontend/src/components
## role
Reusable UI component library providing presentation-layer building blocks (tables, cards, charts, dialogs, widgets) for the frontend application.
Shared UI component library providing reusable React display, table, dialog, card, chart, and widget components for the frontend application.
## parent
index: frontend/src/.pi-map.index.md
map: frontend/src/.pi-map.md
@@ -28,6 +28,7 @@ map: frontend/src/.pi-map.md
- PinnedServiceLink.tsx
- SectionCard.tsx
- SelectionRailCard.tsx
- ServiceTestPanel.tsx
- SessionActivityPanel.tsx
- TabbedCard.tsx
- WidgetConfigDialog.tsx
+5 -4
View File
@@ -4,7 +4,7 @@ dir: frontend/src/components
index: frontend/src/components/.pi-map.index.md
## role
Reusable UI component library providing presentation-layer building blocks (tables, cards, charts, dialogs, widgets) for the frontend application.
Shared UI component library providing reusable React display, table, dialog, card, chart, and widget components for the frontend application.
## files
- BackupAlertsTable.tsx | Renders a responsive table of backup alerts with severity badges and acknowledge actions, switching between desktop table and mobile card layouts. | exp: func:BackupAlertsTable({ alerts, onAcknowledge }: Props), call:useIsMobile, call:onAcknowledge, call:alerts.map, call:severityVariant, call:formatTimestamp | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, @/components/ui/mobile-card, ../hooks/useIsMobile, ../types/backups, useIsMobile hook, BackupAlert type
- BackupDashboardWidget.tsx | Displays a dashboard widget summarizing backup job statistics including total jobs, 24-hour success rate, active alerts, and last failure timestamp. | exp: func:BackupDashboardWidget(), call:useBackupDashboard, call:new Date(data.last_failed_at * 1000).toLocaleString | dep: @/components/ui/badge, @/components/ui/card, ../hooks/useBackups
@@ -14,18 +14,19 @@ Reusable UI component library providing presentation-layer building blocks (tabl
- DialogFooter.tsx | Renders a dialog footer component with cancel, optional secondary action, and confirm buttons, mapping legacy MUI color/variant props to shadcn Button variants. | exp: func:DialogFooter({ onCancel, cancelLabel = "Cancel", onConfirm, confirmLabel, confirmBusyLabel, confirmDisabled, confirmColor = "primary", confirmVariant = "contained", confirmStartIcon, secondaryAction, }: DialogFooterProps), call:resolveConfirmVariant | dep: react, @/components/ui/button
- HoverEditButton.tsx | Renders a hover-reveal edit button for desktop and always-visible edit button for mobile, preserving legacy CSS class hooks. | exp: func:HoverEditButton({ onClick, label = "Edit", mobile = "always", }: HoverEditButtonProps), call:e.stopPropagation, call:onClick | dep: lucide-react, @/components/ui/button
- LibraryOverview.tsx | Renders a two-column responsive grid displaying movie and TV library counts using shadcn/ui Card components | exp: func:LibraryOverview({ libraries }: Props), call:libraries.filter, call:movieLibs.map, call:lib.total.toLocaleString, call:lib.movies.toLocaleString, call:tvLibs.map, call:lib.series.toLocaleString | dep: @/components/ui/card, ../types
- LineSeriesChart.tsx | Renders multiple time-series as a shared recharts line chart with merged data and formatted timestamps. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, }: LineSeriesChartProps), call:mergeSeries, call:formatTime, call:Number, call:series.map | dep: recharts
- LineSeriesChart.tsx | Renders multiple time-series as a shared recharts line-chart with merged timestamps. | exp: SeriesPoint, ChartSeries, func:LineSeriesChart({ series, height = 300, }: LineSeriesChartProps), call:mergeSeries, call:formatTime, call:Number, call:series.map | dep: recharts
- MetricCard.tsx | Renders a compact metric display card with label, value, and optional subtext using Tailwind CSS styling. | exp: func:MetricCard({ label, value, subtext }: Props) | dep: @/components/ui/card
- NowPlaying.tsx | Renders a now-playing panel by wrapping SessionActivityPanel with a specific empty message for user activity sessions. | exp: func:NowPlaying({ sessions, onSelectSession }: Props) | dep: ../types, ./SessionActivityPanel
- PinnedServiceLink.tsx | Renders a navigable card-shaped button for pinned service shortcuts on dashboards and provides a helper to construct service target paths. | exp: PinnedServiceLinkProps, func:PinnedServiceLink({ label, target, icon: Icon = Boxes, className, }: PinnedServiceLinkProps), call:useNavigate, call:navigate, call:cn, func:serviceLinkTarget(serviceType: string, serviceId: string, tab: string) → string | dep: react-router-dom, lucide-react, @/lib/utils
- SectionCard.tsx | A reusable card component that renders a titled section with optional description and action, built on shadcn/ui Card primitives for comfortable density layout. | exp: func:SectionCard({ title, description, action, children, }: SectionCardProps) | dep: react, @/components/ui/card
- SelectionRailCard.tsx | A reusable card component for a selection rail UI with titled header, scrollable body, and optional footer, preserving legacy API compatibility during a migration from MUI. | exp: func:SelectionRailCard({ title, description, children, footer, minHeight = 420, }: SelectionRailCardProps) | dep: react, @/components/ui/card
- ServiceTestPanel.tsx | Presentational component rendering a "Test credentials" panel with test button, result display, and a "Save anyway" checkbox. | exp: func:ServiceTestPanel({ result, isPending, saveAnyway, onTest, onSaveAnywayChange, disabled, }: Props), call:onSaveAnywayChange | dep: @/components/ui/alert, @/components/ui/button, ../types
- SessionActivityPanel.tsx | Renders a scrollable table displaying live media session activity details with status badges and optional session selection callbacks. | exp: func:SessionActivityPanel({ sessions, emptyMessage = "No live sessions matched to this user.", selectedUserLabel, onSelectSession, }: Props), call:buildStatusSummary, call:sessions.map, call:formatStateLabel, call:onSelectSession, call:sessionStateVariant, call:event.stopPropagation | dep: @/components/ui/badge, @/components/ui/button, @/components/ui/table, ../types
- TabbedCard.tsx | Renders a card with a line-style tab bar header and content area, acting as a controlled wrapper around shadcn/ui Tabs for backward-compatible API migration from MUI. | exp: func:TabbedCard({ value, onChange, tabs, children, }: TabbedCardProps), call:onChange, call:String | dep: react, @/components/ui/card, @/components/ui/tabs
- WidgetConfigDialog.tsx | Provides a user interface dialog and form for creating, configuring, editing, and managing the sort order of dashboard widgets and widget references. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:useMemo, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui/*
- WidgetConfigDialog.tsx | Provides a dialog and sheet interface for creating, configuring, reordering, and managing the lifecycle of UI widgets and their dashboard references. | exp: func:WidgetConfigDialog({ open, onClose, serviceId, dashboardScope, editWidgetId, }: Props), call:useWidgetInstances, call:useMemo, call:useServiceInstances, call:useTasks, call:useSaveWidgetInstance, call:useDeleteWidgetInstance, call:useWidgetReferences, call:useCreateWidgetReference, call:useDeleteWidgetReference, call:useDetachWidgetReference, call:useUpdateWidgetReference, call:useState, call:Boolean, call:useEffect, call:instances.find, call:references.find, call:startEdit, call:setDraft, call:SERVICE_REGISTRY[ services.find((s) => s.id === serviceId)?.service_type ?? "" ]?.widgets.find, call:services.find, call:setDraftBaseline, call:onClose, call:saveWidget.mutateAsync, call:reset, call:updateRef.mutateAsync, call:deleteWidget.mutateAsync, call:[...instances].sort, call:references.map, call:[...owned, ...refs].sort, call:instances.map, call:existingSearch.toLowerCase().trim, call:allWidgets .filter((w) => !onDashboard.has(w.id)) .filter, call:onDashboard.has, call:w.title.toLowerCase().includes, call:w.widget_kind.toLowerCase().includes, call:createRef.mutateAsync, call:deleteRef.mutateAsync, call:detachRef.mutateAsync, call:SERVICE_REGISTRY[ services.find((s) => s.id === draft.serviceId)?.service_type ?? "" ]?.widgets.find, call:useIsMobile, call:String, call:Number, call:combinedWidgets.map, call:bindingLabel, call:moveInstance, call:toggleEnabled, call:handleDetach, call:handleRemoveReference, call:removeInstance, call:setShowExisting, call:setExistingSearch, call:availableWidgets.map, call:handleAddReference, call:Object.values(BUILTIN_WIDGETS).map, call:startAddBuiltIn, call:services .filter((s) => s.enabled) // When scoped to a service Overview, only show widgets for THAT // service instance's type (not all services' widgets). .filter((s) => !serviceId || s.id === serviceId) .flatMap, call:(SERVICE_REGISTRY[s.service_type]?.widgets ?? []).map, call:startAddService, call:handleClose, call:JSON.stringify | dep: react, @/components/ui/dialog, @/components/ui/button, @/components/ui/input, @/components/ui/textarea, @/components/ui/label, @/components/ui/switch, @/components/ui/select, @/components/ui/badge, @/components/ui/alert, lucide-react, ../hooks/useWidgets, ../hooks/useServices, ../hooks/useSettings, ../hooks/useIsMobile, @/components/ui/sheet-form, ../types, ../integrations/registry, @/components/ui (dialog, button, input, textarea, label, switch, select, badge, alert, sheet-form)
- WidgetInstance.tsx | Renders a widget instance card that dynamically resolves and displays a widget component, with optional edit and copy actions. | exp: func:WidgetInstanceCard({ widget, onEdit, onCopy }: Props), call:useServiceInstances, call:resolveWidget, call:onCopy, call:onEdit | dep: @/components/ui/alert, @/components/ui/button, lucide-react, ../hooks/useServices, ../integrations/registry, ../types, ./SectionCard
## arch
React functional components with responsive design patterns, built on shadcn/ui and Tailwind CSS with adapter layers for backward-compatible MUI-to-shadcn migration.
Presentational React/TypeScript components built on shadcn/ui primitives with responsive (desktop/mobile) layouts, wrapping recharts for visualization and maintaining backward-compatible APIs during migration from MUI.
## tags
call:use, components, ui, card, widget, table, backup, call:on
## symbols
+3 -3
View File
@@ -72,10 +72,10 @@ export function LineSeriesChart({
<Tooltip
labelFormatter={(label) => formatTime(Number(label))}
contentStyle={{
backgroundColor: "hsl(var(--popover))",
border: "1px solid hsl(var(--border))",
backgroundColor: "var(--color-popover)",
border: "1px solid var(--color-border)",
borderRadius: "0.5rem",
color: "hsl(var(--popover-foreground))",
color: "var(--color-popover-foreground)",
}}
/>
{series.map((s, i) => (
+4 -4
View File
@@ -2,7 +2,7 @@
dir: frontend/src/widgets
## role
Dashboard widget components that render visual data for various integrations (Alertmanager, Backups, Jellyfin, Prometheus, qBittorrent, SSH tasks, static content) within the monitoring application.
Provides self-contained dashboard widget components for rendering various data sources (Alertmanager, backups, Jellyfin, Prometheus metrics, qBittorrent, SSH tasks, and static content) in a unified card layout.
## parent
index: frontend/src/.pi-map.index.md
map: frontend/src/.pi-map.md
@@ -15,9 +15,9 @@ map: frontend/src/.pi-map.md
- BackupsWidget.tsx
- JellyfinNowPlayingWidget.tsx
- JellyfinWidget.tsx
- PrometheusChartWidget.tsx
- PrometheusGaugeWidget.tsx
- PrometheusMeanWidget.tsx
- MetricChartWidget.tsx
- MetricGaugeWidget.tsx
- MetricMeanWidget.tsx
- PrometheusMetricWidget.tsx
- QbittorrentActiveTorrentsWidget.tsx
- QbittorrentSpeedWidget.tsx
+11 -11
View File
@@ -4,34 +4,34 @@ dir: frontend/src/widgets
index: frontend/src/widgets/.pi-map.index.md
## role
Dashboard widget components that render visual data for various integrations (Alertmanager, Backups, Jellyfin, Prometheus, qBittorrent, SSH tasks, static content) within the monitoring application.
Provides self-contained dashboard widget components for rendering various data sources (Alertmanager, backups, Jellyfin, Prometheus metrics, qBittorrent, SSH tasks, and static content) in a unified card layout.
## files
- AlertmanagerAlertsWidget.tsx | Renders an Alertmanager alerts dashboard widget displaying alert summaries, severity badges, and individual alert details. | exp: func:AlertmanagerAlertsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Object.entries(summary.by_severity).map, call:severityVariant, call:alerts.slice(0, 5).map | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
- BackupsWidget.tsx | Displays a dashboard widget showing backup job metrics including total jobs, 24-hour success rate, active alerts, and last failure timestamp. | exp: func:BackupsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:new Date(summary.last_failed_at * 1000).toLocaleString | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types/backups, ../types
- JellyfinNowPlayingWidget.tsx | Displays a Jellyfin now-playing widget that fetches and renders active media sessions using a custom data hook and session activity panel. | exp: func:JellyfinNowPlayingWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Array.isArray | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SessionActivityPanel, ../components/SectionCard, ../hooks/useWidgets, ../types, SessionActivityPanel, SectionCard, useWidgetData, types
- JellyfinWidget.tsx | Displays Jellyfin media server activity sessions in a widget with loading, error, and empty states. | exp: func:JellyfinWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Array.isArray | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SessionActivityPanel, ../components/SectionCard, ../hooks/useWidgets, ../types
- PrometheusChartWidget.tsx | Renders a Prometheus query widget that fetches time-series data and displays it as a line chart, with loading, error, and empty states. | exp: func:PrometheusChartWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData hook, WidgetInstance type
- PrometheusGaugeWidget.tsx | Renders a Prometheus metric as a radial gauge widget with configurable warning/critical threshold bands. | exp: func:PrometheusGaugeWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Math.max, call:Math.round, call:Math.min, call:toPercent, call:trackCells.map, call:formatValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, recharts, SectionCard, useWidgetData
- PrometheusMeanWidget.tsx | Displays a single Prometheus metric value (mean) with adaptive formatting and unit support in a dashboard widget. | exp: func:PrometheusMeanWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMean | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
- PrometheusMetricWidget.tsx | A React component that fetches and displays Prometheus metric data from a widget, handling loading, error, and data states with formatted output for scalar and vector result types. | exp: func:PrometheusMetricWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatPrometheusValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
- MetricChartWidget.tsx | Renders a metric chart widget that fetches time-series data and displays it in a line chart with loading, error, and empty states. | exp: func:MetricChartWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData, WidgetInstance type
- MetricGaugeWidget.tsx | Renders a metric gauge widget using a radial bar chart with threshold-based color bands for visualizing a single numeric value. | exp: func:MetricGaugeWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Math.max, call:Math.round, call:Math.min, call:toPercent, call:formatValue | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, recharts, SectionCard, useWidgetData, WidgetInstance types
- MetricMeanWidget.tsx | Displays a metric mean value from Prometheus/PromQL query results with conditional loading, error, and empty states. | exp: func:MetricMeanWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMean | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
- PrometheusMetricWidget.tsx | Displays a Prometheus metric widget that fetches and formats time-series data with loading and error states. | exp: func:PrometheusMetricWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:formatMetricResult | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
- QbittorrentActiveTorrentsWidget.tsx | Displays a list of active torrents from a qBittorrent instance with download/upload speeds and status badges in a dashboard widget. | exp: func:QbittorrentActiveTorrentsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:torrents.map, call:formatSpeed | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData hook, WidgetInstance type
- QbittorrentSpeedWidget.tsx | Renders a qBittorrent download/upload speed widget with a line series chart, handling loading, error, and empty states. | exp: func:QbittorrentSpeedWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/LineSeriesChart, ../components/SectionCard, ../hooks/useWidgets, ../types, LineSeriesChart, SectionCard, useWidgetData hook
- QbittorrentTotalsWidget.tsx | Displays qBittorrent torrent totals and per-state breakdown using a widget with loading, error, and data states. | exp: func:QbittorrentTotalsWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData, call:Object.keys, call:Object.entries(payload.by_state).map | dep: @/components/ui/alert, @/components/ui/badge, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
- SshTaskWidget.tsx | Displays SSH task execution results with exit status, stdout, and stderr in a polling widget card | exp: func:SshTaskWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: @/components/ui/alert, @/components/ui/skeleton, ../components/SectionCard, ../hooks/useWidgets, ../types
- StaticWidget.tsx | Renders a static text widget that displays fetched text content or a fallback message within a section card. | exp: func:StaticWidget({ widget, refreshIntervalMs, description, }: Props), call:useWidgetData | dep: ../components/SectionCard, ../hooks/useWidgets, ../types, SectionCard, useWidgetData, WidgetInstance
- index.ts | Barrel file that re-exports all dashboard widget components from a widgets directory. | dep: AlertmanagerAlertsWidget, BackupsWidget, PrometheusChartWidget, PrometheusGaugeWidget, PrometheusMeanWidget, JellyfinWidget, JellyfinNowPlayingWidget, PrometheusMetricWidget, QbittorrentActiveTorrentsWidget, QbittorrentSpeedWidget, QbittorrentTotalsWidget, SshTaskWidget, StaticWidget
- index.ts | Barrel file that re-exports all widget components from a dashboard/widget module. | dep: AlertmanagerAlertsWidget, BackupsWidget, MetricChartWidget, MetricGaugeWidget, MetricMeanWidget, JellyfinWidget, JellyfinNowPlayingWidget, PrometheusMetricWidget, QbittorrentActiveTorrentsWidget, QbittorrentSpeedWidget, QbittorrentTotalsWidget, SshTaskWidget, StaticWidget
## arch
React functional components using a per-widget pattern with consistent loading/error/empty state handling, custom data hooks, and barrel-file aggregation for modular dashboard composition.
Composable React functional components using custom data-fetching hooks, each implementing independent loading/error/empty state handling with chart visualizations, exported via a barrel file for modular dashboard composition.
## tags
widget, components, data, ui, call:use, prometheus, sectioncard, types
widget, components, data, ui, call:use, metric, sectioncard, types
## symbols
- AlertmanagerAlertsWidget
- BackupsWidget
- JellyfinNowPlayingWidget
- JellyfinWidget
- PrometheusChartWidget
- PrometheusGaugeWidget
- PrometheusMeanWidget
- MetricChartWidget
- MetricGaugeWidget
- MetricMeanWidget
- PrometheusMetricWidget
## workflows
- change widgets behavior
+18 -29
View File
@@ -60,22 +60,18 @@ export function MetricGaugeWidget({
const toPercent = (v: number) => Math.round(((v - min) / range) * 100);
const valuePct = Math.max(0, Math.min(100, toPercent(value)));
// Build track cells: green / amber / red when bands are set, else neutral.
const trackCells = hasBands
? [
{ pct: toPercent(warnAt!), fill: "hsl(var(--chart-1))" }, // green
{ pct: toPercent(critAt!), fill: "hsl(var(--chart-4))" }, // amber
{ pct: 100, fill: "hsl(var(--destructive))" }, // red
]
: [{ pct: 100, fill: "hsl(var(--muted))" }];
// The value arc is colored by status (green < warn, amber < crit, red >= crit)
// and drawn over a neutral full-track background. We intentionally do NOT
// render separate "track band" rings: recharts draws each data entry as a
// concentric ring (not an arc segment), so multiple entries produced nested
// rings that looked broken.
const valueColor = hasBands
? value >= critAt!
? "hsl(var(--destructive))"
? "var(--color-chart-4)" // red (destructive)
: value >= warnAt!
? "hsl(var(--chart-4))"
: "hsl(var(--chart-1))"
: "hsl(var(--primary))";
? "var(--color-chart-3)" // amber (warning)
: "var(--color-chart-2)" // green (success)
: "var(--color-primary)";
return (
<SectionCard title={widget.title} description={description}>
@@ -86,19 +82,12 @@ export function MetricGaugeWidget({
<AlertDescription>{data.error}</AlertDescription>
</Alert>
) : gauge ? (
<div className="flex flex-col items-center">
<ResponsiveContainer width="100%" height={200}>
<div className="relative w-full" style={{ height: 220 }}>
<ResponsiveContainer width="100%" height="100%">
<RadialBarChart
innerRadius="65%"
innerRadius="72%"
outerRadius="100%"
data={[
...trackCells.map((c) => ({
name: "track",
pct: c.pct,
fill: c.fill,
})),
{ name: "value", pct: valuePct, fill: valueColor },
]}
data={[{ name: "value", pct: valuePct, fill: valueColor }]}
startAngle={90}
endAngle={-270}
>
@@ -109,18 +98,18 @@ export function MetricGaugeWidget({
tick={false}
/>
<RadialBar
background={{ fill: "hsl(var(--muted))" }}
background={{ fill: "var(--color-muted)" }}
dataKey="pct"
cornerRadius={6}
cornerRadius={10}
/>
</RadialBarChart>
</ResponsiveContainer>
<div className="-mt-12 flex flex-col items-center">
<span className="text-2xl font-bold" style={{ color: valueColor }}>
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
<span className="text-3xl font-bold" style={{ color: valueColor }}>
{formatValue(value, gauge.unit)}
</span>
{hasBands ? (
<span className="text-xs text-muted-foreground">
<span className="mt-1 text-xs text-muted-foreground">
warn {formatValue(warnAt!, gauge.unit)} · crit{" "}
{formatValue(critAt!, gauge.unit)}
</span>