diff --git a/CODEOWNERS b/CODEOWNERS
index fc790a889..addd37e8c 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -191,6 +191,14 @@ packages/integrations/src/unraid @manuel-rw
apps/docs/docs/integrations/uptime-kuma @ajnart
packages/integrations/src/uptime-kuma @ajnart
+# Traefik - by @Nerdy-Technician
+apps/docs/docs/integrations/traefik @Nerdy-Technician
+apps/docs/docs/widgets/traefik @Nerdy-Technician
+packages/api/src/router/widgets/traefik.ts @Nerdy-Technician
+packages/integrations/src/traefik @Nerdy-Technician
+packages/request-handler/src/traefik.ts @Nerdy-Technician
+packages/widgets/src/traefik @Nerdy-Technician
+
# Exempt Renovate-managed files (no owners)
package.json
**/package.json
diff --git a/apps/docs/docs/integrations/traefik/index.mdx b/apps/docs/docs/integrations/traefik/index.mdx
new file mode 100644
index 000000000..819ede6c6
--- /dev/null
+++ b/apps/docs/docs/integrations/traefik/index.mdx
@@ -0,0 +1,57 @@
+---
+title: "Traefik"
+description: "Cloud-native reverse proxy and load balancer"
+hide_title: true
+---
+
+import { IntegrationHeader } from "@site/src/components/integrations/header";
+import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
+import { AddingIntegration } from "@site/src/components/integrations/adding";
+import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
+import { traefikIntegration } from ".";
+import { traefikWidget } from "../../widgets/traefik";
+
+
+
+### Widgets & Capabilities
+
+
+
+### Adding the integration
+
+
+
+:::info URL configuration
+Use the URL where Traefik exposes its dashboard/API, for example `http://traefik:8080` or `https://traefik.example.com`. Homarr calls Traefik endpoints under `/api` automatically.
+:::
+
+:::warning API requirement
+Traefik must have the API enabled. For local or internal setups this is commonly exposed through `api.insecure=true`; for production setups, expose the `api@internal` service through an authenticated router.
+:::
+
+### Secrets
+
+
diff --git a/apps/docs/docs/integrations/traefik/index.ts b/apps/docs/docs/integrations/traefik/index.ts
new file mode 100644
index 000000000..6e7db80a5
--- /dev/null
+++ b/apps/docs/docs/integrations/traefik/index.ts
@@ -0,0 +1,8 @@
+import { IntegrationDefinition } from "@site/src/types";
+
+export const traefikIntegration: IntegrationDefinition = {
+ name: "Traefik",
+ description: "Cloud-native reverse proxy and load balancer",
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/traefik.svg",
+ path: "../../integrations/traefik",
+};
diff --git a/apps/docs/docs/widgets/traefik/index.mdx b/apps/docs/docs/widgets/traefik/index.mdx
new file mode 100644
index 000000000..dc9f6a6c4
--- /dev/null
+++ b/apps/docs/docs/widgets/traefik/index.mdx
@@ -0,0 +1,35 @@
+---
+title: "Traefik"
+description: "Displays Traefik router, service, middleware, and entry point counts."
+hide_title: true
+---
+
+import { WidgetHeader } from "@site/src/components/widgets/header";
+import { AddingWidget } from "@site/src/components/widgets/adding";
+import { WidgetConfig } from "@site/src/components/widgets/configuration";
+import { WidgetIntegrations } from "@site/src/components/widgets/integrations";
+import { traefikWidget } from ".";
+import { traefikIntegration } from "@site/docs/integrations/traefik";
+
+
+
+This widget displays a compact overview of your Traefik instance, including total routers, entry points, protocol-specific router/service/middleware counts, and warning or error totals when Traefik reports them.
+
+### Supported Integrations
+
+
+
+### Adding the widget
+
+
+
+### Configuration
+
+
diff --git a/apps/docs/docs/widgets/traefik/index.ts b/apps/docs/docs/widgets/traefik/index.ts
new file mode 100644
index 000000000..d55216eef
--- /dev/null
+++ b/apps/docs/docs/widgets/traefik/index.ts
@@ -0,0 +1,31 @@
+import { WidgetDefinition } from "@site/src/types";
+import { IconRoute } from "@tabler/icons-react";
+
+export const traefikWidget: WidgetDefinition = {
+ icon: IconRoute,
+ name: "Traefik",
+ description: "Overview of Traefik routers, services, middlewares, and entry points.",
+ path: "../../widgets/traefik",
+ configuration: {
+ items: [
+ {
+ name: "Show TCP",
+ description: "Display TCP routers, services, and middlewares.",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ {
+ name: "Show UDP",
+ description: "Display UDP routers and services.",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ {
+ name: "Show entry points",
+ description: "Display the configured Traefik entry point names.",
+ values: { type: "boolean" },
+ defaultValue: "yes",
+ },
+ ],
+ },
+};
diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx
index dbb81dc49..2388e2812 100644
--- a/apps/nextjs/src/app/[locale]/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/layout.tsx
@@ -142,7 +142,7 @@ export default async function Layout(props: {
-
+
diff --git a/packages/api/src/router/widgets/index.ts b/packages/api/src/router/widgets/index.ts
index 046f422fe..88aa78d8e 100644
--- a/packages/api/src/router/widgets/index.ts
+++ b/packages/api/src/router/widgets/index.ts
@@ -38,6 +38,7 @@ export const widgetRouter = createTRPCRouter({
umami: lazy(() => import("./umami").then((mod) => mod.umamiRouter)),
vpn: lazy(() => import("./vpn").then((mod) => mod.vpnRouter)),
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
+ traefik: lazy(() => import("./traefik").then((mod) => mod.traefikRouter)),
customApi: lazy(() => import("./custom-api").then((mod) => mod.customApiRouter)),
secrets: lazy(() => import("./widget-secrets").then((mod) => mod.widgetSecretsRouter)),
});
diff --git a/packages/api/src/router/widgets/traefik.ts b/packages/api/src/router/widgets/traefik.ts
new file mode 100644
index 000000000..8f9c775e2
--- /dev/null
+++ b/packages/api/src/router/widgets/traefik.ts
@@ -0,0 +1,31 @@
+import { traefikRequestHandler } from "@homarr/request-handler/traefik";
+
+import { createManyIntegrationMiddleware } from "../../middlewares/integration";
+import { settleIntegrationQueries } from "../../settle-integrations";
+import { createTRPCRouter, publicProcedure } from "../../trpc";
+
+export const traefikRouter = createTRPCRouter({
+ getDashboard: publicProcedure
+ .meta({
+ mcp: {
+ enabled: true,
+ description:
+ "Retrieve Traefik dashboard data, including routers, services, middlewares, and entry points, for the given integration IDs.",
+ },
+ })
+ .concat(createManyIntegrationMiddleware("query", "traefik"))
+ .query(async ({ ctx }) => {
+ return await settleIntegrationQueries(ctx.integrations, async (integration) => {
+ const innerHandler = traefikRequestHandler.handler(integration, {});
+ const { data, timestamp } = await innerHandler.getDataAsync();
+
+ return {
+ integrationId: integration.id,
+ integrationName: integration.name,
+ integrationUrl: integration.url,
+ dashboard: data,
+ updatedAt: timestamp,
+ };
+ });
+ }),
+});
diff --git a/packages/db/migrations/mysql/meta/_journal.json b/packages/db/migrations/mysql/meta/_journal.json
index 4ce2bf038..b2665bb91 100644
--- a/packages/db/migrations/mysql/meta/_journal.json
+++ b/packages/db/migrations/mysql/meta/_journal.json
@@ -295,13 +295,6 @@
"when": 1780764500000,
"tag": "0041_merge_custom_widget_url",
"breakpoints": true
- },
- {
- "idx": 42,
- "version": "5",
- "when": 1783176100870,
- "tag": "0042_add_widget_secret",
- "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/migrations/postgresql/meta/_journal.json b/packages/db/migrations/postgresql/meta/_journal.json
index 829168b9e..495b5e2fb 100644
--- a/packages/db/migrations/postgresql/meta/_journal.json
+++ b/packages/db/migrations/postgresql/meta/_journal.json
@@ -57,13 +57,6 @@
"when": 1780764500000,
"tag": "0007_merge_custom_widget_url",
"breakpoints": true
- },
- {
- "idx": 8,
- "version": "7",
- "when": 1783176100870,
- "tag": "0008_add_widget_secret",
- "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/migrations/sqlite/meta/_journal.json b/packages/db/migrations/sqlite/meta/_journal.json
index a99a995ce..85ee5fc47 100644
--- a/packages/db/migrations/sqlite/meta/_journal.json
+++ b/packages/db/migrations/sqlite/meta/_journal.json
@@ -281,13 +281,6 @@
"when": 1780764500000,
"tag": "0039_merge_custom_widget_url",
"breakpoints": true
- },
- {
- "idx": 40,
- "version": "6",
- "when": 1783176100870,
- "tag": "0040_add_widget_secret",
- "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts
index 344e791fe..bd813adc0 100644
--- a/packages/definitions/src/docs/homarr-docs-sitemap.ts
+++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts
@@ -319,6 +319,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/gluetun"
| "/docs/integrations/peanut"
| "/docs/integrations/beszel"
+ | "/docs/integrations/traefik"
| "/docs/widgets/clock"
| "/docs/widgets/weather"
| "/docs/widgets/app"
@@ -367,4 +368,5 @@ export type HomarrDocumentationPath =
| "/docs/widgets/beszel-system-grid"
| "/docs/widgets/beszel-alerts"
| "/docs/widgets/beszel-system-stats"
+ | "/docs/widgets/traefik"
| "/docs/widgets/custom-api";
diff --git a/packages/definitions/src/docs/integration-doc-slugs.ts b/packages/definitions/src/docs/integration-doc-slugs.ts
index 3d8287383..7d7b19e3b 100644
--- a/packages/definitions/src/docs/integration-doc-slugs.ts
+++ b/packages/definitions/src/docs/integration-doc-slugs.ts
@@ -50,6 +50,7 @@ export const integrationDocSlugs: Record = {
gluetun: "gluetun",
peaNut: "peanut",
beszel: "beszel",
+ traefik: "traefik",
archiveTeamWarrior: null,
mock: null,
};
diff --git a/packages/definitions/src/docs/widget-doc-slugs.ts b/packages/definitions/src/docs/widget-doc-slugs.ts
index 58079787a..7863aadeb 100644
--- a/packages/definitions/src/docs/widget-doc-slugs.ts
+++ b/packages/definitions/src/docs/widget-doc-slugs.ts
@@ -50,5 +50,6 @@ export const widgetDocSlugs: Record = {
beszelSystemGrid: "beszel-system-grid",
beszelAlerts: "beszel-alerts",
beszelSystemStats: "beszel-system-stats",
+ traefik: "traefik",
customApi: "custom-api",
};
diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts
index c15644c2c..0d548f81a 100644
--- a/packages/definitions/src/integration.ts
+++ b/packages/definitions/src/integration.ts
@@ -433,6 +433,14 @@ export const integrationDefs = {
category: ["vpn"],
documentationUrl: createDocumentationLink("/docs/integrations/gluetun"),
},
+ traefik: {
+ name: "Traefik",
+ secretKinds: [[], ["username", "password"], ["apiKey"]],
+ iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/traefik.svg",
+ category: ["reverseProxy"],
+ documentationUrl: createDocumentationLink("/docs/integrations/traefik"),
+ defaultPort: 8080,
+ },
archiveTeamWarrior: {
name: "ArchiveTeam Warrior",
secretKinds: [[], ["username", "password"]],
@@ -553,6 +561,7 @@ export const integrationCategories = [
"documents",
"mediaLibrary",
"uptimeMonitoring",
+ "reverseProxy",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
diff --git a/packages/definitions/src/widget-integration-map.ts b/packages/definitions/src/widget-integration-map.ts
index a69473393..781c37ecd 100644
--- a/packages/definitions/src/widget-integration-map.ts
+++ b/packages/definitions/src/widget-integration-map.ts
@@ -40,6 +40,7 @@ export const widgetIntegrationSupport: Partial {
diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts
index db2028082..f57251971 100644
--- a/packages/definitions/src/widget.ts
+++ b/packages/definitions/src/widget.ts
@@ -48,6 +48,7 @@ export const widgetKinds = [
"beszelSystemGrid",
"beszelAlerts",
"beszelSystemStats",
+ "traefik",
"customApi",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts
index 82c722d50..497122572 100644
--- a/packages/integrations/src/base/creator.ts
+++ b/packages/integrations/src/base/creator.ts
@@ -45,6 +45,7 @@ import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { SeerrIntegration } from "../seerr/seerr-integration";
import { SpeedtestTrackerIntegration } from "../speedtest-tracker/speedtest-tracker-integration";
import { TracearrIntegration } from "../tracearr/tracearr-integration";
+import { TraefikIntegration } from "../traefik/traefik-integration";
import { SynologyIntegration } from "../synology/synology-integration";
import { TrueNasIntegration } from "../truenas/truenas-integration";
import { UmamiIntegration } from "../umami/umami-integration";
@@ -127,6 +128,7 @@ export const integrationCreators = {
uptimeKuma: UptimeKumaIntegration,
peaNut: PeaNutIntegration,
beszel: BeszelIntegration,
+ traefik: TraefikIntegration,
} satisfies Record Promise]>;
type IntegrationInstanceOfKind = {
diff --git a/packages/integrations/src/gotify/gotify-integration.ts b/packages/integrations/src/gotify/gotify-integration.ts
index 1d0f5eb8c..829e7abcd 100644
--- a/packages/integrations/src/gotify/gotify-integration.ts
+++ b/packages/integrations/src/gotify/gotify-integration.ts
@@ -1,12 +1,13 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
+import { ImageProxy } from "@homarr/image-proxy";
import { Integration } from "../base/integration";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { Notification } from "../interfaces/notifications/notification-types";
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
-import { gotifyMessagesResponseSchema } from "./gotify-schema";
+import { gotifyApplicationsResponseSchema, gotifyMessagesResponseSchema } from "./gotify-schema";
export class GotifyIntegration extends Integration implements INotificationsIntegration {
public async testingAsync(input: IntegrationTestingInput): Promise {
@@ -21,23 +22,90 @@ export class GotifyIntegration extends Integration implements INotificationsInte
return { Authorization: `Basic ${credentials}` };
}
+ private getMessagesUrl(applicationId: number) {
+ const url = this.externalUrl("/");
+ url.hash = `/messages/${applicationId}`;
+ return url.toString();
+ }
+
+ private async getApplicationsByIdAsync(): Promise