feat: expand advanced widget experiences
This commit is contained in:
1 parent
9437d211c8
commit
c099a8eef6
119 files changed
+3624
-844
No files matched your search
@@ -0,0 +1,10 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
export const throwIfDockerOperationsFailed = (results: readonly PromiseSettledResult<unknown>[]) => {
|
||||
if (results.some((result) => result.status === "rejected")) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "One or more Docker container operations failed",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
|
||||
import { dockerMiddleware } from "../../middlewares/docker";
|
||||
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
||||
import { throwIfDockerOperationsFailed } from "./docker-operation-results";
|
||||
|
||||
export const dockerRouter = createTRPCRouter({
|
||||
getContainers: permissionRequiredProcedure
|
||||
@@ -45,12 +46,13 @@ export const dockerRouter = createTRPCRouter({
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
const results = await Promise.allSettled(
|
||||
input.ids.map(async (id) => {
|
||||
const container = await getContainerOrThrowAsync(id);
|
||||
await container.start();
|
||||
}),
|
||||
);
|
||||
throwIfDockerOperationsFailed(results);
|
||||
}),
|
||||
stopAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -63,12 +65,13 @@ export const dockerRouter = createTRPCRouter({
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
const results = await Promise.allSettled(
|
||||
input.ids.map(async (id) => {
|
||||
const container = await getContainerOrThrowAsync(id);
|
||||
await container.stop();
|
||||
}),
|
||||
);
|
||||
throwIfDockerOperationsFailed(results);
|
||||
}),
|
||||
restartAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -82,12 +85,13 @@ export const dockerRouter = createTRPCRouter({
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
const results = await Promise.allSettled(
|
||||
input.ids.map(async (id) => {
|
||||
const container = await getContainerOrThrowAsync(id);
|
||||
await container.restart();
|
||||
}),
|
||||
);
|
||||
throwIfDockerOperationsFailed(results);
|
||||
}),
|
||||
removeAll: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
@@ -101,12 +105,13 @@ export const dockerRouter = createTRPCRouter({
|
||||
.concat(dockerMiddleware())
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
await Promise.allSettled(
|
||||
const results = await Promise.allSettled(
|
||||
input.ids.map(async (id) => {
|
||||
const container = await getContainerOrThrowAsync(id);
|
||||
await container.remove();
|
||||
}),
|
||||
);
|
||||
throwIfDockerOperationsFailed(results);
|
||||
}),
|
||||
logs: permissionRequiredProcedure
|
||||
.requiresPermission("admin")
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { throwIfDockerOperationsFailed } from "../../docker/docker-operation-results";
|
||||
|
||||
describe("Docker operation results", () => {
|
||||
test("accepts an all-success result", async () => {
|
||||
const results = await Promise.allSettled([Promise.resolve(), Promise.resolve()]);
|
||||
|
||||
expect(() => throwIfDockerOperationsFailed(results)).not.toThrow();
|
||||
});
|
||||
|
||||
test("throws a sanitized error after a partial failure", async () => {
|
||||
const results = await Promise.allSettled([
|
||||
Promise.resolve(),
|
||||
Promise.reject(new Error("sensitive Docker daemon response")),
|
||||
]);
|
||||
|
||||
expect(() => throwIfDockerOperationsFailed(results)).toThrow(
|
||||
new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "One or more Docker container operations failed",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -8,17 +8,34 @@ import type { Database } from "@homarr/db";
|
||||
import { integrations, integrationUserPermissions, users } from "@homarr/db/schema";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import type { MediaRequest, MediaRequestStats } from "@homarr/integrations/types";
|
||||
import type {
|
||||
MediaRequest,
|
||||
MediaRequestStats,
|
||||
SpeedtestTrackerDashboardData,
|
||||
TracearrDashboardData,
|
||||
TraefikDashboardData,
|
||||
UpsSummary,
|
||||
UptimeKumaDashboardData,
|
||||
} from "@homarr/integrations/types";
|
||||
import type * as MediaRequestListHandlerModule from "@homarr/request-handler/media-request-list";
|
||||
|
||||
import { calendarRouter } from "../../widgets/calendar";
|
||||
import { dnsHoleRouter } from "../../widgets/dns-hole";
|
||||
import { downloadsRouter } from "../../widgets/downloads";
|
||||
import { beszelRouter } from "../../widgets/beszel";
|
||||
import { firewallRouter } from "../../widgets/firewall";
|
||||
import { healthMonitoringRouter } from "../../widgets/health-monitoring";
|
||||
import { indexerManagerRouter } from "../../widgets/indexer-manager";
|
||||
import { mediaOrganizerRouter } from "../../widgets/media-organizer";
|
||||
import { mediaRequestsRouter } from "../../widgets/media-requests";
|
||||
import { mediaServerRouter } from "../../widgets/media-server";
|
||||
import { networkControllerRouter } from "../../widgets/network-controller";
|
||||
import { notificationsRouter } from "../../widgets/notifications";
|
||||
import { speedtestTrackerRouter } from "../../widgets/speedtest-tracker";
|
||||
import { tracearrRouter } from "../../widgets/tracearr";
|
||||
import { traefikRouter } from "../../widgets/traefik";
|
||||
import { upsRouter } from "../../widgets/ups";
|
||||
import { uptimeKumaRouter } from "../../widgets/uptime-kuma";
|
||||
import { vpnRouter } from "../../widgets/vpn";
|
||||
|
||||
vi.hoisted(() => {
|
||||
@@ -44,6 +61,65 @@ vi.mock("@homarr/request-handler/media-server", () => ({
|
||||
mediaServerRequestHandler: { handler: (integration: { name: string }) => createHandler(integration, []) },
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/dns-hole", () => ({
|
||||
dnsHoleRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
status: "enabled",
|
||||
domainsBeingBlocked: 10,
|
||||
adsBlockedToday: 2,
|
||||
adsBlockedTodayPercentage: 20,
|
||||
dnsQueriesToday: 10,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/downloads", () => ({
|
||||
downloadClientRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
items: [],
|
||||
status: { paused: false, rates: { down: 0, up: 0 }, types: ["torrent"] },
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/health-monitoring", () => ({
|
||||
systemInfoRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
version: "1.0",
|
||||
cpuModelName: "CPU",
|
||||
cpuUtilization: 10,
|
||||
memUsedInBytes: 1,
|
||||
memAvailableInBytes: 2,
|
||||
uptime: 1,
|
||||
network: null,
|
||||
loadAverage: null,
|
||||
rebootRequired: false,
|
||||
availablePkgUpdates: 0,
|
||||
cpuTemp: undefined,
|
||||
fileSystem: [],
|
||||
smart: [],
|
||||
gpu: [],
|
||||
}),
|
||||
},
|
||||
clusterInfoRequestHandler: { handler: () => createHandler({ name: "Online" }, {}) },
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/network-controller", () => ({
|
||||
networkControllerRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
wanStatus: "enabled",
|
||||
www: { status: "enabled", latency: 5, ping: 6, uptime: 99 },
|
||||
wifi: { status: "enabled", users: 2, guests: 1 },
|
||||
lan: { status: "enabled", users: 3, guests: 0 },
|
||||
vpn: { status: "enabled", users: 1 },
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/media-organizer", () => ({
|
||||
mediaOrganizerRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
@@ -78,6 +154,88 @@ vi.mock("@homarr/request-handler/vpn", () => ({
|
||||
vpnSummaryHandler: { handler: (integration: { name: string }) => createHandler(integration, { connected: true }) },
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/tracearr", () => ({
|
||||
tracearrRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
stats: { activeStreams: 0, totalUsers: 1, totalSessions: 1, recentViolations: 0, timestamp: "now" },
|
||||
streams: {
|
||||
data: [],
|
||||
summary: { total: 0, transcodes: 0, directStreams: 0, directPlays: 0, totalBitrate: "0", byServer: [] },
|
||||
},
|
||||
violations: null,
|
||||
recentActivity: null,
|
||||
} satisfies TracearrDashboardData),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/speedtest-tracker", () => ({
|
||||
speedtestTrackerRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
latestResult: null,
|
||||
stats: null,
|
||||
recentResults: [],
|
||||
} satisfies SpeedtestTrackerDashboardData),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/ups", () => ({
|
||||
upsSummariesRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, [
|
||||
{
|
||||
id: "healthy-ups",
|
||||
name: "Healthy UPS",
|
||||
manufacturer: null,
|
||||
model: null,
|
||||
serial: null,
|
||||
status: "online",
|
||||
batteryCharge: 100,
|
||||
batteryRuntime: 3600,
|
||||
batteryVoltage: null,
|
||||
load: 10,
|
||||
inputVoltage: null,
|
||||
outputVoltage: null,
|
||||
power: null,
|
||||
temperature: null,
|
||||
} satisfies UpsSummary,
|
||||
]),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/traefik", () => {
|
||||
const emptySummary = { total: 0, enabled: 0, warnings: 0, errors: 0 };
|
||||
return {
|
||||
traefikRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
version: "3.0.0",
|
||||
entryPoints: [],
|
||||
resources: [],
|
||||
failedEndpoints: [],
|
||||
http: { routers: emptySummary, services: emptySummary, middlewares: emptySummary },
|
||||
tcp: { routers: emptySummary, services: emptySummary, middlewares: emptySummary },
|
||||
udp: { routers: emptySummary, services: emptySummary },
|
||||
} satisfies TraefikDashboardData),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@homarr/request-handler/uptime-kuma", () => ({
|
||||
uptimeKumaRequestHandler: {
|
||||
handler: (integration: { name: string }) =>
|
||||
createHandler(integration, {
|
||||
totalMonitors: 1,
|
||||
upCount: 1,
|
||||
downCount: 0,
|
||||
pausedCount: 0,
|
||||
averageUptimePercent: 100,
|
||||
monitors: [{ id: 1, name: "Healthy monitor", status: "up", uptimePercent24h: 100 }],
|
||||
} satisfies UptimeKumaDashboardData),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@homarr/request-handler/media-request-list", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof MediaRequestListHandlerModule>();
|
||||
return {
|
||||
@@ -124,14 +282,14 @@ const createSession = (userId: string): Session => ({
|
||||
expires: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const setupAsync = async (kind: IntegrationKind) => {
|
||||
const setupAsync = async (kind: IntegrationKind, options: { allOffline?: boolean } = {}) => {
|
||||
const db = createDb();
|
||||
const userId = createId();
|
||||
const onlineId = createId();
|
||||
const offlineId = createId();
|
||||
await db.insert(users).values({ id: userId });
|
||||
await db.insert(integrations).values([
|
||||
{ id: onlineId, kind, name: "Online", url: "https://online.example.com" },
|
||||
{ id: onlineId, kind, name: options.allOffline ? "Offline" : "Online", url: "https://online.example.com" },
|
||||
{ id: offlineId, kind, name: "Offline", url: "https://offline.example.com" },
|
||||
]);
|
||||
await db.insert(integrationUserPermissions).values([
|
||||
@@ -147,6 +305,56 @@ const setupAsync = async (kind: IntegrationKind) => {
|
||||
};
|
||||
};
|
||||
|
||||
type IntegrationSetup = Awaited<ReturnType<typeof setupAsync>>;
|
||||
|
||||
const dashboardPartialFailureCases = [
|
||||
{
|
||||
kind: "tracearr" as const,
|
||||
payloadKey: "dashboard",
|
||||
fallbackValue: null,
|
||||
run: async (setup: IntegrationSetup) =>
|
||||
await tracearrRouter
|
||||
.createCaller({ db: setup.db, deviceType: undefined, session: setup.session })
|
||||
.getDashboard({ integrationIds: setup.integrationIds }),
|
||||
},
|
||||
{
|
||||
kind: "speedtestTracker" as const,
|
||||
payloadKey: "dashboard",
|
||||
fallbackValue: null,
|
||||
run: async (setup: IntegrationSetup) =>
|
||||
await speedtestTrackerRouter
|
||||
.createCaller({ db: setup.db, deviceType: undefined, session: setup.session })
|
||||
.getDashboard({ integrationIds: setup.integrationIds }),
|
||||
},
|
||||
{
|
||||
kind: "peaNut" as const,
|
||||
payloadKey: "summaries",
|
||||
fallbackValue: [],
|
||||
run: async (setup: IntegrationSetup) =>
|
||||
await upsRouter
|
||||
.createCaller({ db: setup.db, deviceType: undefined, session: setup.session })
|
||||
.getSummaries({ integrationIds: setup.integrationIds }),
|
||||
},
|
||||
{
|
||||
kind: "traefik" as const,
|
||||
payloadKey: "dashboard",
|
||||
fallbackValue: null,
|
||||
run: async (setup: IntegrationSetup) =>
|
||||
await traefikRouter
|
||||
.createCaller({ db: setup.db, deviceType: undefined, session: setup.session })
|
||||
.getDashboard({ integrationIds: setup.integrationIds }),
|
||||
},
|
||||
{
|
||||
kind: "uptimeKuma" as const,
|
||||
payloadKey: "dashboard",
|
||||
fallbackValue: null,
|
||||
run: async (setup: IntegrationSetup) =>
|
||||
await uptimeKumaRouter
|
||||
.createCaller({ db: setup.db, deviceType: undefined, session: setup.session })
|
||||
.getDashboard({ integrationIds: setup.integrationIds }),
|
||||
},
|
||||
] as const;
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe("partial integration failures", () => {
|
||||
@@ -187,6 +395,130 @@ describe("partial integration failures", () => {
|
||||
expect(JSON.stringify(streamResults)).not.toContain("secret");
|
||||
});
|
||||
|
||||
test("DNS widgets retain source ownership for partial failures", async () => {
|
||||
const dnsHole = await setupAsync("piHole");
|
||||
const results = await dnsHoleRouter
|
||||
.createCaller({ db: dnsHole.db, deviceType: undefined, session: dnsHole.session })
|
||||
.summary({ integrationIds: dnsHole.integrationIds });
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrationId: dnsHole.onlineId,
|
||||
summary: expect.objectContaining({ status: "enabled" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
integrationId: dnsHole.offlineId,
|
||||
integrationName: "Offline",
|
||||
summary: null,
|
||||
error: "INTEGRATION_REQUEST_FAILED",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(results)).not.toContain("secret");
|
||||
});
|
||||
|
||||
test("downloads retain source ownership for partial failures", async () => {
|
||||
const downloads = await setupAsync("qBittorrent");
|
||||
const results = await downloadsRouter
|
||||
.createCaller({ db: downloads.db, deviceType: undefined, session: downloads.session })
|
||||
.getJobsAndStatuses({ integrationIds: downloads.integrationIds, limitPerIntegration: 50 });
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ integrationId: downloads.onlineId, data: expect.objectContaining({ items: [] }) }),
|
||||
expect.objectContaining({
|
||||
integrationId: downloads.offlineId,
|
||||
integrationName: "Offline",
|
||||
data: null,
|
||||
error: "INTEGRATION_REQUEST_FAILED",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(results)).not.toContain("secret");
|
||||
});
|
||||
|
||||
test("system health retains source ownership for partial failures", async () => {
|
||||
const health = await setupAsync("glances");
|
||||
const results = await healthMonitoringRouter
|
||||
.createCaller({ db: health.db, deviceType: undefined, session: health.session })
|
||||
.getSystemHealthStatus({ integrationIds: health.integrationIds });
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrationId: health.onlineId,
|
||||
healthInfo: expect.objectContaining({ version: "1.0" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
integrationId: health.offlineId,
|
||||
integrationName: "Offline",
|
||||
healthInfo: null,
|
||||
error: "INTEGRATION_REQUEST_FAILED",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(results)).not.toContain("secret");
|
||||
});
|
||||
|
||||
test("network controllers retain source ownership for partial failures", async () => {
|
||||
const network = await setupAsync("unifiController");
|
||||
const results = await networkControllerRouter
|
||||
.createCaller({ db: network.db, deviceType: undefined, session: network.session })
|
||||
.summary({ integrationIds: network.integrationIds });
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrationId: network.onlineId,
|
||||
summary: expect.objectContaining({ wanStatus: "enabled" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
integrationId: network.offlineId,
|
||||
integrationName: "Offline",
|
||||
summary: null,
|
||||
error: "INTEGRATION_REQUEST_FAILED",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(results)).not.toContain("secret");
|
||||
});
|
||||
|
||||
test("dashboard widgets preserve healthy sources and expose sanitized failed-source envelopes", async () => {
|
||||
for (const testCase of dashboardPartialFailureCases) {
|
||||
const setup = await setupAsync(testCase.kind);
|
||||
const results = await testCase.run(setup);
|
||||
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
integrationId: setup.onlineId,
|
||||
integrationName: "Online",
|
||||
[testCase.payloadKey]: expect.anything(),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
integrationId: setup.offlineId,
|
||||
integrationName: "Offline",
|
||||
[testCase.payloadKey]: testCase.fallbackValue,
|
||||
error: "INTEGRATION_REQUEST_FAILED",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(results)).not.toMatch(/internal\.example|token=secret|\/private\/|response body|password/i);
|
||||
}
|
||||
});
|
||||
|
||||
test("dashboard widgets reject total integration failure instead of returning fallback-only data", async () => {
|
||||
for (const testCase of dashboardPartialFailureCases) {
|
||||
const setup = await setupAsync(testCase.kind, { allOffline: true });
|
||||
|
||||
await expect(testCase.run(setup)).rejects.toMatchObject({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "All integration queries failed",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("media request responses expose failed integrations without dropping successful data", async () => {
|
||||
const mediaRequests = await setupAsync("overseerr");
|
||||
const caller = mediaRequestsRouter.createCaller({
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type { MediaRelease } from "@homarr/integrations/types";
|
||||
|
||||
import { PUBLIC_INTEGRATION_ERROR } from "../../../settle-integrations";
|
||||
import { getMediaReleaseResponse } from "../media-release-result";
|
||||
|
||||
const integrations = [
|
||||
{ id: "plex", name: "Living room Plex", kind: "plex" as const },
|
||||
{ id: "jellyfin", name: "Family Jellyfin", kind: "jellyfin" as const },
|
||||
];
|
||||
|
||||
const release: MediaRelease = {
|
||||
id: "shared-id",
|
||||
type: "movie",
|
||||
title: "Example release",
|
||||
releaseDate: new Date("2026-08-13T10:00:00Z"),
|
||||
imageUrls: { poster: undefined, backdrop: undefined },
|
||||
tags: [],
|
||||
href: "https://example.com/release",
|
||||
};
|
||||
|
||||
describe("getMediaReleaseResponse", () => {
|
||||
test("keeps healthy releases and returns a sanitized failure envelope", async () => {
|
||||
const response = await getMediaReleaseResponse(integrations, async (integration) => {
|
||||
if (integration.id === "plex") throw new Error("token=secret at plex.internal");
|
||||
return { releases: [release], updatedAt: new Date("2026-08-13T11:00:00Z") };
|
||||
});
|
||||
|
||||
expect(response.releases).toEqual([
|
||||
{
|
||||
...release,
|
||||
integration: {
|
||||
...integrations[1],
|
||||
updatedAt: new Date("2026-08-13T11:00:00Z"),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(response.failedIntegrations).toEqual([
|
||||
{
|
||||
integrationId: "plex",
|
||||
integrationName: "Living room Plex",
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(response)).not.toMatch(/secret|plex\.internal/i);
|
||||
});
|
||||
|
||||
test("keeps source identity when integrations return the same release id", async () => {
|
||||
const response = await getMediaReleaseResponse(integrations, async () => ({
|
||||
releases: [release],
|
||||
updatedAt: new Date("2026-08-13T11:00:00Z"),
|
||||
}));
|
||||
|
||||
expect(response.releases.map(({ integration }) => integration.id)).toEqual(["plex", "jellyfin"]);
|
||||
});
|
||||
|
||||
test("throws when every integration fails", async () => {
|
||||
await expect(
|
||||
getMediaReleaseResponse(integrations, async () => {
|
||||
throw new Error("Unavailable");
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "All integration queries failed",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,48 @@
|
||||
import type { CoolifyInstanceInfo } from "@homarr/integrations/types";
|
||||
import { coolifyRequestHandler } from "@homarr/request-handler/coolify";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
interface CoolifyInstanceResult {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
integrationUrl: string;
|
||||
instanceInfo: CoolifyInstanceInfo | null;
|
||||
updatedAt: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const coolifyRouter = createTRPCRouter({
|
||||
getInstancesInfo: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "coolify"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = coolifyRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration): Promise<CoolifyInstanceResult> => {
|
||||
const innerHandler = coolifyRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
instanceInfo: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
instanceInfo: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error): CoolifyInstanceResult => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
instanceInfo: null,
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,13 +1,28 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
|
||||
|
||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
interface DnsHoleSummaryResult {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: IntegrationKindByCategory<"dnsHole">;
|
||||
updatedAt?: Date;
|
||||
};
|
||||
summary: DnsHoleSummary | null;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const dnsHoleRouter = createTRPCRouter({
|
||||
summary: publicProcedure
|
||||
.meta({
|
||||
@@ -19,13 +34,28 @@ export const dnsHoleRouter = createTRPCRouter({
|
||||
})
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("dnsHole")))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { data, timestamp } = await dnsHoleRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
|
||||
summary: data,
|
||||
};
|
||||
});
|
||||
return await settleIntegrationQueries<(typeof ctx.integrations)[number], DnsHoleSummaryResult>(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { data, timestamp } = await dnsHoleRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
|
||||
summary: data,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
summary: null,
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
|
||||
enable: protectedProcedure
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
||||
import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations";
|
||||
import { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
|
||||
|
||||
import type { IntegrationAction } from "../../middlewares/integration";
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
const createDownloadClientIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
createManyIntegrationMiddleware(action, ...getIntegrationKindsByCategory("downloadClient"));
|
||||
|
||||
interface DownloadClientQueryResultBase {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
}
|
||||
|
||||
type DownloadClientQueryResult = DownloadClientQueryResultBase &
|
||||
(
|
||||
| {
|
||||
integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: IntegrationKindByCategory<"downloadClient">;
|
||||
updatedAt: Date;
|
||||
};
|
||||
data: DownloadClientJobsAndStatus;
|
||||
error?: never;
|
||||
}
|
||||
| {
|
||||
integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: IntegrationKindByCategory<"downloadClient">;
|
||||
};
|
||||
data: null;
|
||||
error: string;
|
||||
}
|
||||
);
|
||||
|
||||
export const downloadsRouter = createTRPCRouter({
|
||||
getJobsAndStatuses: publicProcedure
|
||||
.meta({
|
||||
@@ -24,14 +54,29 @@ export const downloadsRouter = createTRPCRouter({
|
||||
.concat(createDownloadClientIntegrationMiddleware("query"))
|
||||
.input(z.object({ limitPerIntegration: z.number().default(50) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
|
||||
data,
|
||||
};
|
||||
});
|
||||
return await settleIntegrationQueries<(typeof ctx.integrations)[number], DownloadClientQueryResult>(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = downloadClientRequestHandler.handler(integration, { limit: input.limitPerIntegration });
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: timestamp },
|
||||
data,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data: null,
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
pause: protectedProcedure
|
||||
.meta({
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { SystemHealthMonitoring } from "@homarr/integrations";
|
||||
import { SynologyIntegration } from "@homarr/integrations";
|
||||
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
|
||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const healthMonitoringIntegrationKinds = [
|
||||
@@ -16,6 +17,14 @@ const healthMonitoringIntegrationKinds = [
|
||||
"mock",
|
||||
] as const;
|
||||
|
||||
interface SystemHealthQueryResult {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
healthInfo: SystemHealthMonitoring | null;
|
||||
updatedAt?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const healthMonitoringRouter = createTRPCRouter({
|
||||
getSystemHealthStatus: publicProcedure
|
||||
.meta({
|
||||
@@ -27,15 +36,27 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
})
|
||||
.concat(createManyIntegrationMiddleware("query", ...healthMonitoringIntegrationKinds))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { data, timestamp } = await systemInfoRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
healthInfo: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return await settleIntegrationQueries<(typeof ctx.integrations)[number], SystemHealthQueryResult>(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { data, timestamp } = await systemInfoRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
healthInfo: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
healthInfo: null,
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
listStorageVolumes: publicProcedure
|
||||
.meta({
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import type { MediaRelease } from "@homarr/integrations/types";
|
||||
|
||||
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
|
||||
|
||||
interface MediaReleaseSource {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: IntegrationKindByCategory<"mediaRelease">;
|
||||
}
|
||||
|
||||
interface MediaReleaseLoadResult {
|
||||
releases: MediaRelease[];
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
interface MediaReleaseIntegrationResult {
|
||||
integration: MediaReleaseSource & { updatedAt?: Date };
|
||||
releases: MediaRelease[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function getMediaReleaseResponse<TIntegration extends MediaReleaseSource>(
|
||||
integrations: TIntegration[],
|
||||
load: (integration: TIntegration) => Promise<MediaReleaseLoadResult>,
|
||||
) {
|
||||
const results = await settleIntegrationQueries<TIntegration, MediaReleaseIntegrationResult>(
|
||||
integrations,
|
||||
async (integration) => {
|
||||
const { releases, updatedAt } = await load(integration);
|
||||
return {
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
updatedAt,
|
||||
},
|
||||
releases,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration) => ({
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
releases: [],
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
releases: results.flatMap(({ integration, releases }) => releases.map((release) => ({ ...release, integration }))),
|
||||
failedIntegrations: results.flatMap(({ integration, error }) =>
|
||||
error ? [{ integrationId: integration.id, integrationName: integration.name, error }] : [],
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -2,32 +2,21 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { mediaReleaseRequestHandler } from "@homarr/request-handler/media-release";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
import { getMediaReleaseResponse } from "./media-release-result";
|
||||
|
||||
export const mediaReleaseRouter = createTRPCRouter({
|
||||
getMediaReleases: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRelease")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
return await getMediaReleaseResponse(ctx.integrations, async (integration) => {
|
||||
const innerHandler = mediaReleaseRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
updatedAt: timestamp,
|
||||
},
|
||||
releases: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return results.flatMap((result) =>
|
||||
result.releases.map((release) => ({
|
||||
...release,
|
||||
integration: result.integration,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,27 +1,53 @@
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { NetworkControllerSummary } from "@homarr/integrations/types";
|
||||
import { networkControllerRequestHandler } from "@homarr/request-handler/network-controller";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
interface NetworkControllerQueryResult {
|
||||
integrationId: string;
|
||||
integrationName: string;
|
||||
integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: IntegrationKindByCategory<"networkController">;
|
||||
};
|
||||
summary: NetworkControllerSummary | null;
|
||||
updatedAt?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const networkControllerRouter = createTRPCRouter({
|
||||
summary: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("networkController")))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = networkControllerRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries<(typeof ctx.integrations)[number], NetworkControllerQueryResult>(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = networkControllerRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
summary: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
summary: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
summary: null,
|
||||
error: PUBLIC_INTEGRATION_ERROR,
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,24 +1,39 @@
|
||||
import { speedtestTrackerRequestHandler } from "@homarr/request-handler/speedtest-tracker";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const speedtestTrackerRouter = createTRPCRouter({
|
||||
getDashboard: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "speedtestTracker"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = speedtestTrackerRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = speedtestTrackerRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: null,
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,22 +1,37 @@
|
||||
import { tracearrRequestHandler } from "@homarr/request-handler/tracearr";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const tracearrRouter = createTRPCRouter({
|
||||
getDashboard: publicProcedure.concat(createManyIntegrationMiddleware("query", "tracearr")).query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = tracearrRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = tracearrRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: null,
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { traefikRequestHandler } from "@homarr/request-handler/traefik";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const traefikRouter = createTRPCRouter({
|
||||
@@ -15,17 +15,32 @@ export const traefikRouter = createTRPCRouter({
|
||||
})
|
||||
.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 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,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: null,
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -2,24 +2,39 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { upsSummariesRequestHandler } from "@homarr/request-handler/ups";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const upsRouter = createTRPCRouter({
|
||||
getSummaries: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("ups")))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = upsSummariesRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = upsSummariesRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
summaries: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
summaries: data,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
summaries: [],
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,24 +1,39 @@
|
||||
import { uptimeKumaRequestHandler } from "@homarr/request-handler/uptime-kuma";
|
||||
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const uptimeKumaRouter = createTRPCRouter({
|
||||
getDashboard: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "uptimeKuma"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const innerHandler = uptimeKumaRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const innerHandler = uptimeKumaRequestHandler.handler(integration, {});
|
||||
const { data, timestamp } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
});
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: data as typeof data | null,
|
||||
updatedAt: timestamp,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationUrl: integration.url,
|
||||
dashboard: null,
|
||||
updatedAt: new Date(0),
|
||||
error: toPublicIntegrationError(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user