chore(release): automatic release v1.26.0

This commit is contained in:
homarr-releases[bot]
2025-06-27 19:14:04 +00:00
committed by GitHub
84 changed files with 2570 additions and 1573 deletions

View File

@@ -31,6 +31,7 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.25.0
- 1.24.0
- 1.23.0
- 1.22.0

View File

@@ -1,14 +0,0 @@
# https://github.com/webiny/action-conventional-commits?tab=readme-ov-file
name: "[Conventions] Semantic Commits"
on:
pull_request:
branches: [ dev ]
jobs:
build:
name: Conventional Commits
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: webiny/action-conventional-commits@v1.3.0

2
.nvmrc
View File

@@ -1 +1 @@
22.16.0
22.17.0

View File

@@ -48,28 +48,28 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^8.1.1",
"@mantine/core": "^8.1.1",
"@mantine/dropzone": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/modals": "^8.1.1",
"@mantine/tiptap": "^8.1.1",
"@mantine/colors-generator": "^8.1.2",
"@mantine/core": "^8.1.2",
"@mantine/dropzone": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"@mantine/modals": "^8.1.2",
"@mantine/tiptap": "^8.1.2",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.0",
"@tanstack/react-query": "^5.80.10",
"@tanstack/react-query-devtools": "^5.80.10",
"@tanstack/react-query-next-experimental": "^5.80.10",
"@trpc/client": "^11.4.2",
"@trpc/next": "^11.4.2",
"@trpc/react-query": "^11.4.2",
"@trpc/server": "^11.4.2",
"@tanstack/react-query": "^5.81.4",
"@tanstack/react-query-devtools": "^5.81.4",
"@tanstack/react-query-next-experimental": "^5.81.4",
"@trpc/client": "^11.4.3",
"@trpc/next": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"chroma-js": "^3.1.2",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"dotenv": "^16.5.0",
"dotenv": "^16.6.0",
"flag-icons": "^7.5.0",
"glob": "^11.0.3",
"jotai": "^2.12.5",
@@ -83,7 +83,7 @@
"react-simple-code-editor": "^0.14.1",
"sass": "^1.89.2",
"superjson": "2.2.2",
"swagger-ui-react": "^5.25.2",
"swagger-ui-react": "^5.25.3",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.67"
},
@@ -92,15 +92,15 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2",
"concurrently": "^9.2.0",
"eslint": "^9.29.0",
"node-loader": "^2.1.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
}
}

View File

@@ -1,4 +1,4 @@
import { IconGrid3x3, IconKey, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
import { IconGrid3x3, IconKey, IconMessage, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
import type { IntegrationSecretKind } from "@homarr/definitions";
import type { TablerIcon } from "@homarr/ui";
@@ -9,4 +9,5 @@ export const integrationSecretIcons = {
password: IconPassword,
realm: IconServer,
tokenId: IconGrid3x3,
topic: IconMessage,
} satisfies Record<IntegrationSecretKind, TablerIcon>;

View File

@@ -36,19 +36,19 @@
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"dayjs": "^1.11.13",
"dotenv": "^16.5.0",
"dotenv": "^16.6.0",
"superjson": "2.2.2",
"undici": "7.10.0"
"undici": "7.11.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.32",
"@types/node": "^22.15.33",
"dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5",
"eslint": "^9.29.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"tsx": "4.20.3",
"typescript": "^5.8.3"
}

View File

@@ -25,7 +25,7 @@
"@homarr/log": "workspace:^",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0",
"dotenv": "^16.6.0",
"tsx": "4.20.3",
"ws": "^8.18.2"
},
@@ -36,7 +36,7 @@
"@types/ws": "^8.18.1",
"esbuild": "^0.25.5",
"eslint": "^9.29.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
}
}

View File

