From 8c87680078a1519d29779ea1d83a253087688a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Mon, 17 Aug 2026 21:33:33 +0200 Subject: [PATCH] feat: fold traefik into the service widget type, remove the dedicated one Traefik gets the same treatment as the other six services now instead of a bespoke widget type: docker health merged with a single "Routes" stat, via the same internal container-to-container API call (http://traefik:8080/api/http/routers) it already used. No auth needed, same as before. Deleted lib/collectors/traefik.ts, lib/types/traefik-result.ts, and components/widgets/traefik/ entirely - the generic service Widget.tsx and ServiceWidgetResult shape cover it with zero new component code. --- components/widgets/registry.ts | 2 - components/widgets/traefik/Widget.tsx | 72 --------------------------- lib/collectors/scheduler.ts | 18 +------ lib/collectors/service.ts | 3 ++ lib/collectors/services/base-url.ts | 1 + lib/collectors/services/traefik.ts | 17 +++++++ lib/collectors/traefik.ts | 48 ------------------ lib/config/schema.ts | 17 +++---- lib/types/traefik-result.ts | 19 ------- lib/types/widget-result.ts | 2 - 10 files changed, 29 insertions(+), 170 deletions(-) delete mode 100644 components/widgets/traefik/Widget.tsx create mode 100644 lib/collectors/services/traefik.ts delete mode 100644 lib/collectors/traefik.ts delete mode 100644 lib/types/traefik-result.ts diff --git a/components/widgets/registry.ts b/components/widgets/registry.ts index 19af177..83fe9f9 100644 --- a/components/widgets/registry.ts +++ b/components/widgets/registry.ts @@ -6,7 +6,6 @@ import { DockerWidget } from "./docker/Widget"; import { DatabaseWidget } from "./database/Widget"; import { SystemWidget } from "./system/Widget"; import { HttpWidget } from "./http/Widget"; -import { TraefikWidget } from "./traefik/Widget"; import { ServiceWidget } from "./service/Widget"; type WidgetComponent = ComponentType<{ @@ -21,6 +20,5 @@ export const widgetRegistry: { [K in Widget["type"]]: WidgetComponent } = { database: DatabaseWidget, system: SystemWidget, http: HttpWidget, - traefik: TraefikWidget, service: ServiceWidget, }; diff --git a/components/widgets/traefik/Widget.tsx b/components/widgets/traefik/Widget.tsx deleted file mode 100644 index af68f4c..0000000 --- a/components/widgets/traefik/Widget.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import type { Widget } from "@/lib/config/schema"; -import { useWidgetSubscription } from "@/lib/ws/client"; -import { SkeletonLines } from "@/components/widgets/Skeleton"; -import { WidgetCard } from "@/components/widgets/WidgetCard"; -import { StatusDot } from "@/components/widgets/StatusDot"; -import { BrandIcon } from "@/components/widgets/BrandIcon"; - -type TraefikWidget = Extract; - -export function TraefikWidget({ widget, widgetId }: { widget: TraefikWidget; widgetId: string }) { - const result = useWidgetSubscription(widgetId); - const data = result?.type === "traefik" ? result.data : null; - const errorMessage = result?.type === "error" ? result.message : null; - const enabledCount = data?.routers.filter((router) => router.status === "enabled").length ?? 0; - - return ( - -
-
- - {widget.href ? ( - - {widget.name} - - ) : ( - {widget.name} - )} -
-
- {data && ( - - {data.entrypoints.length} entrypoints - - )} - -
-
- {errorMessage && {errorMessage}} - {!result && !errorMessage && } - {data && ( - <> -
-
- - {data.routers.length} - - Routes -
- {enabledCount !== data.routers.length && ( -
- - {enabledCount} - - Enabled -
- )} -
- - {data.middlewaresCount} middlewares - - - )} -
- ); -} diff --git a/lib/collectors/scheduler.ts b/lib/collectors/scheduler.ts index 0aed6ad..cafe4e0 100644 --- a/lib/collectors/scheduler.ts +++ b/lib/collectors/scheduler.ts @@ -5,7 +5,6 @@ import { parseDuration } from "@/lib/config/duration"; import { collectDockerContainer, dockerLimit } from "./docker"; import { collectSystem } from "./system"; import { collectHttp } from "./http"; -import { collectTraefik } from "./traefik"; import { collectService } from "./service"; import type { WidgetResult } from "@/lib/types/widget-result"; @@ -20,14 +19,7 @@ interface Job { consecutiveFailures: number; } -const COLLECTOR_TYPES = new Set([ - "docker", - "database", - "system", - "http", - "traefik", - "service", -]); +const COLLECTOR_TYPES = new Set(["docker", "database", "system", "http", "service"]); function isCollectorWidget(widget: Widget): boolean { return COLLECTOR_TYPES.has(widget.type); @@ -39,7 +31,6 @@ function getIntervalMs(widget: Widget): number { widget.type === "database" || widget.type === "system" || widget.type === "http" || - widget.type === "traefik" || widget.type === "service" ) { return parseDuration(widget.interval); @@ -60,9 +51,6 @@ function sameTarget(a: Widget, b: Widget): boolean { if (a.type === "http" && b.type === "http") { return a.url === b.url && a.method === b.method && a.expect.status === b.expect.status; } - if (a.type === "traefik" && b.type === "traefik") { - return a.apiUrl === b.apiUrl; - } if (a.type === "service" && b.type === "service") { return a.containerName === b.containerName && a.service === b.service && a.showStats === b.showStats; } @@ -88,10 +76,6 @@ async function collect(widget: Widget): Promise { ); return { type: "http", data: { ...data, consecutiveFailures: 0 } }; } - if (widget.type === "traefik") { - const data = await collectTraefik(widget.apiUrl); - return { type: "traefik", data }; - } if (widget.type === "service") { const data = await collectService(widget); return { type: "service", data }; diff --git a/lib/collectors/service.ts b/lib/collectors/service.ts index 2ba6bc6..9b22f45 100644 --- a/lib/collectors/service.ts +++ b/lib/collectors/service.ts @@ -7,6 +7,7 @@ import { collectImmich } from "./services/immich"; import { collectN8n } from "./services/n8n"; import { collectUmami } from "./services/umami"; import { collectHeadscale } from "./services/headscale"; +import { collectTraefikStat } from "./services/traefik"; function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat[]; detail?: string[] }> { switch (widget.service) { @@ -22,6 +23,8 @@ function collectServiceStat(widget: ServiceWidget): Promise<{ stats: ServiceStat return collectUmami(widget); case "headscale": return collectHeadscale(widget); + case "traefik": + return collectTraefikStat(widget); } } diff --git a/lib/collectors/services/base-url.ts b/lib/collectors/services/base-url.ts index 6299986..a55b0b6 100644 --- a/lib/collectors/services/base-url.ts +++ b/lib/collectors/services/base-url.ts @@ -7,6 +7,7 @@ const SERVICE_PORTS: Record = { n8n: 5678, umami: 3000, headscale: 8080, + traefik: 8080, }; export function serviceBaseUrl(widget: ServiceWidget): string { diff --git a/lib/collectors/services/traefik.ts b/lib/collectors/services/traefik.ts new file mode 100644 index 0000000..4e3d6e9 --- /dev/null +++ b/lib/collectors/services/traefik.ts @@ -0,0 +1,17 @@ +import type { TraefikServiceWidget } from "@/lib/config/schema"; +import type { ServiceStat } from "@/lib/types/service-result"; +import { serviceBaseUrl } from "./base-url"; + +interface RawRouter { + status: string; +} + +export async function collectTraefikStat(widget: TraefikServiceWidget): Promise<{ stats: ServiceStat[] }> { + const base = serviceBaseUrl(widget); + const response = await fetch(`${base}/api/http/routers`, { signal: AbortSignal.timeout(5_000) }); + if (!response.ok) { + throw new Error(`Traefik API returned ${response.status}`); + } + const routers = (await response.json()) as RawRouter[]; + return { stats: [{ label: "Routes", value: String(routers.length) }] }; +} diff --git a/lib/collectors/traefik.ts b/lib/collectors/traefik.ts deleted file mode 100644 index 02ce651..0000000 --- a/lib/collectors/traefik.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { TraefikResult } from "@/lib/types/traefik-result"; - -interface RawRouter { - name: string; - rule: string; - service: string; - status: string; - tls?: unknown; - entryPoints?: string[]; -} - -interface RawEntrypoint { - name: string; - address: string; -} - -async function fetchJson(url: string): Promise { - const response = await fetch(url, { signal: AbortSignal.timeout(5_000) }); - if (!response.ok) return null; - return (await response.json()) as T; -} - -export async function collectTraefik(apiUrl: string): Promise { - const base = apiUrl.replace(/\/$/, ""); - - const [routers, entrypoints, middlewares] = await Promise.all([ - fetchJson(`${base}/http/routers`), - fetchJson(`${base}/entrypoints`), - fetchJson(`${base}/http/middlewares`), - ]); - - if (routers === null) { - throw new Error(`Traefik API at ${base}/http/routers is unreachable or returned an error`); - } - - return { - routers: routers.map((router) => ({ - name: router.name, - rule: router.rule, - service: router.service, - status: router.status, - tls: Boolean(router.tls), - entryPoints: router.entryPoints ?? [], - })), - entrypoints: (entrypoints ?? []).map((ep) => ({ name: ep.name, address: ep.address })), - middlewaresCount: middlewares?.length ?? 0, - }; -} diff --git a/lib/config/schema.ts b/lib/config/schema.ts index 7223cf1..771439d 100644 --- a/lib/config/schema.ts +++ b/lib/config/schema.ts @@ -58,15 +58,6 @@ export const httpWidgetSchema = z.object({ .default({ status: 200 }), }); -export const traefikWidgetSchema = z.object({ - type: z.literal("traefik"), - name: z.string().default("Traefik"), - apiUrl: z.string().url(), - href: z.string().url().optional(), - icon: z.string().optional(), - interval: durationSchema.default("15s"), -}); - const serviceCommonFields = { type: z.literal("service") as z.ZodLiteral<"service">, name: z.string(), @@ -116,6 +107,11 @@ export const headscaleServiceWidgetSchema = z.object({ apiToken: z.string(), }); +export const traefikServiceWidgetSchema = z.object({ + ...serviceCommonFields, + service: z.literal("traefik"), +}); + export const serviceWidgetSchema = z.discriminatedUnion("service", [ giteaServiceWidgetSchema, coolifyServiceWidgetSchema, @@ -123,6 +119,7 @@ export const serviceWidgetSchema = z.discriminatedUnion("service", [ n8nServiceWidgetSchema, umamiServiceWidgetSchema, headscaleServiceWidgetSchema, + traefikServiceWidgetSchema, ]); export const widgetSchema = z.discriminatedUnion("type", [ @@ -132,7 +129,6 @@ export const widgetSchema = z.discriminatedUnion("type", [ databaseWidgetSchema, systemWidgetSchema, httpWidgetSchema, - traefikWidgetSchema, serviceWidgetSchema, ]); @@ -194,3 +190,4 @@ export type ImmichServiceWidget = z.infer; export type N8nServiceWidget = z.infer; export type UmamiServiceWidget = z.infer; export type HeadscaleServiceWidget = z.infer; +export type TraefikServiceWidget = z.infer; diff --git a/lib/types/traefik-result.ts b/lib/types/traefik-result.ts deleted file mode 100644 index 546752c..0000000 --- a/lib/types/traefik-result.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface TraefikRouterInfo { - name: string; - rule: string; - service: string; - status: string; - tls: boolean; - entryPoints: string[]; -} - -export interface TraefikEntrypointInfo { - name: string; - address: string; -} - -export interface TraefikResult { - routers: TraefikRouterInfo[]; - entrypoints: TraefikEntrypointInfo[]; - middlewaresCount: number; -} diff --git a/lib/types/widget-result.ts b/lib/types/widget-result.ts index aa85bbd..8b1f8ce 100644 --- a/lib/types/widget-result.ts +++ b/lib/types/widget-result.ts @@ -1,6 +1,5 @@ import type { SystemResult } from "./system-result"; import type { HttpCheckResult } from "./http-result"; -import type { TraefikResult } from "./traefik-result"; import type { ServiceWidgetResult } from "./service-result"; export interface DockerContainerResult { @@ -20,6 +19,5 @@ export type WidgetResult = | { type: "database"; data: DockerContainerResult } | { type: "system"; data: SystemResult } | { type: "http"; data: HttpCheckResult } - | { type: "traefik"; data: TraefikResult } | { type: "service"; data: ServiceWidgetResult } | { type: "error"; message: string };