Compare commits

..
Author SHA1 Message Date
Manuel d09a4f8468 chore: update cron 2026-06-19 22:08:25 +02:00
11 changed files with 21 additions and 89 deletions

No files matched your search

@@ -3,6 +3,7 @@ name: "[Deployment] Automatic Weekly Release"
on:
schedule:
- cron: "0 19 * * 5" # https://crontab.guru/#0_19_*_*_5
timezone: Europe/Zurich
workflow_dispatch:
inputs:
send-notifications:
@@ -27,7 +27,6 @@ import {
integrationSecretKindObject,
} from "@homarr/definitions";
import { createIntegrationAsync } from "@homarr/integrations";
import { invalidateIntegrationCacheAsync } from "@homarr/redis";
import { byIdSchema } from "@homarr/validation/common";
import {
integrationCreateSchema,
@@ -470,10 +469,6 @@ export const integrationRouter = createTRPCRouter({
kind: integration.kind,
url: input.url,
});
// Invalidate all cached data for this integration so that widgets pick up the
// new configuration immediately instead of serving stale (or errored) data.
await invalidateIntegrationCacheAsync(input.id);
}),
delete: protectedProcedure
.meta({
@@ -495,9 +490,6 @@ export const integrationRouter = createTRPCRouter({
}
await ctx.db.delete(integrations).where(eq(integrations.id, input.id));
// Clean up any cached data left behind by the deleted integration.
await invalidateIntegrationCacheAsync(input.id);
}),
getIntegrationPermissions: protectedProcedure.input(byIdSchema).query(async ({ input, ctx }) => {
await throwIfActionForbiddenAsync(ctx, eq(integrations.id, input.id), "full");
-1
View File
@@ -15,7 +15,6 @@ export {
createGetSetChannel,
createQueryCacheChannel,
getAllQueryCacheAsync,
invalidateIntegrationCacheAsync,
} from "./lib/channel";
export { createIntegrationHistoryChannel } from "./lib/channels/history-channel";
-30
View File
@@ -261,36 +261,6 @@ export const createIntegrationOptionsChannel = <TData>(
return createChannelWithLatestAndEvents<TData>(channelName);
};
/**
* Invalidates all cached data for a given integration by deleting every Redis key
* that starts with `integration:${integrationId}:` as well as the integration's
* session store entry. This ensures that stale data from a misconfigured or
* updated integration is never served from cache.
*/
export const invalidateIntegrationCacheAsync = async (integrationId: string): Promise<void> => {
const client = getSetClient as typeof getSetClient | null;
if (!client) {
return;
}
const patterns = [`integration:${integrationId}:*`, `session-store:${integrationId}`];
let deletedCount = 0;
for (const pattern of patterns) {
let cursor = "0";
do {
const [nextCursor, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", 200);
cursor = nextCursor;
if (keys.length > 0) {
await client.del(...keys);
deletedCount += keys.length;
}
} while (cursor !== "0");
}
if (deletedCount > 0) {
logger.info("Invalidated integration cache", { integrationId, deletedKeys: deletedCount });
}
};
export const createWidgetOptionsChannel = <TData>(
widgetKind: WidgetKind,
queryKey: string,
@@ -40,7 +40,7 @@ export default function BeszelAlertsWidget({
() => ({ integrationIds, includeHistory: options.showHistory, maxHistoryItems: options.maxHistoryItems }),
[integrationIds, options.showHistory, options.maxHistoryItems],
);
const { data: results = [], error: alertsError } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput, {
const { data: results = [] } = clientApi.widget.beszel.getAlerts.useQuery(alertsInput, {
staleTime: 15_000,
refetchInterval: 15_000,
retry: false,
@@ -91,8 +91,6 @@ export default function BeszelAlertsWidget({
const triggeredAlerts = alerts.filter((a) => a.triggered);
const okAlerts = alerts.filter((a) => !a.triggered);
if (alertsError) throw alertsError;
return (
<ScrollArea h="100%" style={{ pointerEvents: isEditMode ? "none" : undefined }}>
<Stack gap="sm" p="sm">
@@ -1,7 +1,7 @@
"use client";
import type { MantineSize } from "@mantine/core";
import { Badge, Box, Card, Group, Progress, Text, Stack } from "@mantine/core";
import { Badge, Box, Card, Group, Progress, Text, Stack, useComputedColorScheme } from "@mantine/core";
import {
Activity,
Battery,
@@ -153,6 +153,7 @@ interface SystemCardProps {
t: ReturnType<typeof useScopedI18n<"widget.beszelSystemGrid">>;
size: SizeConfig;
maxMetrics: number;
backgroundColor: string;
itemRadius: MantineSize;
}
@@ -310,14 +311,14 @@ const metricRenderers = [
},
] as const;
const SystemCard = ({ system, options, t, size, maxMetrics, itemRadius }: SystemCardProps) => {
const SystemCard = ({ system, options, t, size, maxMetrics, backgroundColor, itemRadius }: SystemCardProps) => {
const visibleMetrics = metricRenderers.filter((m) => m.visible(system, options)).slice(0, maxMetrics);
return (
<Card
padding={size.cardPadding}
radius={itemRadius}
bg="transparent"
bg={backgroundColor}
h="100%"
style={{
overflow: "hidden",
@@ -353,8 +354,12 @@ export default function BeszelSystemGridWidget({
}: WidgetComponentProps<"beszelSystemGrid">) {
const t = useScopedI18n("widget.beszelSystemGrid");
const board = useRequiredBoard();
const colorScheme = useComputedColorScheme("light");
const opacity = board.opacity / 100;
const bgBase: Record<"light" | "dark", string> = { dark: "57, 57, 57", light: "246, 247, 248" };
const backgroundColor = `rgba(${bgBase[colorScheme]}, ${opacity})`;
const { data: results = [], error: systemsError } = clientApi.widget.beszel.getSystems.useQuery(
const { data: results = [] } = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 5_000, refetchInterval: 5_000, retry: false },
);
@@ -362,8 +367,6 @@ export default function BeszelSystemGridWidget({
useBeszelSystemsSubscription(integrationIds, !isEditMode);
const filteredSystems = useBeszelFilteredSystems(results, options.statusFilter);
if (systemsError) throw systemsError;
const cols = getColCount(width, filteredSystems.length);
const rows = Math.ceil(filteredSystems.length / cols) || 1;
const rawCellHeight = height / rows;
@@ -393,6 +396,7 @@ export default function BeszelSystemGridWidget({
t={t}
size={size}
maxMetrics={maxMetrics}
backgroundColor={backgroundColor}
itemRadius={board.itemRadius}
/>
))}
@@ -3,12 +3,4 @@
top: 6px;
left: 6px;
z-index: 1;
opacity: 0;
transition: opacity 0.15s ease;
pointer-events: none;
}
.beszelStatsContainer:hover .beszelStatsSystemToggle {
opacity: 1;
pointer-events: auto;
}
@@ -61,11 +61,10 @@ export default function BeszelSystemStatsWidget({
const { data: session } = useSession();
const hasChangeAccess = board ? constructBoardPermissions(board, session).hasChangeAccess : false;
const { mutate: saveItemOptions } = clientApi.widget.options.saveItemOptions.useMutation();
const {
data: systemsResult = [],
isPending: systemsPending,
error: systemsError,
} = clientApi.widget.beszel.getSystems.useQuery({ integrationIds }, { staleTime: 30 * 1000, retry: false });
const { data: systemsResult = [], isPending: systemsPending } = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 30 * 1000 },
);
const systems = useMemo(
() => systemsResult.flatMap((r) => r.systems.map((s) => ({ value: s.id, label: s.name }))),
@@ -90,14 +89,14 @@ export default function BeszelSystemStatsWidget({
const showDocker = options.showDockerCpu || options.showDockerMemory || options.showDockerNetwork;
const isLive = options.timePeriod === "1m";
const { data: statsResult, error: statsError } = clientApi.widget.beszel.getSystemStats.useQuery(
const { data: statsResult } = clientApi.widget.beszel.getSystemStats.useQuery(
{
integrationIds,
systemId: selectedSystem,
timePeriod: options.timePeriod as "1m" | "1h" | "12h" | "24h" | "1w" | "30d",
includeDocker: showDocker,
},
{ staleTime: 30 * 1000, enabled: !isLive && systemReady && selectedSystem !== "", retry: false },
{ staleTime: 30 * 1000, enabled: !isLive && systemReady && selectedSystem !== "" },
);
const { data: liveData, error: liveError } = useLiveStats(integrationIds, selectedSystem, isLive && systemReady);
@@ -186,9 +185,6 @@ export default function BeszelSystemStatsWidget({
const cols = gridColumns[Number(width > 600)];
if (systemsError) throw systemsError;
if (!isLive && statsError) throw statsError;
if (!systemsPending && !systemExists && systems.length > 0) {
return (
<Center h="100%">
@@ -226,11 +222,7 @@ export default function BeszelSystemStatsWidget({
}
return (
<ScrollArea
h="100%"
className={classes.beszelStatsContainer}
style={{ pointerEvents: (isEditMode && "none") || undefined }}
>
<ScrollArea h="100%" style={{ pointerEvents: (isEditMode && "none") || undefined }}>
<Stack gap="md" p="sm">
{!isEditMode && systems.length > 1 && (
<Menu position="bottom-start" withArrow shadow="md" withinPortal>
@@ -66,7 +66,7 @@ export default function BeszelSystemTableWidget({
width,
}: WidgetComponentProps<"beszelSystemTable">) {
const t = useScopedI18n("widget.beszelSystemTable");
const { data: results = [], error: systemsError } = clientApi.widget.beszel.getSystems.useQuery(
const { data: results = [] } = clientApi.widget.beszel.getSystems.useQuery(
{ integrationIds },
{ staleTime: 5_000, refetchInterval: 5_000, retry: false },
);
@@ -255,8 +255,6 @@ export default function BeszelSystemTableWidget({
return cols.filter(Boolean) as DataTableColumn<SystemRowWithKey>[];
}, [options, t, size]);
if (systemsError) throw systemsError;
return (
<DataTable
style={{ pointerEvents: isEditMode ? "none" : undefined }}
@@ -1,17 +1,3 @@
/* Make the DataTable transparent so the board card's opacity-controlled
background shows through. The mantine-datatable defaults to
var(--mantine-color-body) which is opaque. */
.beszel-table,
.beszel-table .mantine-datatable-table,
.beszel-table .mantine-datatable-table thead,
.beszel-table .mantine-datatable-table tbody,
.beszel-table .mantine-datatable-table tfoot,
.beszel-table .mantine-datatable-table tr,
.beszel-table th,
.beszel-table td {
background-color: transparent !important;
}
.beszel-table th {
white-space: nowrap;
}
@@ -19,7 +19,7 @@ function prepareRecords<T>(records: T[], live: boolean) {
}
const yAxisBase = { tickMargin: 0, tick: { fontSize: 10 } } as const;
const chartStyle = { minWidth: 0, minHeight: 1 } as const;
const chartStyle = { minWidth: 0, minHeight: 0 } as const;
const panelStyle = { minWidth: 0, overflow: "hidden" } as const;
export const CPU_Y_AXIS_DOMAIN: [number, string] = [0, "auto"];