@@ -36,16 +36,16 @@
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.1",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.4",
"@vitejs/plugin-react": "^4.5.2",
"@vitejs/plugin-react": "^4.6.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.0.0",
"cross-env": "^7.0.3",
"jsdom": "^26.1.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"semantic-release": "^24.2.5",
"testcontainers": "^11.0.3",
"turbo": "^2.5.4",
@@ -53,9 +53,9 @@
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.12.1",
"packageManager": "pnpm@10.12.4",
"engines": {
"node": ">=22.16.0"
"node": ">=22.17.0"
},
"pnpm": {
"onlyBuiltDependencies": [
@@ -70,7 +70,7 @@
"tree-sitter-json"
],
"overrides": {
"proxmox-api>undici": "7.10.0"
"proxmox-api>undici": "7.11.0"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [

View File

@@ -41,11 +41,11 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.80.10",
"@trpc/client": "^11.4.2",
"@trpc/react-query": "^11.4.2",
"@trpc/server": "^11.4.2",
"@trpc/tanstack-react-query": "^11.4.2",
"@tanstack/react-query": "^5.81.4",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@trpc/tanstack-react-query": "^11.4.3",
"lodash.clonedeep": "^4.5.0",
"next": "15.3.4",
"react": "19.1.0",
@@ -59,7 +59,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.29.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
}
}

View File

@@ -11,6 +11,7 @@ import { mediaTranscodingRouter } from "./media-transcoding";
import { minecraftRouter } from "./minecraft";
import { networkControllerRouter } from "./network-controller";
import { notebookRouter } from "./notebook";
import { notificationsRouter } from "./notifications";
import { optionsRouter } from "./options";
import { releasesRouter } from "./releases";
import { rssFeedRouter } from "./rssFeed";
@@ -37,4 +38,5 @@ export const widgetRouter = createTRPCRouter({
options: optionsRouter,
releases: releasesRouter,
networkController: networkControllerRouter,
notifications: notificationsRouter,
});

View File

@@ -0,0 +1,64 @@
import { observable } from "@trpc/server/observable";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { Notification } from "@homarr/integrations";
import { notificationsRequestHandler } from "@homarr/request-handler/notifications";
import type { IntegrationAction } from "../../middlewares/integration";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
const createNotificationsIntegrationMiddleware = (action: IntegrationAction) =>
createManyIntegrationMiddleware(action, ...getIntegrationKindsByCategory("notifications"));
export const notificationsRouter = createTRPCRouter({
getNotifications: publicProcedure
.unstable_concat(createNotificationsIntegrationMiddleware("query"))
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = notificationsRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
data,
};
}),
);
}),
subscribeNotifications: publicProcedure
.unstable_concat(createNotificationsIntegrationMiddleware("query"))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"notifications"> }>;
data: Notification[];
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = notificationsRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((data) => {
emit.next({
integration,
data,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
});

View File

@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.39.1",
"@auth/drizzle-adapter": "^1.9.1",
"@auth/core": "^0.40.0",
"@auth/drizzle-adapter": "^1.10.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
@@ -34,9 +34,9 @@
"@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0",
"cookies": "^0.9.1",
"ldapts": "8.0.1",
"ldapts": "8.0.2",
"next": "15.3.4",
"next-auth": "5.0.0-beta.28",
"next-auth": "5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"zod": "^3.25.67"
@@ -48,7 +48,7 @@
"@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.1",
"eslint": "^9.29.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
}
}

View File

@@ -24,7 +24,7 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"undici": "7.10.0"
"undici": "7.11.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -28,7 +28,7 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0"
"dotenv": "^16.6.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -33,7 +33,7 @@
"next": "15.3.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.10.0",
"undici": "7.11.0",
"zod": "^3.25.67",
"zod-validation-error": "^3.5.2"
},

View File

@@ -10,11 +10,11 @@ const calculateTimeAgo = (timestamp: Date) => {
return dayjs().to(timestamp);
};
export const useTimeAgo = (timestamp: Date) => {
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
useEffect(() => {
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), 1000); // update every second
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
return () => clearInterval(intervalId); // clear interval on hook unmount
}, [timestamp]);

View File

