feat: modernize widget experience

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:48 +02:00
1 parent 6331c88911
commit 1fa345edd1
165 files changed
+3698 -1649

No files matched your search

@@ -1,3 +1,5 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { readBoundedJsonResponseAsync, validateCustomApiUrl } from "../custom-api-security";
+10 -15
View File
@@ -10,7 +10,7 @@ import {
beszelSystemsRequestHandler,
} from "@homarr/request-handler/beszel";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
import { BoundedAsyncQueue } from "./bounded-async-queue";
@@ -18,8 +18,6 @@ import { BoundedAsyncQueue } from "./bounded-async-queue";
const logger = createLogger({ module: "beszelRouter" });
const MAX_PENDING_LIVE_EVENTS = 4;
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
export const beszelRouter = createTRPCRouter({
getSystems: publicProcedure
.meta({
@@ -41,7 +39,6 @@ export const beszelRouter = createTRPCRouter({
return {
integrationId: integration.id,
integrationName: integration.name,
integrationUrl: integration.url,
systems: data,
updatedAt: timestamp,
};
@@ -50,10 +47,9 @@ export const beszelRouter = createTRPCRouter({
fallback: (integration, error) => ({
integrationId: integration.id,
integrationName: integration.name,
integrationUrl: integration.url,
systems: [],
updatedAt: new Date(0),
error: errorMessage(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
@@ -127,7 +123,7 @@ export const beszelRouter = createTRPCRouter({
history: [],
systemNameMap: {},
updatedAt: new Date(0),
error: errorMessage(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
@@ -194,7 +190,7 @@ export const beszelRouter = createTRPCRouter({
systemStats: [],
containerStats: [],
updatedAt: new Date(0),
error: errorMessage(error),
error: toPublicIntegrationError(error),
};
}
}),
@@ -271,15 +267,14 @@ export const beszelRouter = createTRPCRouter({
integrationId: integration.id,
systemId: input.systemId,
emittedEventCount,
error: errorMessage(error),
error,
});
queue.fail(
error instanceof TRPCError
? error
: new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: error instanceof Error ? error.message : String(error),
}),
new TRPCError({
code: "BAD_GATEWAY",
message: "Live integration request failed",
cause: error,
}),
);
}
})();
+2 -2
View File
@@ -5,7 +5,7 @@ import { radarrReleaseTypes } from "@homarr/integrations/types";
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { PUBLIC_INTEGRATION_ERROR, settleIntegrationQueries } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const calendarRouter = createTRPCRouter({
@@ -52,7 +52,7 @@ export const calendarRouter = createTRPCRouter({
name: integration.name,
kind: integration.kind,
},
error: "Integration request failed",
error: PUBLIC_INTEGRATION_ERROR,
}),
throwOnAllFailures: true,
},
@@ -1,6 +1,7 @@
import { readBoundedJsonResponseAsync as readBoundedJsonResponse } from "@homarr/common/server";
export const MAX_CUSTOM_API_RESPONSE_BYTES = 2 * 1024 * 1024;
export const CUSTOM_API_FETCH_TIMEOUT_MS = 10_000;
export function validateCustomApiUrl(urlString: string): URL {
if (urlString.length > 2_048) throw new Error("Custom API URL is too long");
@@ -19,3 +20,48 @@ export async function readBoundedJsonResponseAsync(
): Promise<unknown> {
return await readBoundedJsonResponse(response, maxBytes, "Custom API response");
}
export async function consumeBoundedResponseAsync(
response: Response,
maxBytes = MAX_CUSTOM_API_RESPONSE_BYTES,
): Promise<void> {
const declaredLength = Number(response.headers.get("content-length"));
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
await response.body?.cancel();
throw new Error(`Custom API response exceeds ${maxBytes} bytes`);
}
if (!response.body) return;
const reader = response.body.getReader();
let byteLength = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) return;
byteLength += value?.byteLength ?? 0;
if (byteLength > maxBytes) {
await reader.cancel();
throw new Error(`Custom API response exceeds ${maxBytes} bytes`);
}
}
}
export async function withCustomApiResponseAsync<TResult>(
url: URL,
init: Omit<RequestInit, "redirect" | "signal">,
consumeResponseAsync: (response: Response) => Promise<TResult>,
timeoutMs = CUSTOM_API_FETCH_TIMEOUT_MS,
): Promise<TResult> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url.toString(), {
...init,
redirect: "error",
signal: controller.signal,
});
return await consumeResponseAsync(response);
} finally {
clearTimeout(timeout);
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import {
} from "@homarr/request-handler/firewall";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const firewallMiddleware = createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall"));
@@ -35,7 +35,7 @@ const queryFirewall = <
fallback: (integration, error) => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind, updatedAt: new Date(0) },
summary: createFallback(),
error: error instanceof Error ? error.message : "Integration request failed",
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
@@ -6,7 +6,7 @@ import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-ma
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
const createIndexerManagerIntegrationMiddleware = (action: IntegrationAction) =>
@@ -34,7 +34,7 @@ export const indexerManagerRouter = createTRPCRouter({
integrationId: integration.id,
integrationName: integration.name,
indexers: [],
error: error instanceof Error ? error.message : String(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
@@ -4,7 +4,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { mediaOrganizerRequestHandler } from "@homarr/request-handler/media-organizer";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const mediaOrganizerRouter = createTRPCRouter({
@@ -39,7 +39,7 @@ export const mediaOrganizerRouter = createTRPCRouter({
missingCount: 0,
queued: [],
queuedCount: 0,
error: error instanceof Error ? error.message : String(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
@@ -12,7 +12,7 @@ import {
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
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 IntegrationQueryResult<TData> {
@@ -47,7 +47,7 @@ export const mediaRequestsRouter = createTRPCRouter({
fallback: (integration) => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind },
data: [],
error: "Integration request failed",
error: PUBLIC_INTEGRATION_ERROR,
}),
throwOnAllFailures: true,
},
@@ -104,7 +104,7 @@ export const mediaRequestsRouter = createTRPCRouter({
fallback: (integration) => ({
integration: { id: integration.id, name: integration.name, kind: integration.kind },
data: { stats: null, users: [] },
error: "Integration request failed",
error: PUBLIC_INTEGRATION_ERROR,
}),
throwOnAllFailures: true,
},
@@ -5,7 +5,7 @@ import { mediaServerRequestHandler } from "@homarr/request-handler/media-server"
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, publicProcedure } from "../../trpc";
const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
@@ -43,7 +43,7 @@ export const mediaServerRouter = createTRPCRouter({
integrationName: integration.name,
integrationKind: integration.kind,
sessions: [],
error: "Integration request failed",
error: PUBLIC_INTEGRATION_ERROR,
}),
throwOnAllFailures: true,
},
@@ -3,7 +3,7 @@ import { notificationsRequestHandler } from "@homarr/request-handler/notificatio
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const createNotificationsIntegrationMiddleware = (action: IntegrationAction) =>
@@ -39,7 +39,7 @@ export const notificationsRouter = createTRPCRouter({
updatedAt: new Date(0),
},
data: [],
error: error instanceof Error ? error.message : String(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},
+22 -2
View File
@@ -13,6 +13,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
import { throwIfActionForbiddenAsync } from "../board/board-access";
const logger = createLogger({ module: "releasesRouter" });
const PUBLIC_RELEASE_ERROR_MESSAGE: string = "Release provider request failed";
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined;
@@ -100,19 +101,38 @@ export const releasesRouter = createTRPCRouter({
})
.getDataAsync();
const data = response.data.success
? response.data
: {
success: false as const,
error: { code: response.data.error.code, message: PUBLIC_RELEASE_ERROR_MESSAGE },
};
if (!response.data.success) {
logger.warn("Release provider request failed", {
repositoryId,
provider: repository.provider,
error: response.data.error,
});
}
return {
id: repositoryId,
provider: repository.provider,
timestamp: response.timestamp,
...response.data,
...data,
};
} catch (error) {
logger.warn("Release provider request failed", {
repositoryId,
provider: repository.provider,
error,
});
return {
id: repositoryId,
provider: repository.provider,
timestamp: new Date(),
success: false as const,
error: { code: "unexpected" as const, message: String(error) },
error: { code: "unexpected" as const, message: PUBLIC_RELEASE_ERROR_MESSAGE },
};
}
}),
+2 -2
View File
@@ -2,7 +2,7 @@ import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { vpnSummaryHandler } from "@homarr/request-handler/vpn";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { settleIntegrationQueries } from "../../settle-integrations";
import { settleIntegrationQueries, toPublicIntegrationError } from "../../settle-integrations";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const vpnRouter = createTRPCRouter({
@@ -33,7 +33,7 @@ export const vpnRouter = createTRPCRouter({
updatedAt: new Date(0),
},
summary: null,
error: error instanceof Error ? error.message : String(error),
error: toPublicIntegrationError(error),
}),
throwOnAllFailures: true,
},