@@ -25,6 +25,7 @@ export const cronJobs = {
minecraftServerStatus: { preventManualExecution: false },
networkController: { preventManualExecution: false },
dockerContainers: { preventManualExecution: false },
refreshNotifications: { preventManualExecution: false },
} satisfies Record<JobGroupKeys, { preventManualExecution?: boolean }>;
/**

View File

@@ -11,6 +11,7 @@ import { mediaRequestListJob, mediaRequestStatsJob } from "./jobs/integrations/m
import { mediaServerJob } from "./jobs/integrations/media-server";
import { mediaTranscodingJob } from "./jobs/integrations/media-transcoding";
import { networkControllerJob } from "./jobs/integrations/network-controller";
import { refreshNotificationsJob } from "./jobs/integrations/notifications";
import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
@@ -38,6 +39,7 @@ export const jobGroup = createCronJobGroup({
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
refreshNotifications: refreshNotificationsJob,
});
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];

View File

@@ -0,0 +1,14 @@
import { EVERY_5_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { notificationsRequestHandler } from "@homarr/request-handler/notifications";
import { createCronJob } from "../../lib";
export const refreshNotificationsJob = createCronJob("refreshNotifications", EVERY_5_MINUTES).withCallback(
createRequestIntegrationJobHandler(notificationsRequestHandler.handler, {
widgetKinds: ["notifications"],
getInput: {
notifications: (options) => options,
},
}),
);

View File

@@ -38,18 +38,18 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.39.1",
"@auth/core": "^0.40.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/core": "^8.1.2",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.0.3",
"better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1",
"better-sqlite3": "^12.1.1",
"dotenv": "^16.6.0",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.2",
"drizzle-zod": "^0.7.1",
"mysql2": "3.14.1"
@@ -62,7 +62,7 @@
"dotenv-cli": "^8.0.0",
"esbuild": "^0.25.5",
"eslint": "^9.29.0",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"tsx": "4.20.3",
"typescript": "^5.8.3"
}

View File

@@ -7,6 +7,7 @@ export const integrationSecretKindObject = {
password: { isPublic: false },
tokenId: { isPublic: true },
realm: { isPublic: true },
topic: { isPublic: true },
} satisfies Record<string, { isPublic: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
@@ -169,6 +170,12 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
},
ntfy: {
name: "ntfy",
secretKinds: [["topic"], ["topic", "apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
category: ["notifications"],
},
} as const satisfies Record<string, integrationDefinition>;
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
@@ -223,4 +230,5 @@ export type IntegrationCategory =
| "healthMonitoring"
| "search"
| "mediaTranscoding"
| "networkController";
| "networkController"
| "notifications";

View File

@@ -25,5 +25,6 @@ export const widgetKinds = [
"healthMonitoring",
"releases",
"dockerContainers",
"notifications",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];

View File

@@ -26,7 +26,7 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.1.1",
"@mantine/form": "^8.1.2",
"zod": "^3.25.67"
},
"devDependencies": {

View File

@@ -29,7 +29,7 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/core": "^8.1.2",
"react": "19.1.0",
"zod": "^3.25.67"
},

View File

@@ -38,11 +38,11 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.11.0",
"maria2": "^0.4.0",
"maria2": "^0.4.1",
"node-ical": "^0.20.1",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.5",
"undici": "7.10.0",
"undici": "7.11.0",
"xml2js": "^0.6.2",
"zod": "^3.25.67"
},

View File

@@ -21,6 +21,7 @@ import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integrati
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
import { NTFYIntegration } from "../ntfy/ntfy-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
@@ -92,6 +93,7 @@ export const integrationCreators = {
emby: EmbyIntegration,
nextcloud: NextcloudIntegration,
unifiController: UnifiControllerIntegration,
ntfy: NTFYIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {

View File

@@ -1,26 +1,27 @@
// General integrations
export { AdGuardHomeIntegration } from "./adguard-home/adguard-home-integration";
export { Aria2Integration } from "./download-client/aria2/aria2-integration";
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
export { Aria2Integration } from "./download-client/aria2/aria2-integration";
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
export { NTFYIntegration } from "./ntfy/ntfy-integration";
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration";
export { NextcloudIntegration } from "./nextcloud/nextcloud.integration";
// Types
export type { IntegrationInput } from "./base/integration";
@@ -34,6 +35,7 @@ export type { StreamSession } from "./interfaces/media-server/session";
export type { TdarrQueue } from "./interfaces/media-transcoding/queue";
export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics";
export type { TdarrWorker } from "./interfaces/media-transcoding/workers";
export type { Notification } from "./interfaces/notifications/notification";
// Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";

View File

@@ -43,6 +43,7 @@ export interface MediaRequestStats {
users: RequestUser[];
}
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
export enum MediaRequestStatus {
PendingApproval = 1,
Approved = 2,
@@ -51,6 +52,7 @@ export enum MediaRequestStatus {
Completed = 5,
}
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
export enum MediaAvailability {
Unknown = 1,
Pending = 2,
@@ -58,4 +60,5 @@ export enum MediaAvailability {
PartiallyAvailable = 4,
Available = 5,
Blacklisted = 6,
Deleted = 7,
}

View File

@@ -0,0 +1,6 @@
export interface Notification {
id: string;
time: Date;
title: string;
body: string;
}

View File

@@ -0,0 +1,6 @@
import { Integration } from "../../base/integration";
import type { Notification } from "./notification";
export abstract class NotificationsIntegration extends Integration {
public abstract getNotificationsAsync(): Promise<Notification[]>;
}

View File

@@ -0,0 +1,65 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { Notification } from "../interfaces/notifications/notification";
import { NotificationsIntegration } from "../interfaces/notifications/notifications-integration";
import { ntfyNotificationSchema } from "./ntfy-schema";
export class NTFYIntegration extends NotificationsIntegration {
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
return { success: true };
}
private getTopicURL() {
return this.url(`/${encodeURIComponent(super.getSecretValue("topic"))}/json`, { poll: 1 });
}
private getHeaders() {
return this.hasSecretValue("apiKey") ? { Authorization: `Bearer ${super.getSecretValue("apiKey")}` } : {};
}
public async getNotificationsAsync() {
const url = this.getTopicURL();
const notifications = await Promise.all(
(
await fetchWithTrustedCertificatesAsync(url, { headers: this.getHeaders() })
.then((response) => {
if (!response.ok) throw new ResponseError(response);
return response.text();
})
.catch((error) => {
if (error instanceof Error) throw error;
else {
throw new Error("Error communicating with ntfy");
}
})
)
// response is provided as individual lines of JSON
.split("\n")
.map(async (line) => {
// ignore empty lines
if (line.length === 0) return null;
const json = JSON.parse(line) as unknown;
const parsed = await ntfyNotificationSchema.parseAsync(json);
if (parsed.event === "message") return parsed;
// ignore non-event messages
else return null;
}),
);
return notifications
.filter((notification) => notification !== null)
.map((notification): Notification => {
const topicURL = this.url(`/${notification.topic}`);
return {
id: notification.id,
time: new Date(notification.time * 1000),
title: notification.title ?? topicURL.hostname + topicURL.pathname,
body: notification.message,
};
});
}
}

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
// There are more properties, see: https://docs.ntfy.sh/subscribe/api/#json-message-format
// Not all properties are required for this use case.
export const ntfyNotificationSchema = z.object({
id: z.string(),
time: z.number(),
event: z.string(), // we only care about "message"
topic: z.string(),
title: z.optional(z.string()),
message: z.string(),
});

View File

@@ -33,7 +33,7 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/core": "^8.1.2",
"@tabler/icons-react": "^3.34.0",
"dayjs": "^1.11.13",
"next": "15.3.4",

View File

@@ -24,8 +24,8 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/core": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"react": "19.1.0"
},
"devDependencies": {

View File

@@ -24,7 +24,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.1.1",
"@mantine/notifications": "^8.1.2",
"@tabler/icons-react": "^3.34.0"
},
"devDependencies": {

View File

@@ -37,8 +37,8 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/core": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"adm-zip": "0.5.16",
"next": "15.3.4",
"react": "19.1.0",

View File

@@ -0,0 +1,20 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import type { Notification } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const notificationsRequestHandler = createCachedIntegrationRequestHandler<
Notification[],
IntegrationKindByCategory<"notifications">,
Record<string, never>
>({
async requestAsync(integration) {
const integrationInstance = await createIntegrationAsync(integration);
return await integrationInstance.getNotificationsAsync();
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "notificationsJobStatus",
});

View File

@@ -26,7 +26,7 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^8.1.1",
"@mantine/dates": "^8.1.2",
"next": "15.3.4",
"react": "19.1.0",
"react-dom": "19.1.0"

View File

@@ -33,9 +33,9 @@
"@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/spotlight": "^8.1.1",
"@mantine/core": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"@mantine/spotlight": "^8.1.2",
"@tabler/icons-react": "^3.34.0",
"jotai": "^2.12.5",
"next": "15.3.4",

View File

@@ -33,7 +33,7 @@
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.4",
"next-intl": "4.1.0",
"next-intl": "4.3.1",
"react": "19.1.0",
"react-dom": "19.1.0"
},

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "领域",
"newLabel": "新领域"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "等待处理",
"processing": "处理中",
"partiallyAvailable": "部分",
"available": "可用"
"available": "可用",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "待处理",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "获取网络控制器概述失败"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "网络控制器"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": "Docker 容器"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Částečně dostupné",
"available": "K dispozici"
"available": "K dispozici",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Realm",
"newLabel": "Nyt realm"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Afventende",
"processing": "Behandler",
"partiallyAvailable": "Delvis",
"available": "Tilgængelig"
"available": "Tilgængelig",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "Afventende",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Kunne ikke hente Netværkskontroloversigt"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "Netværkskontroller"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": "Docker containers"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Bereich",
"newLabel": "Neuer Bereich"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Ausstehend",
"processing": "In Bearbeitung",
"partiallyAvailable": "Teilweise",
"available": "Verfügbar"
"available": "Verfügbar",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "Ausstehend",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Bereich",
"newLabel": "Neuer Bereich"
},
"topic": {
"label": "Thema",
"newLabel": "Neues Thema erstellen"
}
}
},
@@ -1758,7 +1762,7 @@
"label": "Speicher-Info anzeigen"
},
"showUptime": {
"label": ""
"label": "Laufzeit anzeigen"
},
"fileSystem": {
"label": "Dateisystem Info anzeigen"
@@ -1767,7 +1771,7 @@
"label": "Standard Tab"
},
"visibleClusterSections": {
"label": ""
"label": "Sichtbare Cluster-Abschnitte"
},
"sectionIndicatorRequirement": {
"label": "Anforderung der Sektionsindikatoren"
@@ -1839,11 +1843,11 @@
}
},
"dockerContainers": {
"name": "",
"description": "",
"name": "Docker Statistiken",
"description": "Statistiken Ihrer Container (Dieses Widget kann nur mit Administratorrechten hinzugefügt werden)",
"option": {},
"error": {
"internalServerError": ""
"internalServerError": "Fehler beim Abrufen der Container Statistiken"
}
},
"common": {
@@ -1961,8 +1965,8 @@
"label": "Filter zur Berechnung des Verhältnisses verwenden"
},
"limitPerIntegration": {
"label": "",
"description": ""
"label": "Elemente pro Integration begrenzen",
"description": "Dies begrenzt die Anzahl der Elemente pro Integration, jedoch nicht global"
}
},
"errors": {
@@ -2088,7 +2092,9 @@
"pending": "Ausstehend",
"processing": "In Bearbeitung",
"partiallyAvailable": "Teilweise",
"available": "Verfügbar"
"available": "Verfügbar",
"blacklisted": "Gesperrt",
"deleted": "Gelöscht"
},
"status": {
"pending": "Ausstehend",
@@ -2227,13 +2233,13 @@
"label": "Repository hinzufügen"
},
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
"label": "Von Docker importieren",
"loading": "Lade Docker Images",
"noImagesFound": "Keine Docker Images gefunden",
"listFoundImages": "Liste der gefundenen Images",
"listAlreadyImportedImages": "Liste der bereits importierten Images",
"allImagesAlreadyImported": "Alle Images wurden bereits importiert",
"onlyAdminCanImport": "Nur Administratoren können vom Docker importieren"
},
"provider": {
"label": "Anbieter"
@@ -2276,7 +2282,7 @@
}
},
"importForm": {
"title": ""
"title": "Von Docker importieren"
},
"example": {
"label": "Beispiel"
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Fehler beim Abrufen der Netzwerk Controller Zusammenfassung"
}
},
"notifications": {
"name": "Benachrichtigungen",
"description": "Benachrichtigungshistorie von einer Integration anzeigen",
"noItems": "Keine Benachrichtigungen zum Anzeigen.",
"option": {}
}
},
"widgetPreview": {
@@ -3122,8 +3134,11 @@
"networkController": {
"label": "Netzwerk Controller"
},
"refreshNotifications": {
"label": "Benachrichtigungs Updater"
},
"dockerContainers": {
"label": ""
"label": "Docker Container"
}
}
},
@@ -3189,7 +3204,7 @@
"updated": "Aktualisiert {when}",
"search": "{count} Container durchsuchen",
"selected": "{selectCount} von {totalCount} ausgewählten Containern",
"footer": ""
"footer": "Insgesamt {count} Container"
},
"field": {
"name": {
@@ -3209,10 +3224,10 @@
},
"stats": {
"cpu": {
"label": ""
"label": "CPU"
},
"memory": {
"label": ""
"label": "Speicher"
}
},
"containerImage": {
@@ -3223,7 +3238,7 @@
}
},
"action": {
"title": "",
"title": "Aktionen",
"start": {
"label": "Starten",
"notification": {

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Μερικώς",
"available": "Διαθέσιμο"
"available": "Διαθέσιμο",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Realm",
"newLabel": "New realm"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Realm",
"newLabel": "New realm"
},
"topic": {
"label": "Topic",
"newLabel": "New topic"
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Pending",
"processing": "Processing",
"partiallyAvailable": "Partial",
"available": "Available"
"available": "Available",
"blacklisted": "Blacklisted",
"deleted": "Deleted"
},
"status": {
"pending": "Pending",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Failed to fetch Network Controller Summary"
}
},
"notifications": {
"name": "Notifications",
"description": "Display notification history from an integration",
"noItems": "No notifications to display.",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "Network Controller"
},
"refreshNotifications": {
"label": "Notification Updater"
},
"dockerContainers": {
"label": "Docker containers"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Parcial",
"available": "Disponible"
"available": "Disponible",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Domaine",
"newLabel": "Nouveau domaine"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "En attente",
"processing": "Traitement en cours",
"partiallyAvailable": "Partiel",
"available": "Disponible"
"available": "Disponible",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "En attente",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "תחום",
"newLabel": "תחום חדש"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "בהמתנה",
"processing": "מעבד",
"partiallyAvailable": "חלקי",
"available": "זמין"
"available": "זמין",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "ממתין",
@@ -2227,13 +2233,13 @@
"label": "הוסף מאגר"
},
"importRepositories": {
"label": "",
"loading": "",
"noImagesFound": "",
"listFoundImages": "",
"listAlreadyImportedImages": "",
"allImagesAlreadyImported": "",
"onlyAdminCanImport": ""
"label": "ייבוא מדוקר",
"loading": "טוען תמונות דוקר",
"noImagesFound": "לא נמצאו תמונות דוקר",
"listFoundImages": "רשימת התמונות שנמצאו",
"listAlreadyImportedImages": "רשימת התמונות שכבר יובאו",
"allImagesAlreadyImported": "כל התמונות כבר יובאו",
"onlyAdminCanImport": "רק מנהלים יכולים לייבא מדוקר"
},
"provider": {
"label": "ספק"
@@ -2276,7 +2282,7 @@
}
},
"importForm": {
"title": ""
"title": "ייבוא מדוקר"
},
"example": {
"label": "דוגמא"
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "אחזור תקציר בקר הרשת נכשל"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "בקר רשת"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": "מכולות דוקר"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "Feldolgozás",
"partiallyAvailable": "Részleges",
"available": "Elérhető"
"available": "Elérhető",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Parziale",
"available": "Disponibile"
"available": "Disponibile",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

File diff suppressed because it is too large Load Diff

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Dalis",
"available": "Galima"
"available": "Galima",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Daļējs",
"available": "Pieejams"
"available": "Pieejams",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Realm",
"newLabel": "Nieuwe Realm"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "In afwachting",
"processing": "Verwerken",
"partiallyAvailable": "Gedeeltelijk",
"available": "Beschikbaar"
"available": "Beschikbaar",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "In afwachting",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Netwerkcontroller samenvatting ophalen mislukt"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "Netwerkcontroller"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Område",
"newLabel": "Nytt område"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Pågår",
"processing": "Prosesserer",
"partiallyAvailable": "Delvis",
"available": "Tilgjengelig"
"available": "Tilgjengelig",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "Venter",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Oczekujący",
"processing": "Przetwarzanie",
"partiallyAvailable": "",
"available": "Dostępne"
"available": "Dostępne",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Parcial",
"available": "Disponível"
"available": "Disponível",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Parțial",
"available": "Disponibil"
"available": "Disponibil",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Область",
"newLabel": "Новая область"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "В ожидании",
"processing": "Обработка",
"partiallyAvailable": "Частично доступно",
"available": "Доступно"
"available": "Доступно",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "В ожидании",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Ríša",
"newLabel": "Nová ríša"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Čakajúce",
"processing": "Spracovanie",
"partiallyAvailable": "Čiastočný",
"available": "K dispozícii"
"available": "K dispozícii",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "Čakajúce",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Nepodarilo sa načítať súhrn sieťového ovládača"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "Sieťový ovládač"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": "Docker kontajnery"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "",
"available": ""
"available": "",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Delmängd",
"available": "Tillgänglig"
"available": "Tillgänglig",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "Erişim Alanı",
"newLabel": "Yeni erişim alanı"
},
"topic": {
"label": "Konu",
"newLabel": "Yeni konu"
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "Bekleyen",
"processing": "İşlemde",
"partiallyAvailable": "Kısmi",
"available": "Mevcut"
"available": "Mevcut",
"blacklisted": "Engellenenler",
"deleted": "Silinen"
},
"status": {
"pending": "Bekleyen",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "Ağ Denetleyicisi Özeti alınamadı"
}
},
"notifications": {
"name": "Bildirimler",
"description": "Entegrasyonların bildirim geçmişini görüntüle",
"noItems": "Görüntülenecek bildirim yok.",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "Ağ Denetleyicisi"
},
"refreshNotifications": {
"label": "Bildirim Güncelleyici"
},
"dockerContainers": {
"label": "Docker konteynerleri"
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "В очікуванні",
"processing": "В обробці",
"partiallyAvailable": "Частково доступно",
"available": "Доступно"
"available": "Доступно",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "",
"newLabel": ""
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "",
"processing": "",
"partiallyAvailable": "Một phần",
"available": "Khả dụng"
"available": "Khả dụng",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": ""
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": ""
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -936,6 +936,10 @@
"realm": {
"label": "領域",
"newLabel": "新領域"
},
"topic": {
"label": "",
"newLabel": ""
}
}
},
@@ -2088,7 +2092,9 @@
"pending": "等待處理中",
"processing": "處理中",
"partiallyAvailable": "部分",
"available": "待定"
"available": "待定",
"blacklisted": "",
"deleted": ""
},
"status": {
"pending": "待處理",
@@ -2357,6 +2363,12 @@
"error": {
"internalServerError": "無法獲取網路控制總覽"
}
},
"notifications": {
"name": "",
"description": "",
"noItems": "",
"option": {}
}
},
"widgetPreview": {
@@ -3122,6 +3134,9 @@
"networkController": {
"label": "網路控制"
},
"refreshNotifications": {
"label": ""
},
"dockerContainers": {
"label": ""
}

View File

@@ -29,9 +29,9 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.1.1",
"@mantine/dates": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/core": "^8.1.2",
"@mantine/dates": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"@tabler/icons-react": "^3.34.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.3.4",

View File

@@ -48,25 +48,25 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^8.1.1",
"@mantine/core": "^8.1.1",
"@mantine/hooks": "^8.1.1",
"@mantine/charts": "^8.1.2",
"@mantine/core": "^8.1.2",
"@mantine/hooks": "^8.1.2",
"@tabler/icons-react": "^3.34.0",
"@tiptap/extension-color": "2.22.0",
"@tiptap/extension-highlight": "2.22.0",
"@tiptap/extension-image": "2.22.0",
"@tiptap/extension-link": "^2.22.0",
"@tiptap/extension-table": "2.22.0",
"@tiptap/extension-table-cell": "2.22.0",
"@tiptap/extension-table-header": "2.22.0",
"@tiptap/extension-table-row": "2.22.0",
"@tiptap/extension-task-item": "2.22.0",
"@tiptap/extension-task-list": "2.22.0",
"@tiptap/extension-text-align": "2.22.0",
"@tiptap/extension-text-style": "2.22.0",
"@tiptap/extension-underline": "2.22.0",
"@tiptap/react": "^2.22.0",
"@tiptap/starter-kit": "^2.22.0",
"@tiptap/extension-color": "2.23.0",
"@tiptap/extension-highlight": "2.23.0",
"@tiptap/extension-image": "2.23.0",
"@tiptap/extension-link": "^2.23.0",
"@tiptap/extension-table": "2.23.0",
"@tiptap/extension-table-cell": "2.23.0",
"@tiptap/extension-table-header": "2.23.0",
"@tiptap/extension-table-row": "2.23.0",
"@tiptap/extension-task-item": "2.23.0",
"@tiptap/extension-task-list": "2.23.0",
"@tiptap/extension-text-align": "2.23.0",
"@tiptap/extension-text-style": "2.23.0",
"@tiptap/extension-underline": "2.23.0",
"@tiptap/react": "^2.23.0",
"@tiptap/starter-kit": "^2.23.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9",
@@ -74,7 +74,7 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-markdown": "^10.1.0",
"recharts": "^2.15.3",
"recharts": "^2.15.4",
"video.js": "^8.23.3",
"zod": "^3.25.67"
},

View File

@@ -28,6 +28,7 @@ import * as minecraftServerStatus from "./minecraft/server-status";
import * as networkControllerStatus from "./network-controller/network-status";
import * as networkControllerSummary from "./network-controller/summary";
import * as notebook from "./notebook";
import * as notifications from "./notifications";
import type { WidgetOptionDefinition } from "./options";
import * as releases from "./releases";
import * as rssFeed from "./rssFeed";
@@ -67,6 +68,7 @@ export const widgetImports = {
minecraftServerStatus,
dockerContainers,
releases,
notifications,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;

View File

@@ -252,7 +252,11 @@ function getAvailabilityProperties(
return { color: "blue", label: t("availability.processing") };
case MediaAvailability.Pending:
return { color: "violet", label: t("availability.pending") };
case MediaAvailability.Blacklisted:
return { color: "gray", label: t("availability.blacklisted") };
case MediaAvailability.Deleted:
return { color: "red", label: t("availability.deleted") };
default:
return { color: "red", label: t("availability.unknown") };
return { color: "orange", label: t("availability.unknown") };
}
}

View File

@@ -0,0 +1,106 @@
"use client";
import { useMemo } from "react";
import { Card, Flex, Group, ScrollArea, Stack, Text } from "@mantine/core";
import { IconClock } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { useTimeAgo } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
export default function NotificationsWidget({ options, integrationIds }: WidgetComponentProps<"notifications">) {
const [notificationIntegrations] = clientApi.widget.notifications.getNotifications.useSuspenseQuery(
{
...options,
integrationIds,
},
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
},
);
const utils = clientApi.useUtils();
clientApi.widget.notifications.subscribeNotifications.useSubscription(
{
...options,
integrationIds,
},
{
onData: (data) => {
utils.widget.notifications.getNotifications.setData({ ...options, integrationIds }, (prevData) => {
return prevData?.map((item) => {
if (item.integration.id !== data.integration.id) return item;
return {
data: data.data,
integration: {
...data.integration,
updatedAt: new Date(),
},
};
});
});
},
},
);
const t = useScopedI18n("widget.notifications");
const board = useRequiredBoard();
const sortedNotifications = useMemo(
() =>
notificationIntegrations
.flatMap((integration) => integration.data)
.sort((entryA, entryB) => entryB.time.getTime() - entryA.time.getTime()),
[notificationIntegrations],
);
return (
<ScrollArea className="scroll-area-w100" w="100%" p="sm">
<Stack w={"100%"} gap="sm">
{sortedNotifications.length > 0 ? (
sortedNotifications.map((notification) => (
<Card key={notification.id} withBorder radius={board.itemRadius} w="100%" p="sm">
<Flex gap="sm" direction="column" w="100%">
{notification.title && (
<Text fz="sm" lh="sm" lineClamp={2}>
{notification.title}
</Text>
)}
<Text c="dimmed" size="sm" lineClamp={4} style={{ whiteSpace: "pre-line" }}>
{notification.body}
</Text>
<InfoDisplay date={notification.time} />
</Flex>
</Card>
))
) : (
<Text size="sm" c="dimmed">
{t("noItems")}
</Text>
)}
</Stack>
</ScrollArea>
);
}
const InfoDisplay = ({ date }: { date: Date }) => {
const timeAgo = useTimeAgo(date, 30000); // update every 30sec
return (
<Group gap={5} align={"center"}>
<IconClock size={"1rem"} color={"var(--mantine-color-dimmed)"} />
<Text size="sm" c="dimmed">
{timeAgo}
</Text>
</Group>
);
};

View File

@@ -0,0 +1,14 @@
import { IconMessage } from "@tabler/icons-react";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("notifications", {
icon: IconMessage,
createOptions() {
return optionsBuilder.from(() => ({}));
},
supportedIntegrations: getIntegrationKindsByCategory("notifications"),
}).withDynamicImport(() => import("./component"));

1790
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,11 +20,11 @@
"@next/eslint-plugin-next": "15.3.4",
"eslint-config-prettier": "^10.1.5",
"eslint-config-turbo": "^2.5.4",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.34.1"
"typescript-eslint": "^8.35.0"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",

View File

@@ -11,11 +11,11 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ianvs/prettier-plugin-sort-imports": "^4.4.2",
"prettier": "^3.5.3"
"prettier": "^3.6.2"
},
"devDependencies": {
"@homarr/tsconfig": "workspace:^0.1.0",
"prettier-plugin-packagejson": "^2.5.15",
"prettier-plugin-packagejson": "^2.5.16",
"typescript": "^5.8.3"
}
}