Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dd695ecf7 | ||
|
|
2726247e1b | ||
|
|
5ecc4db5da | ||
|
|
29884dda97 | ||
|
|
72633bf178 | ||
|
|
27fe82401b | ||
|
|
473a9f47f3 | ||
|
|
7fb7af12f4 | ||
|
|
380055e205 | ||
|
|
39bf0d036a | ||
|
|
5d8126d71e | ||
|
|
9020440193 | ||
|
|
bd8ac62c7a | ||
|
|
5a8653d850 | ||
|
|
dc754232bc | ||
|
|
9af542ee81 | ||
|
|
41a7182133 | ||
|
|
99358109ba | ||
|
|
df5b6fcb6a | ||
|
|
b03c9c1043 | ||
|
|
d68b80e6be | ||
|
|
f1143e54a0 | ||
|
|
54392dcea4 | ||
|
|
661afa72be | ||
|
|
d16e0ba7c1 | ||
|
|
e21b236d37 | ||
|
|
7e92f1bafa | ||
|
|
ee286c9556 | ||
|
|
68593335ec | ||
|
|
fb57d46b57 | ||
|
|
1724f9d26c | ||
|
|
90f6a2dd01 | ||
|
|
3f191da25c | ||
|
|
7c2f8b85bd | ||
|
|
c5db9db5d7 | ||
|
|
6a06a214be | ||
|
|
c08caeac5a | ||
|
|
c793bf1982 | ||
|
|
a369beb248 | ||
|
|
d736cf6daa | ||
|
|
3411354677 | ||
|
|
7c0a7428d8 | ||
|
|
5d394c849f | ||
|
|
9c7cfeb849 | ||
|
|
1eb47311fa | ||
|
|
1fe3450555 | ||
|
|
8b9832b2b8 | ||
|
|
57be43f51b | ||
|
|
824f443949 | ||
|
|
e9a35ba811 | ||
|
|
12f4e90e44 | ||
|
|
2ac75b23a4 | ||
|
|
430dc58068 | ||
|
|
8f1c3d8a2d | ||
|
|
b0bdf28016 | ||
|
|
01fd7b660c |
No files matched your search
+6
-1
@@ -12,6 +12,10 @@ AUTH_SECRET="supersecret"
|
||||
# or starting the project without any (which will show a randomly generated one).
|
||||
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
|
||||
|
||||
# Cron job API key is generated every time the container starts as it is required for communication between nextjs-api and tasks-api
|
||||
# export CRON_JOB_API_KEY=$(openssl rand -base64 32)
|
||||
# CRON_JOB_API_KEY="your-generated-api-key"
|
||||
|
||||
LOG_LEVEL='info'
|
||||
|
||||
# This is how you can use the sqlite driver:
|
||||
@@ -30,7 +34,8 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
# DB_PASSWORD='password'
|
||||
# DB_NAME='name-of-database'
|
||||
|
||||
# The below path can be used to store trusted certificates during development, it is not required and can be left empty.
|
||||
# The below path can be used to store trusted certificates, it is not required and can be left empty.
|
||||
# If it is empty, it will default to `/appdata/trusted-certificates` in production.
|
||||
# If it is used, please use the full path to the directory where the certificates are stored.
|
||||
# LOCAL_CERTIFICATE_PATH='FULL_PATH_TO_CERTIFICATES'
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.28.0
|
||||
- 1.27.0
|
||||
- 1.26.0
|
||||
- 1.25.0
|
||||
- 1.24.0
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
matchPackagePatterns: ["^@homarr/"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchPackagePatterns: ["^zod$", "^drizzle-zod$", "^zod-form-data$"],
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
matchUpdateTypes: ["minor", "patch", "pin", "digest"],
|
||||
automerge: true,
|
||||
|
||||
@@ -1,3 +1,46 @@
|
||||
## [1.28.1](https://github.com/homarr-labs/homarr/compare/v1.28.0...v1.28.1) (2025-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps:** update tiptap monorepo to v2.26.1 ([#3603](https://github.com/homarr-labs/homarr/issues/3603)) ([473a9f4](https://github.com/homarr-labs/homarr/commit/473a9f47f352a532128c1bf2c02490e8e4c6ca7c))
|
||||
* **releases-widget:** fixed bug with changing a missing provider ([#3605](https://github.com/homarr-labs/homarr/issues/3605)) ([5ecc4db](https://github.com/homarr-labs/homarr/commit/5ecc4db5dabcfd4f953f3b436527d34ea9191eef))
|
||||
|
||||
## [1.28.0](https://github.com/homarr-labs/homarr/compare/v1.27.0...v1.28.0) (2025-07-11)
|
||||
|
||||
### Features
|
||||
|
||||
* **icon-color:** add icon color clear button ([#3593](https://github.com/homarr-labs/homarr/issues/3593)) ([5a8653d](https://github.com/homarr-labs/homarr/commit/5a8653d8500388d9392b150d62a56474c05843d7))
|
||||
* **notebook:** add placeholder, redo and undo button ([#3564](https://github.com/homarr-labs/homarr/issues/3564)) ([3411354](https://github.com/homarr-labs/homarr/commit/3411354677fedec6ff5b5b85eb25eacc94308425))
|
||||
* **ping:** ignore certificate error and show request duration ([#3546](https://github.com/homarr-labs/homarr/issues/3546)) ([1eb4731](https://github.com/homarr-labs/homarr/commit/1eb47311fa81184bd67830d0669d9f87d9922e70))
|
||||
* **releases-widget:** define providers as integrations ([#3253](https://github.com/homarr-labs/homarr/issues/3253)) ([5d8126d](https://github.com/homarr-labs/homarr/commit/5d8126d71eea61af9c023895cb911d24544f8066))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **bookmarks:** wrong widget icon ([#3572](https://github.com/homarr-labs/homarr/issues/3572)) ([fb57d46](https://github.com/homarr-labs/homarr/commit/fb57d46b573cc9798469540f4823cbf73afd21a5))
|
||||
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.5.0 ([#3585](https://github.com/homarr-labs/homarr/issues/3585)) ([ee286c9](https://github.com/homarr-labs/homarr/commit/ee286c95563c99297b5419c005550936ca96757b))
|
||||
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.5.1 ([#3586](https://github.com/homarr-labs/homarr/issues/3586)) ([7e92f1b](https://github.com/homarr-labs/homarr/commit/7e92f1bafadf1188ee3ddc9d2dc9904f103aab4e))
|
||||
* **deps:** update dependency dotenv to ^17.1.0 ([#3567](https://github.com/homarr-labs/homarr/issues/3567)) ([a369beb](https://github.com/homarr-labs/homarr/commit/a369beb248e2cac1f1dcf32421efb48f241700a6))
|
||||
* **deps:** update dependency dotenv to ^17.2.0 ([#3578](https://github.com/homarr-labs/homarr/issues/3578)) ([1724f9d](https://github.com/homarr-labs/homarr/commit/1724f9d26c5761e3195d336eb451dd47855266a4))
|
||||
* **deps:** update dependency ldapts to v8.0.5 ([#3559](https://github.com/homarr-labs/homarr/issues/3559)) ([8b9832b](https://github.com/homarr-labs/homarr/commit/8b9832b2b81dcc1a4f9081755defe44fc0f38a1d))
|
||||
* **deps:** update dependency mysql2 to v3.14.2 ([#3592](https://github.com/homarr-labs/homarr/issues/3592)) ([d68b80e](https://github.com/homarr-labs/homarr/commit/d68b80e6be38366860de84778a45e5a43aae06eb))
|
||||
* **deps:** update dependency node-cron to ^4.2.1 ([#3590](https://github.com/homarr-labs/homarr/issues/3590)) ([e21b236](https://github.com/homarr-labs/homarr/commit/e21b236d37f076654768e88bc7ef139a3e51abbc))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.26.1 ([#3554](https://github.com/homarr-labs/homarr/issues/3554)) ([12f4e90](https://github.com/homarr-labs/homarr/commit/12f4e90e440a8ea1708a2a9ef64a851ef977bd6c))
|
||||
* **deps:** update dependency swagger-ui-react to ^5.26.2 ([#3558](https://github.com/homarr-labs/homarr/issues/3558)) ([57be43f](https://github.com/homarr-labs/homarr/commit/57be43f51b7ef306b962299789194de68ec9e8ea))
|
||||
* **deps:** update dependency typescript-eslint to ^8.36.0 ([#3563](https://github.com/homarr-labs/homarr/issues/3563)) ([7c0a742](https://github.com/homarr-labs/homarr/commit/7c0a7428d85d11f3d2226d809ba04671ca713b72))
|
||||
* **deps:** update dependency zod to ^3.25.75 ([#3555](https://github.com/homarr-labs/homarr/issues/3555)) ([e9a35ba](https://github.com/homarr-labs/homarr/commit/e9a35ba811bd1807d63c5f0c760d470b20f4410f))
|
||||
* **deps:** update dependency zod to ^3.25.76 ([#3569](https://github.com/homarr-labs/homarr/issues/3569)) ([c793bf1](https://github.com/homarr-labs/homarr/commit/c793bf1982c3a484176860cebad80c721acd597d))
|
||||
* **deps:** update dependency zod-validation-error to ^3.5.3 ([#3600](https://github.com/homarr-labs/homarr/issues/3600)) ([9020440](https://github.com/homarr-labs/homarr/commit/9020440193595a5b437464957d9a3745fb200c93))
|
||||
* **deps:** update mantine monorepo to ^8.1.3 ([#3560](https://github.com/homarr-labs/homarr/issues/3560)) ([1fe3450](https://github.com/homarr-labs/homarr/commit/1fe34505550f401be97c9d84c383ea70bf39b937))
|
||||
* **deps:** update tanstack-query monorepo to ^5.82.0 ([#3577](https://github.com/homarr-labs/homarr/issues/3577)) ([90f6a2d](https://github.com/homarr-labs/homarr/commit/90f6a2dd014a73cd0131781b6db56a1056e563b6))
|
||||
* **deps:** update tanstack-query monorepo to ^5.83.0 ([#3599](https://github.com/homarr-labs/homarr/issues/3599)) ([bd8ac62](https://github.com/homarr-labs/homarr/commit/bd8ac62c7af034525803410751bfd5ab72094f45))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.1.0 ([#3550](https://github.com/homarr-labs/homarr/issues/3550)) ([b0bdf28](https://github.com/homarr-labs/homarr/commit/b0bdf28016bae2ccfab24cab6df5b4846d1a8f48))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.2.0 ([#3556](https://github.com/homarr-labs/homarr/issues/3556)) ([824f443](https://github.com/homarr-labs/homarr/commit/824f443949e75ec3f3edacb9093faefbdec47ef4))
|
||||
* **deps:** update testcontainers-node monorepo to ^11.2.1 ([#3562](https://github.com/homarr-labs/homarr/issues/3562)) ([5d394c8](https://github.com/homarr-labs/homarr/commit/5d394c849f5f47b47c595f73763e89d0a5d6247a))
|
||||
* **deps:** update tiptap monorepo to v2.25.0 ([#3552](https://github.com/homarr-labs/homarr/issues/3552)) ([430dc58](https://github.com/homarr-labs/homarr/commit/430dc580682cf0885fcd0ef2b7b81a0dfdf93425))
|
||||
* **deps:** update tiptap monorepo to v2.25.1 ([#3591](https://github.com/homarr-labs/homarr/issues/3591)) ([661afa7](https://github.com/homarr-labs/homarr/commit/661afa72be96c629cad4ab7f6007214b54d8ef6a))
|
||||
* **deps:** update tiptap monorepo to v2.26.0 ([#3596](https://github.com/homarr-labs/homarr/issues/3596)) ([df5b6fc](https://github.com/homarr-labs/homarr/commit/df5b6fcb6a6215f5c1cf92e034e27f6e32405b3e))
|
||||
* unable to change certificate path for running from source ([#3582](https://github.com/homarr-labs/homarr/issues/3582)) ([f1143e5](https://github.com/homarr-labs/homarr/commit/f1143e54a06e00a348e37b5633fad4cb6390cff7))
|
||||
|
||||
## [1.27.0](https://github.com/homarr-labs/homarr/compare/v1.26.0...v1.27.0) (2025-07-04)
|
||||
|
||||
### Features
|
||||
|
||||
+1
-4
@@ -10,8 +10,6 @@ RUN apk add --no-cache libc6-compat curl bash
|
||||
RUN apk update
|
||||
COPY . .
|
||||
|
||||
# Install working version of corepack (See https://github.com/nodejs/corepack/issues/612)
|
||||
RUN npm install -g corepack@0.31.0 && corepack --version
|
||||
RUN corepack enable pnpm && pnpm install --recursive --frozen-lockfile
|
||||
|
||||
# Copy static data as it is not part of the build
|
||||
@@ -19,8 +17,7 @@ COPY static-data ./static-data
|
||||
ARG SKIP_ENV_VALIDATION='true'
|
||||
ARG CI='true'
|
||||
ARG DISABLE_REDIS_LOGS='true'
|
||||
# Install working version of corepack (See https://github.com/nodejs/corepack/issues/612)
|
||||
RUN npm install -g corepack@0.31.0 && corepack --version
|
||||
|
||||
RUN corepack enable pnpm && pnpm build
|
||||
|
||||
FROM base AS runner
|
||||
|
||||
+14
-14
@@ -49,17 +49,17 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@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",
|
||||
"@mantine/colors-generator": "^8.1.3",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@mantine/dropzone": "^8.1.3",
|
||||
"@mantine/hooks": "^8.1.3",
|
||||
"@mantine/modals": "^8.1.3",
|
||||
"@mantine/tiptap": "^8.1.3",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query-devtools": "^5.81.5",
|
||||
"@tanstack/react-query-next-experimental": "^5.81.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.83.0",
|
||||
"@tanstack/react-query-next-experimental": "^5.83.0",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/next": "^11.4.3",
|
||||
"@trpc/react-query": "^11.4.3",
|
||||
@@ -70,7 +70,7 @@
|
||||
"chroma-js": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.0.1",
|
||||
"dotenv": "^17.2.0",
|
||||
"flag-icons": "^7.5.0",
|
||||
"glob": "^11.0.3",
|
||||
"jotai": "^2.12.5",
|
||||
@@ -84,22 +84,22 @@
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.89.2",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.26.0",
|
||||
"swagger-ui-react": "^5.26.2",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.16.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"@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.2.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
@@ -102,12 +103,25 @@ export const ColorSettingsContent = ({ board }: Props) => {
|
||||
</InputWrapper>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ sm: 12, md: 6 }}>
|
||||
<ColorInput
|
||||
label={t("board.field.iconColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
{...form.getInputProps("iconColor")}
|
||||
/>
|
||||
<Group align="end">
|
||||
<ColorInput
|
||||
label={t("board.field.iconColor.label")}
|
||||
format="hex"
|
||||
swatches={Object.values(theme.colors).map((color) => color[6])}
|
||||
flex={1}
|
||||
{...form.getInputProps("iconColor")}
|
||||
/>
|
||||
<Button
|
||||
variant="subtle"
|
||||
leftSection={<IconX />}
|
||||
onClick={() => form.setFieldValue("iconColor", "")}
|
||||
disabled={!form.values.iconColor}
|
||||
>
|
||||
{t("board.field.clearColor.label")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ sm: 12, md: 6 }}>
|
||||
<Select
|
||||
label={t("board.field.itemRadius.label")}
|
||||
description={t("board.field.itemRadius.description")}
|
||||
|
||||
+10
-1
@@ -1,4 +1,12 @@
|
||||
import { IconGrid3x3, IconKey, IconMessage, IconPassword, IconServer, IconUser } from "@tabler/icons-react";
|
||||
import {
|
||||
IconGrid3x3,
|
||||
IconKey,
|
||||
IconMessage,
|
||||
IconPassword,
|
||||
IconPasswordUser,
|
||||
IconServer,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
@@ -9,5 +17,6 @@ export const integrationSecretIcons = {
|
||||
password: IconPassword,
|
||||
realm: IconServer,
|
||||
tokenId: IconGrid3x3,
|
||||
personalAccessToken: IconPasswordUser,
|
||||
topic: IconMessage,
|
||||
} satisfies Record<IntegrationSecretKind, TablerIcon>;
|
||||
@@ -21,7 +21,13 @@ import { z } from "zod";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { revalidatePathActionAsync } from "@homarr/common/client";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
import { getAllSecretKindOptions, getIconUrl, getIntegrationName, integrationDefs } from "@homarr/definitions";
|
||||
import {
|
||||
getAllSecretKindOptions,
|
||||
getIconUrl,
|
||||
getIntegrationDefaultUrl,
|
||||
getIntegrationName,
|
||||
integrationDefs,
|
||||
} from "@homarr/definitions";
|
||||
import type { UseFormReturnType } from "@homarr/form";
|
||||
import { useZodForm } from "@homarr/form";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -54,7 +60,7 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
const form = useZodForm(formSchema, {
|
||||
initialValues: {
|
||||
name: searchParams.name ?? getIntegrationName(searchParams.kind),
|
||||
url: searchParams.url ?? "",
|
||||
url: searchParams.url ?? getIntegrationDefaultUrl(searchParams.kind) ?? "",
|
||||
secrets: secretKinds[0].map((kind) => ({
|
||||
kind,
|
||||
value: "",
|
||||
|
||||
@@ -30,13 +30,12 @@
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^17.0.1",
|
||||
"dotenv": "^17.2.0",
|
||||
"fastify": "^5.4.0",
|
||||
"superjson": "2.2.2",
|
||||
"undici": "7.11.0"
|
||||
@@ -45,10 +44,10 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.16.0",
|
||||
"@types/node": "^22.16.3",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.30.1",
|
||||
"esbuild": "^0.25.6",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.0.1",
|
||||
"dotenv": "^17.2.0",
|
||||
"tsx": "4.20.3",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
@@ -34,8 +34,8 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.30.1",
|
||||
"esbuild": "^0.25.6",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "homarr",
|
||||
"version": "1.27.0",
|
||||
"version": "1.28.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "cross-env CI=true turbo build",
|
||||
@@ -42,18 +42,18 @@
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"conventional-changelog-conventionalcommits": "^9.0.0",
|
||||
"conventional-changelog-conventionalcommits": "^9.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.6",
|
||||
"testcontainers": "^11.0.3",
|
||||
"semantic-release": "^24.2.7",
|
||||
"testcontainers": "^11.2.1",
|
||||
"turbo": "^2.5.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"engines": {
|
||||
"node": ">=22.17.0"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -35,13 +35,12 @@
|
||||
"@homarr/log": "workspace:^",
|
||||
"@homarr/old-import": "workspace:^0.1.0",
|
||||
"@homarr/old-schema": "workspace:^0.1.0",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.3.0",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/react-query": "^11.4.3",
|
||||
"@trpc/server": "^11.4.3",
|
||||
@@ -52,13 +51,13 @@
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.3.2",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
getIconUrl,
|
||||
getIntegrationKindsByCategory,
|
||||
getPermissionsWithParents,
|
||||
integrationCategories,
|
||||
integrationDefs,
|
||||
integrationKinds,
|
||||
integrationSecretKindObject,
|
||||
@@ -129,6 +130,57 @@ export const integrationRouter = createTRPCRouter({
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
}),
|
||||
allOfGivenCategory: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
category: z.enum(integrationCategories),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const groupsOfCurrentUser = await ctx.db.query.groupMembers.findMany({
|
||||
where: eq(groupMembers.userId, ctx.session?.user.id ?? ""),
|
||||
});
|
||||
|
||||
const intergrationKinds = getIntegrationKindsByCategory(input.category);
|
||||
|
||||
const integrationsFromDb = await ctx.db.query.integrations.findMany({
|
||||
with: {
|
||||
userPermissions: {
|
||||
where: eq(integrationUserPermissions.userId, ctx.session?.user.id ?? ""),
|
||||
},
|
||||
groupPermissions: {
|
||||
where: inArray(
|
||||
integrationGroupPermissions.groupId,
|
||||
groupsOfCurrentUser.map((group) => group.groupId),
|
||||
),
|
||||
},
|
||||
},
|
||||
where: inArray(integrations.kind, intergrationKinds),
|
||||
});
|
||||
return integrationsFromDb
|
||||
.map((integration) => {
|
||||
const permissions = integration.userPermissions
|
||||
.map(({ permission }) => permission)
|
||||
.concat(integration.groupPermissions.map(({ permission }) => permission));
|
||||
|
||||
return {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
url: integration.url,
|
||||
permissions: {
|
||||
hasUseAccess:
|
||||
permissions.includes("use") || permissions.includes("interact") || permissions.includes("full"),
|
||||
hasInteractAccess: permissions.includes("interact") || permissions.includes("full"),
|
||||
hasFullAccess: permissions.includes("full"),
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(z.object({ query: z.string(), limit: z.number().min(1).max(100).default(10) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import type { Session } from "@homarr/auth";
|
||||
import { createDb } from "@homarr/db/test";
|
||||
import * as ping from "@homarr/ping";
|
||||
|
||||
import { appRouter } from "../../widgets/app";
|
||||
|
||||
// Mock the auth module to return an empty session
|
||||
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
|
||||
vi.mock("@homarr/ping", () => ({ sendPingRequestAsync: async () => await Promise.resolve(null) }));
|
||||
|
||||
describe("ping should call sendPingRequestAsync with url and return result", () => {
|
||||
test("ping with error response should return error and url", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ping, "sendPingRequestAsync");
|
||||
const url = "http://localhost";
|
||||
const db = createDb();
|
||||
const caller = appRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: null,
|
||||
});
|
||||
spy.mockImplementation(() => Promise.resolve({ error: "error" }));
|
||||
|
||||
// Act
|
||||
const result = await caller.ping({ url });
|
||||
|
||||
// Assert
|
||||
expect(result.url).toBe(url);
|
||||
expect("error" in result).toBe(true);
|
||||
});
|
||||
|
||||
test("ping with success response should return statusCode and url", async () => {
|
||||
// Arrange
|
||||
const spy = vi.spyOn(ping, "sendPingRequestAsync");
|
||||
const url = "http://localhost";
|
||||
const db = createDb();
|
||||
const caller = appRouter.createCaller({
|
||||
db,
|
||||
deviceType: undefined,
|
||||
session: null,
|
||||
});
|
||||
spy.mockImplementation(() => Promise.resolve({ statusCode: 200 }));
|
||||
|
||||
// Act
|
||||
const result = await caller.ping({ url });
|
||||
|
||||
// Assert
|
||||
expect(result.url).toBe(url);
|
||||
expect("statusCode" in result).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,12 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
import { sendPingRequestAsync } from "@homarr/ping";
|
||||
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
||||
import { pingUrlChannel } from "@homarr/redis";
|
||||
import { pingRequestHandler } from "@homarr/request-handler/ping";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
ping: publicProcedure.input(z.object({ url: z.string() })).query(async ({ input }) => {
|
||||
const pingResult = await sendPingRequestAsync(input.url);
|
||||
|
||||
return {
|
||||
url: input.url,
|
||||
...pingResult,
|
||||
};
|
||||
}),
|
||||
updatedPing: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -23,21 +15,27 @@ export const appRouter = createTRPCRouter({
|
||||
)
|
||||
.subscription(async ({ input }) => {
|
||||
await pingUrlChannel.addAsync(input.url);
|
||||
const innerHandler = pingRequestHandler.handler({ url: input.url });
|
||||
|
||||
const pingResult = await sendPingRequestAsync(input.url);
|
||||
return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>(
|
||||
(emit) => {
|
||||
// Run ping request in background
|
||||
void innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then(({ data }) => {
|
||||
emit.next({ url: input.url, ...data });
|
||||
});
|
||||
|
||||
return observable<{ url: string; statusCode: number } | { url: string; error: string }>((emit) => {
|
||||
emit.next({ url: input.url, ...pingResult });
|
||||
const unsubscribe = pingChannel.subscribe((message) => {
|
||||
// Only emit if same url
|
||||
if (message.url !== input.url) return;
|
||||
emit.next(message);
|
||||
});
|
||||
const unsubscribe = innerHandler.subscribe((pingResponse) => {
|
||||
emit.next({
|
||||
url: input.url,
|
||||
...pingResponse,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
void pingUrlChannel.removeAsync(input.url);
|
||||
};
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
void pingUrlChannel.removeAsync(input.url);
|
||||
};
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
import { escapeForRegEx } from "@tiptap/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
|
||||
@@ -23,31 +25,31 @@ const releaseVersionFilterSchema = z.object({
|
||||
|
||||
export const releasesRouter = createTRPCRouter({
|
||||
getLatest: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("releasesProvider")))
|
||||
.input(
|
||||
z.object({
|
||||
repositories: z.array(
|
||||
z.object({
|
||||
providerKey: z.string(),
|
||||
id: z.string(),
|
||||
identifier: z.string(),
|
||||
versionFilter: releaseVersionFilterSchema.optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const result = await Promise.all(
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await Promise.all(
|
||||
input.repositories.map(async (repository) => {
|
||||
const innerHandler = releasesRequestHandler.handler({
|
||||
providerKey: repository.providerKey,
|
||||
const innerHandler = releasesRequestHandler.handler(ctx.integration, {
|
||||
id: repository.id,
|
||||
identifier: repository.identifier,
|
||||
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
||||
});
|
||||
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
@@ -34,12 +34,12 @@
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "8.0.4",
|
||||
"ldapts": "8.0.5",
|
||||
"next": "15.3.5",
|
||||
"next-auth": "5.0.0-beta.29",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.9.1",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
|
||||
interface IntegrationContextProps {
|
||||
integrations: {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
kind: string;
|
||||
kind: IntegrationKind;
|
||||
permissions: {
|
||||
hasFullAccess: boolean;
|
||||
hasInteractAccess: boolean;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,8 @@ import { db } from "@homarr/db";
|
||||
import type { trustedCertificateHostnames } from "@homarr/db/schema";
|
||||
|
||||
const getCertificateFolder = () => {
|
||||
return env.NODE_ENV === "production"
|
||||
? path.join("/appdata", "trusted-certificates")
|
||||
: process.env.LOCAL_CERTIFICATE_PATH;
|
||||
if (env.NODE_ENV !== "production") return process.env.LOCAL_CERTIFICATE_PATH;
|
||||
return process.env.LOCAL_CERTIFICATE_PATH ?? path.join("/appdata", "trusted-certificates");
|
||||
};
|
||||
|
||||
export const loadCustomRootCertificatesAsync = async () => {
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"dotenv": "^17.0.1"
|
||||
"dotenv": "^17.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.30.1",
|
||||
"esbuild": "^0.25.6",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,20 @@
|
||||
"dependencies": {
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.3.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"undici": "7.11.0",
|
||||
"zod": "^3.25.74",
|
||||
"zod-validation-error": "^3.5.2"
|
||||
"zod": "^3.25.76",
|
||||
"zod-validation-error": "^3.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createId } from "@paralleldrive/cuid2";
|
||||
@@ -5,6 +5,7 @@ export * from "./array";
|
||||
export * from "./date";
|
||||
export * from "./stopwatch";
|
||||
export * from "./hooks";
|
||||
export * from "./id";
|
||||
export * from "./url";
|
||||
export * from "./number";
|
||||
export * from "./error";
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
"@homarr/cron-jobs": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/server": "^11.4.3",
|
||||
"@trpc/tanstack-react-query": "^11.4.3",
|
||||
"node-cron": "^4.2.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"@types/react": "19.1.8",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"node-cron": "^4.2.0"
|
||||
"node-cron": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@
|
||||
"@homarr/icons": "workspace:^0.1.0",
|
||||
"@homarr/integrations": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/ping": "workspace:^0.1.0",
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"@homarr/request-handler": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
@@ -44,7 +43,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
|
||||
import { db } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { logger } from "@homarr/log";
|
||||
import { sendPingRequestAsync } from "@homarr/ping";
|
||||
import { pingChannel, pingUrlChannel } from "@homarr/redis";
|
||||
import { pingUrlChannel } from "@homarr/redis";
|
||||
import { pingRequestHandler } from "@homarr/request-handler/ping";
|
||||
|
||||
import { createCronJob } from "../lib";
|
||||
|
||||
@@ -28,16 +28,6 @@ export const pingJob = createCronJob("ping", EVERY_MINUTE, {
|
||||
});
|
||||
|
||||
const pingAsync = async (url: string) => {
|
||||
const pingResult = await sendPingRequestAsync(url);
|
||||
|
||||
if ("statusCode" in pingResult) {
|
||||
logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`);
|
||||
} else {
|
||||
logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`);
|
||||
}
|
||||
|
||||
await pingChannel.publishAsync({
|
||||
url,
|
||||
...pingResult,
|
||||
});
|
||||
const handler = pingRequestHandler.handler({ url });
|
||||
await handler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { createId } from "@homarr/common";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
|
||||
import { eq } from "../..";
|
||||
import type { Database } from "../..";
|
||||
import { items } from "../../schema";
|
||||
|
||||
export async function migrateReleaseWidgetProviderToOptionsAsync(db: Database) {
|
||||
const existingItems = await db.query.items.findMany({
|
||||
where: (items, { eq }) => eq(items.kind, "releases"),
|
||||
});
|
||||
|
||||
const integrationKinds = getIntegrationKindsByCategory("releasesProvider");
|
||||
const providerIntegrations = await db.query.integrations.findMany({
|
||||
where: (integrations, { inArray }) => inArray(integrations.kind, integrationKinds),
|
||||
columns: {
|
||||
id: true,
|
||||
kind: true,
|
||||
},
|
||||
});
|
||||
|
||||
const providerIntegrationMap = new Map<IntegrationKind, string>(
|
||||
providerIntegrations.map((integration) => [integration.kind, integration.id]),
|
||||
);
|
||||
|
||||
const updates: {
|
||||
id: string;
|
||||
options: object;
|
||||
}[] = [];
|
||||
for (const item of existingItems) {
|
||||
const options = SuperJSON.parse<object>(item.options);
|
||||
if (!("repositories" in options)) continue;
|
||||
if (!Array.isArray(options.repositories)) continue;
|
||||
if (options.repositories.length === 0) continue;
|
||||
if (!options.repositories.some((repository) => "providerKey" in repository)) continue;
|
||||
|
||||
const updatedRepositories = options.repositories.map(
|
||||
({ providerKey, ...otherFields }: { providerKey: string; [key: string]: unknown }) => {
|
||||
// Ensure providerKey is camelCase
|
||||
const provider = providerKey.charAt(0).toLowerCase() + providerKey.slice(1);
|
||||
|
||||
return {
|
||||
id: createId(),
|
||||
providerIntegrationId: providerIntegrationMap.get(provider as IntegrationKind) ?? null,
|
||||
...otherFields,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
updates.push({
|
||||
id: item.id,
|
||||
options: {
|
||||
...options,
|
||||
repositories: updatedRepositories,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const update of updates) {
|
||||
await db
|
||||
.update(items)
|
||||
.set({
|
||||
options: SuperJSON.stringify(update.options),
|
||||
})
|
||||
.where(eq(items.id, update.id));
|
||||
}
|
||||
|
||||
console.log(`Migrated release widget providers to integrations count="${updates.length}"`);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Database } from "../..";
|
||||
import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widget_provider_to_options";
|
||||
|
||||
export const applyCustomMigrationsAsync = async (db: Database) => {
|
||||
await migrateReleaseWidgetProviderToOptionsAsync(db);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { applyCustomMigrationsAsync } from ".";
|
||||
import { database } from "../../driver";
|
||||
|
||||
applyCustomMigrationsAsync(database)
|
||||
.then(() => {
|
||||
console.log("Custom migrations applied successfully");
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed to apply custom migrations\n\t", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import mysql from "mysql2";
|
||||
import type { Database } from "../..";
|
||||
import { env } from "../../env";
|
||||
import * as mysqlSchema from "../../schema/mysql";
|
||||
import { applyCustomMigrationsAsync } from "../custom";
|
||||
import { seedDataAsync } from "../seed";
|
||||
|
||||
const migrationsFolder = process.argv[2] ?? ".";
|
||||
@@ -30,6 +31,7 @@ const migrateAsync = async () => {
|
||||
|
||||
await migrate(db, { migrationsFolder });
|
||||
await seedDataAsync(db as unknown as Database);
|
||||
await applyCustomMigrationsAsync(db as unknown as Database);
|
||||
};
|
||||
|
||||
migrateAsync()
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { objectKeys } from "@homarr/common";
|
||||
import { createDocumentationLink, everyoneGroup } from "@homarr/definitions";
|
||||
import {
|
||||
createDocumentationLink,
|
||||
everyoneGroup,
|
||||
getIntegrationDefaultUrl,
|
||||
getIntegrationName,
|
||||
integrationKinds,
|
||||
} from "@homarr/definitions";
|
||||
import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server-settings";
|
||||
|
||||
import type { Database } from "..";
|
||||
@@ -9,7 +15,8 @@ import {
|
||||
insertServerSettingByKeyAsync,
|
||||
updateServerSettingByKeyAsync,
|
||||
} from "../queries/server-setting";
|
||||
import { onboarding, searchEngines } from "../schema";
|
||||
import { integrations, onboarding, searchEngines } from "../schema";
|
||||
import type { Integration } from "../schema";
|
||||
import { groups } from "../schema/mysql";
|
||||
|
||||
export const seedDataAsync = async (db: Database) => {
|
||||
@@ -17,6 +24,7 @@ export const seedDataAsync = async (db: Database) => {
|
||||
await seedOnboardingAsync(db);
|
||||
await seedServerSettingsAsync(db);
|
||||
await seedDefaultSearchEnginesAsync(db);
|
||||
await seedDefaultIntegrationsAsync(db);
|
||||
};
|
||||
|
||||
const seedEveryoneGroupAsync = async (db: Database) => {
|
||||
@@ -131,3 +139,53 @@ const seedServerSettingsAsync = async (db: Database) => {
|
||||
console.log(`Updated serverSetting through seed key=${settingsKey}`);
|
||||
}
|
||||
};
|
||||
|
||||
const seedDefaultIntegrationsAsync = async (db: Database) => {
|
||||
const defaultIntegrations = integrationKinds.reduce<Integration[]>((acc, kind) => {
|
||||
const name = getIntegrationName(kind);
|
||||
const defaultUrl = getIntegrationDefaultUrl(kind);
|
||||
|
||||
if (defaultUrl !== undefined) {
|
||||
acc.push({
|
||||
id: "new",
|
||||
name: `${name} Default`,
|
||||
url: defaultUrl,
|
||||
kind,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (defaultIntegrations.length === 0) {
|
||||
console.warn("No default integrations found to seed");
|
||||
return;
|
||||
}
|
||||
|
||||
let createdCount = 0;
|
||||
await Promise.all(
|
||||
defaultIntegrations.map(async (integration) => {
|
||||
const existingKind = await db.$count(integrations, eq(integrations.kind, integration.kind));
|
||||
|
||||
if (existingKind > 0) {
|
||||
console.log(`Skipping seeding of default ${integration.kind} integration as one already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
const newIntegration = {
|
||||
...integration,
|
||||
id: createId(),
|
||||
};
|
||||
|
||||
await db.insert(integrations).values(newIntegration);
|
||||
createdCount++;
|
||||
}),
|
||||
);
|
||||
|
||||
if (createdCount === 0) {
|
||||
console.log("No default integrations were created as they already exist");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Created ${createdCount} default integrations through seeding process`);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
||||
|
||||
import { env } from "../../env";
|
||||
import * as sqliteSchema from "../../schema/sqlite";
|
||||
import { applyCustomMigrationsAsync } from "../custom";
|
||||
import { seedDataAsync } from "../seed";
|
||||
|
||||
const migrationsFolder = process.argv[2] ?? ".";
|
||||
@@ -16,6 +17,7 @@ const migrateAsync = async () => {
|
||||
migrate(db, { migrationsFolder });
|
||||
|
||||
await seedDataAsync(db);
|
||||
await applyCustomMigrationsAsync(db);
|
||||
};
|
||||
|
||||
migrateAsync()
|
||||
|
||||
@@ -23,12 +23,13 @@
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"migration:custom": "pnpm with-env tsx ./migrations/custom/run-custom.ts",
|
||||
"migration:mysql:drop": "pnpm with-env drizzle-kit drop --config ./configs/mysql.config.ts",
|
||||
"migration:mysql:generate": "pnpm with-env drizzle-kit generate --config ./configs/mysql.config.ts",
|
||||
"migration:mysql:run": "pnpm with-env drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed",
|
||||
"migration:mysql:run": "pnpm with-env drizzle-kit migrate --config ./configs/mysql.config.ts && pnpm run seed && pnpm run migration:custom",
|
||||
"migration:sqlite:drop": "pnpm with-env drizzle-kit drop --config ./configs/sqlite.config.ts",
|
||||
"migration:sqlite:generate": "pnpm with-env drizzle-kit generate --config ./configs/sqlite.config.ts",
|
||||
"migration:sqlite:run": "pnpm with-env drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed",
|
||||
"migration:sqlite:run": "pnpm with-env drizzle-kit migrate --config ./configs/sqlite.config.ts && pnpm run seed && pnpm run migration:custom",
|
||||
"push:mysql": "pnpm with-env drizzle-kit push --config ./configs/mysql.config.ts",
|
||||
"push:sqlite": "pnpm with-env drizzle-kit push --config ./configs/sqlite.config.ts",
|
||||
"seed": "pnpm with-env tsx ./migrations/run-seed.ts",
|
||||
@@ -44,15 +45,16 @@
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^11.0.3",
|
||||
"@testcontainers/mysql": "^11.2.1",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"dotenv": "^17.0.1",
|
||||
"dotenv": "^17.2.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.2",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"mysql2": "3.14.1"
|
||||
"mysql2": "3.14.2",
|
||||
"superjson": "2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -60,8 +62,8 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"eslint": "^9.30.1",
|
||||
"esbuild": "^0.25.6",
|
||||
"eslint": "^9.31.0",
|
||||
"prettier": "^3.6.2",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/analytics"
|
||||
| "/docs/tags/api"
|
||||
| "/docs/tags/apps"
|
||||
| "/docs/tags/background"
|
||||
| "/docs/tags/banner"
|
||||
| "/docs/tags/blocking"
|
||||
| "/docs/tags/boards"
|
||||
@@ -89,6 +90,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/integrations"
|
||||
| "/docs/tags/interface"
|
||||
| "/docs/tags/jellyserr"
|
||||
| "/docs/tags/jobs"
|
||||
| "/docs/tags/layout"
|
||||
| "/docs/tags/ldap"
|
||||
| "/docs/tags/links"
|
||||
@@ -98,6 +100,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/media"
|
||||
| "/docs/tags/minecraft"
|
||||
| "/docs/tags/monitoring"
|
||||
| "/docs/tags/network"
|
||||
| "/docs/tags/news"
|
||||
| "/docs/tags/notebook"
|
||||
| "/docs/tags/notes"
|
||||
@@ -130,11 +133,13 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/tags/stocks"
|
||||
| "/docs/tags/system"
|
||||
| "/docs/tags/table"
|
||||
| "/docs/tags/tasks"
|
||||
| "/docs/tags/technical-documentation"
|
||||
| "/docs/tags/text"
|
||||
| "/docs/tags/torrent"
|
||||
| "/docs/tags/traefik"
|
||||
| "/docs/tags/translations"
|
||||
| "/docs/tags/unifi-controller"
|
||||
| "/docs/tags/unraid"
|
||||
| "/docs/tags/uploads"
|
||||
| "/docs/tags/usenet"
|
||||
@@ -190,6 +195,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/kubernetes"
|
||||
| "/docs/integrations/media-requester"
|
||||
| "/docs/integrations/media-server"
|
||||
| "/docs/integrations/network"
|
||||
| "/docs/integrations/servarr"
|
||||
| "/docs/integrations/torrent"
|
||||
| "/docs/integrations/usenet"
|
||||
@@ -201,6 +207,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/management/media"
|
||||
| "/docs/management/search-engines"
|
||||
| "/docs/management/settings"
|
||||
| "/docs/management/tasks"
|
||||
| "/docs/management/users"
|
||||
| "/docs/widgets/bookmarks"
|
||||
| "/docs/widgets/calendar"
|
||||
@@ -214,6 +221,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/widgets/media-requests"
|
||||
| "/docs/widgets/media-server"
|
||||
| "/docs/widgets/minecraft-server-status"
|
||||
| "/docs/widgets/network-controller"
|
||||
| "/docs/widgets/notebook"
|
||||
| "/docs/widgets/releases"
|
||||
| "/docs/widgets/rss"
|
||||
|
||||
@@ -7,6 +7,7 @@ export const integrationSecretKindObject = {
|
||||
password: { isPublic: false },
|
||||
tokenId: { isPublic: true },
|
||||
realm: { isPublic: true },
|
||||
personalAccessToken: { isPublic: false },
|
||||
topic: { isPublic: true },
|
||||
} satisfies Record<string, { isPublic: boolean }>;
|
||||
|
||||
@@ -17,6 +18,7 @@ interface integrationDefinition {
|
||||
iconUrl: string;
|
||||
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
|
||||
category: AtLeastOneOf<IntegrationCategory>;
|
||||
defaultUrl?: string; // optional default URL for the integration
|
||||
}
|
||||
|
||||
export const integrationDefs = {
|
||||
@@ -170,6 +172,41 @@ export const integrationDefs = {
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
||||
category: ["networkController"],
|
||||
},
|
||||
github: {
|
||||
name: "Github",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://api.github.com",
|
||||
},
|
||||
dockerHub: {
|
||||
name: "Docker Hub",
|
||||
secretKinds: [[], ["username", "personalAccessToken"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://hub.docker.com",
|
||||
},
|
||||
gitlab: {
|
||||
name: "Gitlab",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://gitlab.com",
|
||||
},
|
||||
npm: {
|
||||
name: "NPM",
|
||||
secretKinds: [[]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://registry.npmjs.org",
|
||||
},
|
||||
codeberg: {
|
||||
name: "Codeberg",
|
||||
secretKinds: [[], ["personalAccessToken"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg",
|
||||
category: ["releasesProvider"],
|
||||
defaultUrl: "https://codeberg.org",
|
||||
},
|
||||
ntfy: {
|
||||
name: "ntfy",
|
||||
secretKinds: [["topic"], ["topic", "apiKey"]],
|
||||
@@ -209,6 +246,11 @@ export const getDefaultSecretKinds = (integration: IntegrationKind): Integration
|
||||
export const getAllSecretKindOptions = (integration: IntegrationKind): AtLeastOneOf<IntegrationSecretKind[]> =>
|
||||
integrationDefs[integration].secretKinds;
|
||||
|
||||
export const getIntegrationDefaultUrl = (integration: IntegrationKind) => {
|
||||
const definition = integrationDefs[integration];
|
||||
return "defaultUrl" in definition ? definition.defaultUrl : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all integration kinds that share a category, typed only by the kinds belonging to the category
|
||||
* @param category Category to filter by, belonging to IntegrationCategory
|
||||
@@ -234,20 +276,25 @@ export type IntegrationKindByCategory<TCategory extends IntegrationCategory> = {
|
||||
|
||||
export type IntegrationSecretKind = keyof typeof integrationSecretKindObject;
|
||||
export type IntegrationKind = keyof typeof integrationDefs;
|
||||
export type IntegrationCategory =
|
||||
| "dnsHole"
|
||||
| "mediaService"
|
||||
| "calendar"
|
||||
| "mediaSearch"
|
||||
| "mediaRequest"
|
||||
| "downloadClient"
|
||||
| "usenet"
|
||||
| "torrent"
|
||||
| "miscellaneous"
|
||||
| "smartHomeServer"
|
||||
| "indexerManager"
|
||||
| "healthMonitoring"
|
||||
| "search"
|
||||
| "mediaTranscoding"
|
||||
| "networkController"
|
||||
| "notifications";
|
||||
|
||||
export const integrationCategories = [
|
||||
"dnsHole",
|
||||
"mediaService",
|
||||
"calendar",
|
||||
"mediaSearch",
|
||||
"mediaRequest",
|
||||
"downloadClient",
|
||||
"usenet",
|
||||
"torrent",
|
||||
"miscellaneous",
|
||||
"smartHomeServer",
|
||||
"indexerManager",
|
||||
"healthMonitoring",
|
||||
"search",
|
||||
"mediaTranscoding",
|
||||
"networkController",
|
||||
"releasesProvider",
|
||||
"notifications",
|
||||
] as const;
|
||||
|
||||
export type IntegrationCategory = (typeof integrationCategories)[number];
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.42",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
Vendored
+2
-2
@@ -24,13 +24,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.13.8",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^8.1.2",
|
||||
"zod": "^3.25.74"
|
||||
"@mantine/form": "^8.1.3",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -29,15 +29,15 @@
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
"@ctrl/deluge": "^7.1.0",
|
||||
"@ctrl/qbittorrent": "^9.6.0",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@gitbeaker/rest": "^42.5.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
@@ -40,11 +41,12 @@
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"maria2": "^0.4.1",
|
||||
"node-ical": "^0.20.1",
|
||||
"octokit": "^5.0.3",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.5",
|
||||
"undici": "7.11.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
@@ -52,7 +54,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-unifi": "^2.5.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import type { Integration as DbIntegration } from "@homarr/db/schema";
|
||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||
|
||||
import { AdGuardHomeIntegration } from "../adguard-home/adguard-home-integration";
|
||||
import { CodebergIntegration } from "../codeberg/codeberg-integration";
|
||||
import { DashDotIntegration } from "../dashdot/dashdot-integration";
|
||||
import { DockerHubIntegration } from "../docker-hub/docker-hub-integration";
|
||||
import { Aria2Integration } from "../download-client/aria2/aria2-integration";
|
||||
import { DelugeIntegration } from "../download-client/deluge/deluge-integration";
|
||||
import { NzbGetIntegration } from "../download-client/nzbget/nzbget-integration";
|
||||
@@ -12,6 +14,8 @@ import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorre
|
||||
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
|
||||
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
|
||||
import { EmbyIntegration } from "../emby/emby-integration";
|
||||
import { GithubIntegration } from "../github/github-integration";
|
||||
import { GitlabIntegration } from "../gitlab/gitlab-integration";
|
||||
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
|
||||
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
|
||||
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
|
||||
@@ -22,6 +26,7 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"
|
||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||
import { MockIntegration } from "../mock/mock-integration";
|
||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||
import { NPMIntegration } from "../npm/npm-integration";
|
||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||
@@ -94,6 +99,11 @@ export const integrationCreators = {
|
||||
emby: EmbyIntegration,
|
||||
nextcloud: NextcloudIntegration,
|
||||
unifiController: UnifiControllerIntegration,
|
||||
github: GithubIntegration,
|
||||
dockerHub: DockerHubIntegration,
|
||||
gitlab: GitlabIntegration,
|
||||
npm: NPMIntegration,
|
||||
codeberg: CodebergIntegration,
|
||||
ntfy: NTFYIntegration,
|
||||
mock: MockIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import type { RequestInit, Response } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import type {
|
||||
DetailsProviderResponse,
|
||||
ReleasesRepository,
|
||||
ReleasesResponse,
|
||||
} from "../interfaces/releases-providers/releases-providers-types";
|
||||
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
|
||||
|
||||
const localLogger = logger.child({ module: "CodebergIntegration" });
|
||||
|
||||
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||
if (!this.hasSecretValue("personalAccessToken")) return await callback({});
|
||||
|
||||
return await callback({
|
||||
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
|
||||
});
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await this.withHeadersAsync(async (headers) => {
|
||||
return await input.fetchAsync(this.url("/version"), {
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
||||
const [owner, name] = repository.identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
localLogger.warn(
|
||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Codeberg integration`,
|
||||
{
|
||||
identifier: repository.identifier,
|
||||
},
|
||||
);
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "invalidIdentifier" },
|
||||
};
|
||||
}
|
||||
|
||||
const details = await this.getDetailsAsync(owner, name);
|
||||
|
||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||
return fetchWithTrustedCertificatesAsync(
|
||||
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`),
|
||||
{ headers },
|
||||
);
|
||||
});
|
||||
|
||||
if (!releasesResponse.ok) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { message: releasesResponse.statusText },
|
||||
};
|
||||
}
|
||||
|
||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: {
|
||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const formattedReleases = data.map((tag) => ({
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.url,
|
||||
releaseDescription: tag.body,
|
||||
isPreRelease: tag.prerelease,
|
||||
}));
|
||||
return getLatestRelease(formattedReleases, repository, details);
|
||||
}
|
||||
}
|
||||
|
||||
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {
|
||||
const response = await this.withHeadersAsync(async (headers) => {
|
||||
return await fetchWithTrustedCertificatesAsync(
|
||||
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`),
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
localLogger.warn(`Failed to get details response for ${owner}/${name} with Codeberg integration`, {
|
||||
owner,
|
||||
name,
|
||||
error: response.statusText,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const responseJson = await response.json();
|
||||
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
||||
|
||||
if (!success) {
|
||||
localLogger.warn(`Failed to parse details response for ${owner}/${name} with Codeberg integration`, {
|
||||
owner,
|
||||
name,
|
||||
error,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
projectUrl: data.html_url,
|
||||
projectDescription: data.description,
|
||||
isFork: data.fork,
|
||||
isArchived: data.archived,
|
||||
createdAt: data.created_at,
|
||||
starsCount: data.stars_count,
|
||||
openIssues: data.open_issues_count,
|
||||
forksCount: data.forks_count,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const releasesResponseSchema = z.array(
|
||||
z.object({
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
url: z.string(),
|
||||
body: z.string(),
|
||||
prerelease: z.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const detailsResponseSchema = z.object({
|
||||
html_url: z.string(),
|
||||
description: z.string(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
stars_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import type { fetch, RequestInit, Response } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import type { SessionStore } from "../base/session-store";
|
||||
import { createSessionStore } from "../base/session-store";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import type {
|
||||
DetailsProviderResponse,
|
||||
ReleasesRepository,
|
||||
ReleasesResponse,
|
||||
} from "../interfaces/releases-providers/releases-providers-types";
|
||||
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
|
||||
|
||||
const localLogger = logger.child({ module: "DockerHubIntegration" });
|
||||
|
||||
export class DockerHubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private readonly sessionStore: SessionStore<string>;
|
||||
|
||||
constructor(integration: IntegrationInput) {
|
||||
super(integration);
|
||||
this.sessionStore = createSessionStore(integration);
|
||||
}
|
||||
|
||||
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
|
||||
if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) return await callback({});
|
||||
|
||||
const storedSession = await this.sessionStore.getAsync();
|
||||
|
||||
if (storedSession) {
|
||||
localLogger.debug("Using stored session for request", { integrationId: this.integration.id });
|
||||
const response = await callback({
|
||||
Authorization: `Bearer ${storedSession}`,
|
||||
});
|
||||
if (response.status !== 401) {
|
||||
return response;
|
||||
}
|
||||
|
||||
localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id });
|
||||
}
|
||||
|
||||
const accessToken = await this.getSessionAsync();
|
||||
await this.sessionStore.setAsync(accessToken);
|
||||
return await callback({
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
});
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const hasAuth = this.hasSecretValue("username") && this.hasSecretValue("personalAccessToken");
|
||||
|
||||
if (hasAuth) {
|
||||
localLogger.debug("Testing DockerHub connection with authentication", { integrationId: this.integration.id });
|
||||
await this.getSessionAsync(input.fetchAsync);
|
||||
} else {
|
||||
localLogger.debug("Testing DockerHub connection without authentication", { integrationId: this.integration.id });
|
||||
const response = await input.fetchAsync(this.url("/v2/repositories/library"));
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
||||
const relativeUrl = this.getRelativeUrl(repository.identifier);
|
||||
if (relativeUrl === "/") {
|
||||
localLogger.warn(
|
||||
`Invalid identifier format. Expected 'owner/name' or 'name', for ${repository.identifier} on DockerHub`,
|
||||
{
|
||||
identifier: repository.identifier,
|
||||
},
|
||||
);
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "invalidIdentifier" },
|
||||
};
|
||||
}
|
||||
|
||||
const details = await this.getDetailsAsync(relativeUrl);
|
||||
|
||||
const releasesResponse = await this.withHeadersAsync(async (headers) => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/tags?page_size=100`), {
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
if (!releasesResponse.ok) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { message: releasesResponse.statusText },
|
||||
};
|
||||
}
|
||||
|
||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||
const releasesResult = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||
|
||||
if (!releasesResult.success) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: {
|
||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return getLatestRelease(releasesResult.data.results, repository, details);
|
||||
}
|
||||
}
|
||||
|
||||
private async getDetailsAsync(relativeUrl: `/${string}`): Promise<DetailsProviderResponse | undefined> {
|
||||
const response = await this.withHeadersAsync(async (headers) => {
|
||||
return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/`), {
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
localLogger.warn(`Failed to get details response for ${relativeUrl} with DockerHub integration`, {
|
||||
relativeUrl,
|
||||
error: response.statusText,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const responseJson = await response.json();
|
||||
const { data, success, error } = detailsResponseSchema.safeParse(responseJson);
|
||||
|
||||
if (!success) {
|
||||
localLogger.warn(`Failed to parse details response for ${relativeUrl} with DockerHub integration`, {
|
||||
relativeUrl,
|
||||
error,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
projectUrl: `https://hub.docker.com/r/${data.namespace === "library" ? "_" : data.namespace}/${data.name}`,
|
||||
projectDescription: data.description,
|
||||
createdAt: data.date_registered,
|
||||
starsCount: data.star_count,
|
||||
};
|
||||
}
|
||||
|
||||
private getRelativeUrl(identifier: string): `/${string}` {
|
||||
if (identifier.indexOf("/") > 0) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "/";
|
||||
}
|
||||
return `/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`;
|
||||
} else {
|
||||
return `/v2/repositories/library/${encodeURIComponent(identifier)}`;
|
||||
}
|
||||
}
|
||||
|
||||
private async getSessionAsync(fetchAsync: typeof fetch = fetchWithTrustedCertificatesAsync): Promise<string> {
|
||||
const response = await fetchAsync(this.url("/v2/auth/token"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
identifier: this.getSecretValue("username"),
|
||||
secret: this.getSecretValue("personalAccessToken"),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) throw new ResponseError(response);
|
||||
|
||||
const data = await response.json();
|
||||
const result = await accessTokenResponseSchema.parseAsync(data);
|
||||
|
||||
if (!result.access_token) {
|
||||
throw new ResponseError({ status: 401, url: response.url });
|
||||
}
|
||||
|
||||
localLogger.info("Received session successfully", { integrationId: this.integration.id });
|
||||
|
||||
return result.access_token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const accessTokenResponseSchema = z.object({
|
||||
access_token: z.string(),
|
||||
});
|
||||
|
||||
export const releasesResponseSchema = z.object({
|
||||
results: z.array(
|
||||
z.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) }).transform((tag) => ({
|
||||
latestRelease: tag.name,
|
||||
latestReleaseAt: tag.last_updated,
|
||||
})),
|
||||
),
|
||||
});
|
||||
|
||||
export const detailsResponseSchema = z.object({
|
||||
name: z.string(),
|
||||
namespace: z.string(),
|
||||
description: z.string(),
|
||||
star_count: z.number(),
|
||||
date_registered: z.string().transform((value) => new Date(value)),
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Octokit, RequestError } from "octokit";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import type {
|
||||
DetailsProviderResponse,
|
||||
ReleaseProviderResponse,
|
||||
ReleasesRepository,
|
||||
ReleasesResponse,
|
||||
} from "../interfaces/releases-providers/releases-providers-types";
|
||||
|
||||
const localLogger = logger.child({ module: "GithubIntegration" });
|
||||
|
||||
export class GithubIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
private static readonly userAgent = "Homarr-Lab/Homarr:GithubIntegration";
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const headers: RequestInit["headers"] = {
|
||||
"User-Agent": GithubIntegration.userAgent,
|
||||
};
|
||||
|
||||
if (this.hasSecretValue("personalAccessToken"))
|
||||
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
|
||||
|
||||
const response = await input.fetchAsync(this.url("/octocat"), {
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
||||
const [owner, name] = repository.identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
localLogger.warn(
|
||||
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Github integration`,
|
||||
{
|
||||
identifier: repository.identifier,
|
||||
},
|
||||
);
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "invalidIdentifier" },
|
||||
};
|
||||
}
|
||||
|
||||
const api = this.getApi();
|
||||
|
||||
const details = await this.getDetailsAsync(api, owner, name);
|
||||
|
||||
try {
|
||||
const releasesResponse = await api.rest.repos.listReleases({
|
||||
owner,
|
||||
repo: name,
|
||||
});
|
||||
|
||||
if (releasesResponse.data.length === 0) {
|
||||
localLogger.warn(`No releases found, for ${repository.identifier} with Github integration`, {
|
||||
identifier: repository.identifier,
|
||||
});
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "noReleasesFound" },
|
||||
};
|
||||
}
|
||||
|
||||
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
||||
if (!release.published_at) return acc;
|
||||
|
||||
acc.push({
|
||||
latestRelease: release.tag_name,
|
||||
latestReleaseAt: new Date(release.published_at),
|
||||
releaseUrl: release.html_url,
|
||||
releaseDescription: release.body ?? undefined,
|
||||
isPreRelease: release.prerelease,
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof RequestError ? error.message : String(error);
|
||||
|
||||
localLogger.warn(`Failed to get releases for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
name,
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { message: errorMessage },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async getDetailsAsync(
|
||||
api: Octokit,
|
||||
owner: string,
|
||||
name: string,
|
||||
): Promise<DetailsProviderResponse | undefined> {
|
||||
try {
|
||||
const response = await api.rest.repos.get({
|
||||
owner,
|
||||
repo: name,
|
||||
});
|
||||
|
||||
return {
|
||||
projectUrl: response.data.html_url,
|
||||
projectDescription: response.data.description ?? undefined,
|
||||
isFork: response.data.fork,
|
||||
isArchived: response.data.archived,
|
||||
createdAt: new Date(response.data.created_at),
|
||||
starsCount: response.data.stargazers_count,
|
||||
openIssues: response.data.open_issues_count,
|
||||
forksCount: response.data.forks_count,
|
||||
};
|
||||
} catch (error) {
|
||||
localLogger.warn(`Failed to get details for ${owner}\\${name} with Github integration`, {
|
||||
owner,
|
||||
name,
|
||||
error: error instanceof RequestError ? error.message : String(error),
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
return new Octokit({
|
||||
baseUrl: this.url("/").origin,
|
||||
request: {
|
||||
fetch: fetchWithTrustedCertificatesAsync,
|
||||
},
|
||||
userAgent: GithubIntegration.userAgent,
|
||||
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
|
||||
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { Gitlab as CoreGitlab } from "@gitbeaker/core";
|
||||
import { createRequesterFn, defaultOptionsHandler } from "@gitbeaker/requester-utils";
|
||||
import type { FormattedResponse, RequestOptions, ResourceOptions } from "@gitbeaker/requester-utils";
|
||||
import { Gitlab } from "@gitbeaker/rest";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import type {
|
||||
DetailsProviderResponse,
|
||||
ReleaseProviderResponse,
|
||||
ReleasesRepository,
|
||||
ReleasesResponse,
|
||||
} from "../interfaces/releases-providers/releases-providers-types";
|
||||
|
||||
const localLogger = logger.child({ module: "GitlabIntegration" });
|
||||
|
||||
export class GitlabIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/v4/projects"), {
|
||||
headers: {
|
||||
...(this.hasSecretValue("personalAccessToken")
|
||||
? { Authorization: `Bearer ${this.getSecretValue("personalAccessToken")}` }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
||||
const api = this.getApi();
|
||||
|
||||
const details = await this.getDetailsAsync(api, repository.identifier);
|
||||
|
||||
try {
|
||||
const releasesResponse = await api.ProjectReleases.all(repository.identifier, {
|
||||
perPage: 100,
|
||||
});
|
||||
|
||||
if (releasesResponse instanceof Error) {
|
||||
localLogger.warn(`Failed to get releases for ${repository.identifier} with Gitlab integration`, {
|
||||
identifier: repository.identifier,
|
||||
error: releasesResponse.message,
|
||||
});
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "noReleasesFound" },
|
||||
};
|
||||
}
|
||||
|
||||
const releasesProviderResponse = releasesResponse.reduce<ReleaseProviderResponse[]>((acc, release) => {
|
||||
if (!release.released_at) return acc;
|
||||
|
||||
const releaseDate = new Date(release.released_at);
|
||||
|
||||
acc.push({
|
||||
latestRelease: release.name ?? release.tag_name,
|
||||
latestReleaseAt: releaseDate,
|
||||
releaseUrl: release._links.self,
|
||||
releaseDescription: release.description ?? undefined,
|
||||
isPreRelease: releaseDate > new Date(), // For upcoming releases the `released_at` will be set to the future (https://docs.gitlab.com/api/releases/#upcoming-releases). Gitbreaker doesn't currently support the `upcoming_release` field (https://github.com/jdalrymple/gitbeaker/issues/3730)
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return getLatestRelease(releasesProviderResponse, repository, details);
|
||||
} catch (error) {
|
||||
localLogger.warn(`Failed to get releases for ${repository.identifier} with Gitlab integration`, {
|
||||
identifier: repository.identifier,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { code: "noReleasesFound" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async getDetailsAsync(api: CoreGitlab, identifier: string): Promise<DetailsProviderResponse | undefined> {
|
||||
try {
|
||||
const response = await api.Projects.show(identifier);
|
||||
|
||||
if (response instanceof Error) {
|
||||
localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, {
|
||||
identifier,
|
||||
error: response.message,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!response.web_url) {
|
||||
localLogger.warn(`No web URL found for ${identifier} with Gitlab integration`, {
|
||||
identifier,
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
projectUrl: response.web_url,
|
||||
projectDescription: response.description,
|
||||
isFork: response.forked_from_project !== null,
|
||||
isArchived: response.archived,
|
||||
createdAt: new Date(response.created_at),
|
||||
starsCount: response.star_count,
|
||||
openIssues: response.open_issues_count,
|
||||
forksCount: response.forks_count,
|
||||
};
|
||||
} catch (error) {
|
||||
localLogger.warn(`Failed to get details for ${identifier} with Gitlab integration`, {
|
||||
identifier,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getApi() {
|
||||
return new Gitlab({
|
||||
host: this.url("/").origin,
|
||||
requesterFn: createRequesterFn(
|
||||
async (serviceOptions: ResourceOptions, _: RequestOptions) => await defaultOptionsHandler(serviceOptions),
|
||||
async (endpoint: string, options?: Record<string, unknown>): Promise<FormattedResponse> => {
|
||||
if (options === undefined) {
|
||||
throw new Error("Gitlab library is not configured correctly. Options must be provided.");
|
||||
}
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(
|
||||
`${options.prefixUrl as string}${endpoint}`,
|
||||
options,
|
||||
);
|
||||
const headers = Object.fromEntries(response.headers.entries());
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
headers,
|
||||
body: await response.json(),
|
||||
} as FormattedResponse;
|
||||
},
|
||||
),
|
||||
...(this.hasSecretValue("personalAccessToken") ? { token: this.getSecretValue("personalAccessToken") } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export type { IntegrationInput } from "./base/integration";
|
||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||
|
||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||
@@ -37,6 +38,7 @@ export type {
|
||||
TdarrStatistics,
|
||||
TdarrWorker,
|
||||
} from "./interfaces/media-transcoding/media-transcoding-types";
|
||||
export type { ReleasesResponse } from "./interfaces/releases-providers/releases-providers-types";
|
||||
export type { Notification } from "./interfaces/notifications/notification-types";
|
||||
|
||||
// Schemas
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import type {
|
||||
DetailsProviderResponse,
|
||||
ReleaseProviderResponse,
|
||||
ReleasesRepository,
|
||||
ReleasesResponse,
|
||||
} from "./releases-providers-types";
|
||||
|
||||
export const getLatestRelease = (
|
||||
releases: ReleaseProviderResponse[],
|
||||
repository: ReleasesRepository,
|
||||
details?: DetailsProviderResponse,
|
||||
): ReleasesResponse => {
|
||||
const validReleases = releases.filter((result) => {
|
||||
if (result.latestRelease) {
|
||||
return repository.versionRegex ? new RegExp(repository.versionRegex).test(result.latestRelease) : true;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const latest =
|
||||
validReleases.length === 0
|
||||
? ({
|
||||
id: repository.id,
|
||||
error: { code: "noMatchingVersion" },
|
||||
} as ReleasesResponse)
|
||||
: validReleases.reduce(
|
||||
(latest, result) => {
|
||||
return {
|
||||
...details,
|
||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
||||
id: repository.id,
|
||||
};
|
||||
},
|
||||
{
|
||||
id: "",
|
||||
latestRelease: "",
|
||||
latestReleaseAt: new Date(0),
|
||||
},
|
||||
);
|
||||
|
||||
return latest;
|
||||
};
|
||||
|
||||
export interface ReleasesProviderIntegration {
|
||||
getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse>;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { TranslationObject } from "@homarr/translation";
|
||||
|
||||
export interface DetailsProviderResponse {
|
||||
projectUrl?: string;
|
||||
projectDescription?: string;
|
||||
isFork?: boolean;
|
||||
isArchived?: boolean;
|
||||
createdAt?: Date;
|
||||
starsCount?: number;
|
||||
openIssues?: number;
|
||||
forksCount?: number;
|
||||
}
|
||||
|
||||
export interface ReleaseProviderResponse {
|
||||
latestRelease: string;
|
||||
latestReleaseAt: Date;
|
||||
releaseUrl?: string;
|
||||
releaseDescription?: string;
|
||||
isPreRelease?: boolean;
|
||||
}
|
||||
|
||||
export interface ReleasesRepository {
|
||||
id: string;
|
||||
identifier: string;
|
||||
versionRegex?: string;
|
||||
}
|
||||
|
||||
type ReleasesErrorKeys = keyof TranslationObject["widget"]["releases"]["error"]["messages"];
|
||||
|
||||
export interface ReleasesResponse {
|
||||
id: string;
|
||||
latestRelease?: string;
|
||||
latestReleaseAt?: Date;
|
||||
releaseUrl?: string;
|
||||
releaseDescription?: string;
|
||||
isPreRelease?: boolean;
|
||||
projectUrl?: string;
|
||||
projectDescription?: string;
|
||||
isFork?: boolean;
|
||||
isArchived?: boolean;
|
||||
createdAt?: Date;
|
||||
starsCount?: number;
|
||||
openIssues?: number;
|
||||
forksCount?: number;
|
||||
|
||||
error?:
|
||||
| {
|
||||
code: ReleasesErrorKeys;
|
||||
}
|
||||
| {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
|
||||
import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
|
||||
import { releasesResponseSchema } from "./npm-schemas";
|
||||
|
||||
export class NPMIntegration extends Integration implements ReleasesProviderIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/"));
|
||||
|
||||
if (!response.ok) {
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
|
||||
const releasesResponse = await fetchWithTrustedCertificatesAsync(
|
||||
this.url(`/${encodeURIComponent(repository.identifier)}`),
|
||||
);
|
||||
|
||||
if (!releasesResponse.ok) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: { message: releasesResponse.statusText },
|
||||
};
|
||||
}
|
||||
|
||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
id: repository.id,
|
||||
error: {
|
||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : error.message,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const formattedReleases = data.time.map((tag) => ({
|
||||
...tag,
|
||||
releaseUrl: `https://www.npmjs.com/package/${encodeURIComponent(data.name)}/v/${encodeURIComponent(tag.latestRelease)}`,
|
||||
releaseDescription: data.versions[tag.latestRelease]?.description ?? "",
|
||||
}));
|
||||
return getLatestRelease(formattedReleases, repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const releasesResponseSchema = z.object({
|
||||
time: z.record(z.string().transform((value) => new Date(value))).transform((version) =>
|
||||
Object.entries(version).map(([key, value]) => ({
|
||||
latestRelease: key,
|
||||
latestReleaseAt: value,
|
||||
})),
|
||||
),
|
||||
versions: z.record(z.object({ description: z.string() })),
|
||||
name: z.string(),
|
||||
});
|
||||
@@ -155,7 +155,7 @@ export class OpenMediaVaultIntegration extends Integration implements ISystemHea
|
||||
return response;
|
||||
}
|
||||
|
||||
localLogger.info("Session expired, getting new session", { integrationId: this.integration.id });
|
||||
localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id });
|
||||
}
|
||||
|
||||
const session = await this.getSessionAsync();
|
||||
|
||||
@@ -128,7 +128,7 @@ export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIn
|
||||
return response;
|
||||
}
|
||||
|
||||
localLogger.info("Session expired, getting new session", { integrationId: this.integration.id });
|
||||
localLogger.debug("Session expired, getting new session", { integrationId: this.integration.id });
|
||||
}
|
||||
|
||||
const sessionId = await this.getSessionAsync();
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
"ioredis": "5.6.1",
|
||||
"superjson": "2.2.2",
|
||||
"winston": "3.17.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -33,19 +33,19 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.3.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,15 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.1.2",
|
||||
"@mantine/hooks": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@mantine/hooks": "^8.1.3",
|
||||
"react": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^8.1.2",
|
||||
"@mantine/notifications": "^8.1.3",
|
||||
"@tabler/icons-react": "^3.34.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,14 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^8.1.2",
|
||||
"@mantine/hooks": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@mantine/hooks": "^8.1.3",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.3.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"zod": "^3.25.74",
|
||||
"zod": "^3.25.76",
|
||||
"zod-form-data": "^2.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,7 +52,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -23,13 +23,13 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"zod": "^3.25.74"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [...baseConfig];
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./src";
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "@homarr/ping",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { fetch } from "undici";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { extractErrorMessage } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
export const sendPingRequestAsync = async (url: string) => {
|
||||
try {
|
||||
return await fetchWithTimeoutAndCertificates(url).then((response) => ({ statusCode: response.status }));
|
||||
} catch (error) {
|
||||
logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error }));
|
||||
return {
|
||||
error: extractErrorMessage(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as fetch, but with a timeout of 10 seconds.
|
||||
* Also respects certificates.
|
||||
* https://stackoverflow.com/questions/46946380/fetch-api-request-timeout
|
||||
* @param param0 fetch arguments
|
||||
* @returns fetch response
|
||||
*/
|
||||
export const fetchWithTimeoutAndCertificates = (...[url, requestInit]: Parameters<typeof fetch>) => {
|
||||
const controller = new AbortController();
|
||||
|
||||
// 10 seconds timeout:
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
return fetchWithTrustedCertificatesAsync(url, { signal: controller.signal, ...requestInit }).finally(() => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,6 @@ export {
|
||||
createGetSetChannel,
|
||||
} from "./lib/channel";
|
||||
|
||||
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
|
||||
export const pingChannel = createSubPubChannel<{ url: string; statusCode: number } | { url: string; error: string }>(
|
||||
"ping",
|
||||
);
|
||||
export const pingUrlChannel = createListChannel<string>("ping-url");
|
||||
|
||||
export const homeAssistantEntityState = createSubPubChannel<{
|
||||
|
||||
@@ -31,13 +31,14 @@
|
||||
"@homarr/redis": "workspace:^0.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"octokit": "^5.0.3",
|
||||
"superjson": "2.2.2"
|
||||
"superjson": "2.2.2",
|
||||
"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",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import { fetch } from "undici";
|
||||
|
||||
import { extractErrorMessage } from "@homarr/common";
|
||||
import { LoggingAgent } from "@homarr/common/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
type PingResponse =
|
||||
| {
|
||||
statusCode: number;
|
||||
durationMs: number;
|
||||
}
|
||||
| {
|
||||
error: string;
|
||||
};
|
||||
export const pingRequestHandler = createCachedWidgetRequestHandler<PingResponse, "app", { url: string }>({
|
||||
queryKey: "pingResult",
|
||||
widgetKind: "app",
|
||||
async requestAsync(input) {
|
||||
return await sendPingRequestAsync(input.url);
|
||||
},
|
||||
cacheDuration: dayjs.duration(1, "minute"),
|
||||
});
|
||||
|
||||
const sendPingRequestAsync = async (url: string) => {
|
||||
try {
|
||||
const start = performance.now();
|
||||
return await fetch(url, {
|
||||
dispatcher: new LoggingAgent({
|
||||
connect: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
}),
|
||||
}).then((response) => {
|
||||
const end = performance.now();
|
||||
logger.debug(`Ping request succeeded url="${url}" status="${response.status}" duration="${end - start}ms"`);
|
||||
return { statusCode: response.status, durationMs: end - start };
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(new Error(`Failed to send ping request to url="${url}"`, { cause: error }));
|
||||
return {
|
||||
error: extractErrorMessage(error),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1,304 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface ReleasesProvider {
|
||||
getDetailsUrl: (identifier: string) => string | undefined;
|
||||
parseDetailsResponse: (response: unknown) => z.SafeParseReturnType<unknown, DetailsResponse> | undefined;
|
||||
getReleasesUrl: (identifier: string) => string;
|
||||
parseReleasesResponse: (response: unknown) => z.SafeParseReturnType<unknown, ReleasesResponse[]>;
|
||||
}
|
||||
|
||||
interface ProvidersProps {
|
||||
[key: string]: ReleasesProvider;
|
||||
DockerHub: ReleasesProvider;
|
||||
Github: ReleasesProvider;
|
||||
Gitlab: ReleasesProvider;
|
||||
Npm: ReleasesProvider;
|
||||
Codeberg: ReleasesProvider;
|
||||
}
|
||||
|
||||
export const Providers: ProvidersProps = {
|
||||
DockerHub: {
|
||||
getDetailsUrl(identifier) {
|
||||
if (identifier.indexOf("/") > 0) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://hub.docker.com/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`;
|
||||
} else {
|
||||
return `https://hub.docker.com/v2/repositories/library/${encodeURIComponent(identifier)}`;
|
||||
}
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
name: z.string(),
|
||||
namespace: z.string(),
|
||||
description: z.string(),
|
||||
star_count: z.number(),
|
||||
date_registered: z.string().transform((value) => new Date(value)),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
|
||||
projectDescription: resp.description,
|
||||
createdAt: resp.date_registered,
|
||||
starsCount: resp.star_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/tags?page_size=200`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
results: z.array(
|
||||
z
|
||||
.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) })
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.name,
|
||||
latestReleaseAt: tag.last_updated,
|
||||
})),
|
||||
),
|
||||
})
|
||||
.transform((resp) => {
|
||||
return resp.results;
|
||||
})
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Github: {
|
||||
getDetailsUrl(identifier) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
html_url: z.string(),
|
||||
description: z.string().nullable(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
stargazers_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.html_url,
|
||||
projectDescription: resp.description ?? undefined,
|
||||
isFork: resp.fork,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.stargazers_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
html_url: z.string(),
|
||||
body: z.string().nullable(),
|
||||
prerelease: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.html_url,
|
||||
releaseDescription: tag.body ?? undefined,
|
||||
isPreRelease: tag.prerelease,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Gitlab: {
|
||||
getDetailsUrl(identifier) {
|
||||
return `https://gitlab.com/api/v4/projects/${encodeURIComponent(identifier)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
web_url: z.string(),
|
||||
description: z.string(),
|
||||
forked_from_project: z.object({ id: z.number() }).optional(),
|
||||
archived: z.boolean().optional(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
star_count: z.number(),
|
||||
open_issues_count: z.number().optional(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.web_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.forked_from_project !== undefined,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.star_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string(),
|
||||
released_at: z.string().transform((value) => new Date(value)),
|
||||
description: z.string(),
|
||||
_links: z.object({ self: z.string() }),
|
||||
upcoming_release: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.name,
|
||||
latestReleaseAt: tag.released_at,
|
||||
releaseUrl: tag._links.self,
|
||||
releaseDescription: tag.description,
|
||||
isPreRelease: tag.upcoming_release,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Npm: {
|
||||
getDetailsUrl(_) {
|
||||
return undefined;
|
||||
},
|
||||
parseDetailsResponse(_) {
|
||||
return undefined;
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `https://registry.npmjs.org/${encodeURIComponent(identifier)}`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
time: z.record(z.string().transform((value) => new Date(value))).transform((version) =>
|
||||
Object.entries(version).map(([key, value]) => ({
|
||||
identifier: "",
|
||||
latestRelease: key,
|
||||
latestReleaseAt: value,
|
||||
})),
|
||||
),
|
||||
versions: z.record(z.object({ description: z.string() })),
|
||||
name: z.string(),
|
||||
})
|
||||
.transform((resp) => {
|
||||
return resp.time.map((release) => ({
|
||||
...release,
|
||||
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
|
||||
releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
|
||||
}));
|
||||
})
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Codeberg: {
|
||||
getDetailsUrl(identifier) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://codeberg.org/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
html_url: z.string(),
|
||||
description: z.string(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
stars_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.html_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.fork,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.stars_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
url: z.string(),
|
||||
body: z.string(),
|
||||
prerelease: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.url,
|
||||
releaseDescription: tag.body,
|
||||
isPreRelease: tag.prerelease,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const _detailsSchema = z
|
||||
.object({
|
||||
projectUrl: z.string().optional(),
|
||||
projectDescription: z.string().optional(),
|
||||
isFork: z.boolean().optional(),
|
||||
isArchived: z.boolean().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
starsCount: z.number().optional(),
|
||||
openIssues: z.number().optional(),
|
||||
forksCount: z.number().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const _releasesSchema = z.object({
|
||||
latestRelease: z.string(),
|
||||
latestReleaseAt: z.date(),
|
||||
releaseUrl: z.string().optional(),
|
||||
releaseDescription: z.string().optional(),
|
||||
isPreRelease: z.boolean().optional(),
|
||||
error: z
|
||||
.object({
|
||||
code: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type DetailsResponse = z.infer<typeof _detailsSchema>;
|
||||
|
||||
export type ReleasesResponse = z.infer<typeof _releasesSchema>;
|
||||
@@ -1,122 +1,37 @@
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { ReleasesResponse } from "@homarr/integrations";
|
||||
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
import { Providers } from "./releases-providers";
|
||||
import type { DetailsResponse } from "./releases-providers";
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
const errorSchema = z.object({
|
||||
code: z.string().optional(),
|
||||
message: z.string().optional(),
|
||||
});
|
||||
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
|
||||
ReleasesResponse,
|
||||
IntegrationKindByCategory<"releasesProvider">,
|
||||
{
|
||||
id: string;
|
||||
identifier: string;
|
||||
versionRegex?: string;
|
||||
}
|
||||
>({
|
||||
async requestAsync(integration, input) {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
const response = await integrationInstance.getLatestMatchingReleaseAsync({
|
||||
id: input.id,
|
||||
identifier: input.identifier,
|
||||
versionRegex: input.versionRegex,
|
||||
});
|
||||
|
||||
type ReleasesError = z.infer<typeof errorSchema>;
|
||||
|
||||
const _reponseSchema = z.object({
|
||||
identifier: z.string(),
|
||||
providerKey: z.string(),
|
||||
latestRelease: z.string().optional(),
|
||||
latestReleaseAt: z.date().optional(),
|
||||
releaseUrl: z.string().optional(),
|
||||
releaseDescription: z.string().optional(),
|
||||
isPreRelease: z.boolean().optional(),
|
||||
projectUrl: z.string().optional(),
|
||||
projectDescription: z.string().optional(),
|
||||
isFork: z.boolean().optional(),
|
||||
isArchived: z.boolean().optional(),
|
||||
createdAt: z.date().optional(),
|
||||
starsCount: z.number().optional(),
|
||||
openIssues: z.number().optional(),
|
||||
forksCount: z.number().optional(),
|
||||
error: errorSchema.optional(),
|
||||
});
|
||||
|
||||
const formatErrorRelease = (identifier: string, providerKey: string, error: ReleasesError) => ({
|
||||
identifier,
|
||||
providerKey,
|
||||
latestRelease: undefined,
|
||||
latestReleaseAt: undefined,
|
||||
releaseUrl: undefined,
|
||||
releaseDescription: undefined,
|
||||
isPreRelease: undefined,
|
||||
projectUrl: undefined,
|
||||
projectDescription: undefined,
|
||||
isFork: undefined,
|
||||
isArchived: undefined,
|
||||
createdAt: undefined,
|
||||
starsCount: undefined,
|
||||
openIssues: undefined,
|
||||
forksCount: undefined,
|
||||
error,
|
||||
});
|
||||
|
||||
export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
queryKey: "releasesApiResult",
|
||||
widgetKind: "releases",
|
||||
async requestAsync(input: { providerKey: string; identifier: string; versionRegex: string | undefined }) {
|
||||
const provider = Providers[input.providerKey];
|
||||
|
||||
if (!provider) return undefined;
|
||||
|
||||
let detailsResult: DetailsResponse;
|
||||
const detailsUrl = provider.getDetailsUrl(input.identifier);
|
||||
if (detailsUrl !== undefined) {
|
||||
const detailsResponse = await fetchWithTimeout(detailsUrl);
|
||||
const parsedDetails = provider.parseDetailsResponse(await detailsResponse.json());
|
||||
|
||||
if (parsedDetails?.success) {
|
||||
detailsResult = parsedDetails.data;
|
||||
} else {
|
||||
detailsResult = undefined;
|
||||
logger.warn(`Failed to parse details response for ${input.identifier} on ${input.providerKey}`, {
|
||||
provider: input.providerKey,
|
||||
identifier: input.identifier,
|
||||
detailsUrl,
|
||||
error: parsedDetails?.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
|
||||
const releasesResponseJson: unknown = await releasesResponse.json();
|
||||
const releasesResult = provider.parseReleasesResponse(releasesResponseJson);
|
||||
|
||||
if (!releasesResult.success) {
|
||||
return formatErrorRelease(input.identifier, input.providerKey, {
|
||||
message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
|
||||
});
|
||||
} else {
|
||||
const releases = releasesResult.data.filter((result) =>
|
||||
input.versionRegex && result.latestRelease ? new RegExp(input.versionRegex).test(result.latestRelease) : true,
|
||||
);
|
||||
|
||||
const latest =
|
||||
releases.length === 0
|
||||
? formatErrorRelease(input.identifier, input.providerKey, { code: "noMatchingVersion" })
|
||||
: releases.reduce(
|
||||
(latest, result) => {
|
||||
return {
|
||||
...detailsResult,
|
||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
||||
identifier: input.identifier,
|
||||
providerKey: input.providerKey,
|
||||
};
|
||||
},
|
||||
{
|
||||
identifier: "",
|
||||
providerKey: "",
|
||||
latestRelease: "",
|
||||
latestReleaseAt: new Date(0),
|
||||
},
|
||||
);
|
||||
|
||||
return latest;
|
||||
}
|
||||
return {
|
||||
...response,
|
||||
integration: {
|
||||
name: integration.name,
|
||||
iconUrl: getIconUrl(integration.kind),
|
||||
},
|
||||
};
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
queryKey: "repositoriesReleases",
|
||||
});
|
||||
|
||||
export type ReleaseResponse = z.infer<typeof _reponseSchema>;
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.2",
|
||||
"@mantine/dates": "^8.1.3",
|
||||
"next": "15.3.5",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
@@ -35,7 +35,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -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.2",
|
||||
"@mantine/hooks": "^8.1.2",
|
||||
"@mantine/spotlight": "^8.1.2",
|
||||
"@mantine/core": "^8.1.3",
|
||||
"@mantine/hooks": "^8.1.3",
|
||||
"@mantine/spotlight": "^8.1.3",
|
||||
"@tabler/icons-react": "^3.34.0",
|
||||
"jotai": "^2.12.5",
|
||||
"next": "15.3.5",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "",
|
||||
"width": "",
|
||||
"height": ""
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "领域",
|
||||
"newLabel": "新领域"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "主题",
|
||||
"newLabel": "新主题"
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "行数",
|
||||
"width": "宽度",
|
||||
"height": "高度"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": "示例"
|
||||
},
|
||||
"invalid": "仓库定义无效,请检查值"
|
||||
"invalid": "仓库定义无效,请检查值",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "未找到",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "已创建",
|
||||
"error": {
|
||||
"label": "错误",
|
||||
"options": {
|
||||
"noMatchingVersion": "没有找到匹配的版本"
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "没有找到匹配的版本",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "图标颜色"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "自定义此面板的 css",
|
||||
"description": "只推荐有经验的用户使用 CSS 自定义面板",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Řádky",
|
||||
"width": "Šířka",
|
||||
"height": "Výška"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Dále si můžete přizpůsobit ovládací panel pomocí CSS, doporučujeme pouze zkušeným uživatelům",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Realm",
|
||||
"newLabel": "Nyt realm"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "Emne",
|
||||
"newLabel": "Nyt emne"
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Rækker",
|
||||
"width": "Bredde",
|
||||
"height": "Højde"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "indlejret dokument (iframe)",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": "Eksempel"
|
||||
},
|
||||
"invalid": "Ugyldig repository definition, tjek venligst værdierne"
|
||||
"invalid": "Ugyldig repository definition, tjek venligst værdierne",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "Ikke fundet",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "Oprettet",
|
||||
"error": {
|
||||
"label": "Fejl",
|
||||
"options": {
|
||||
"noMatchingVersion": "Ingen matchende version fundet"
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "Ingen matchende version fundet",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Ikonfarve"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "Brugerdefineret css for denne tavle",
|
||||
"description": "Tilpas din tavle ved hjælp af CSS, anbefales kun til erfarne brugere",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Bereich",
|
||||
"newLabel": "Neuer Bereich"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Zeilen",
|
||||
"width": "Breite",
|
||||
"height": "Höhe"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Symbolfarbe"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "Benutzerdefinierte css für dieses Board",
|
||||
"description": "Außerdem können Sie Ihr Dashboard mittels CSS anpassen, dies wird nur für erfahrene Benutzer empfohlen",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Bereich",
|
||||
"newLabel": "Neuer Bereich"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "Thema",
|
||||
"newLabel": "Neues Thema erstellen"
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Zeilen",
|
||||
"width": "Breite",
|
||||
"height": "Höhe"
|
||||
}
|
||||
},
|
||||
"placeholder": "Mit dem Schreiben von Notizen beginnen"
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": "Beispiel"
|
||||
},
|
||||
"invalid": "Ungültige Repository Definition, bitte überprüfen Sie die Werte"
|
||||
"invalid": "Ungültige Repository Definition, bitte überprüfen Sie die Werte",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "Nicht gefunden",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "Erstellt",
|
||||
"error": {
|
||||
"label": "Fehler",
|
||||
"options": {
|
||||
"noMatchingVersion": "Keine passende Version gefunden"
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "Keine passende Version gefunden",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Symbolfarbe"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "Benutzerdefinierte css für dieses Board",
|
||||
"description": "Außerdem können Sie Ihr Dashboard mittels CSS anpassen, dies wird nur für erfahrene Benutzer empfohlen",
|
||||
@@ -3078,7 +3094,7 @@
|
||||
"idle": "Inaktiv",
|
||||
"running": "Wird ausgeführt",
|
||||
"error": "Fehler",
|
||||
"disabled": ""
|
||||
"disabled": "Deaktiviert"
|
||||
},
|
||||
"job": {
|
||||
"minecraftServerStatus": {
|
||||
@@ -3143,18 +3159,18 @@
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
"seconds": "",
|
||||
"minutes": "",
|
||||
"hours": "",
|
||||
"midnight": "",
|
||||
"weeklyMonday": ""
|
||||
"seconds": "Alle(r) {interval, plural, =1 {Sekunde} other {# Sekunden}}",
|
||||
"minutes": "Alle(r) {interval, plural, one {}=1 {Minute} other {# Minuten}}",
|
||||
"hours": "Alle(r) {interval, plural, one {}=1 {Stunde} other {# Stunden}}",
|
||||
"midnight": "Jeden Tag um Mitternacht",
|
||||
"weeklyMonday": "Jede Woche am Montag"
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
"title": "Aufgabeneinstellungen für {jobName}"
|
||||
},
|
||||
"field": {
|
||||
"interval": {
|
||||
"label": ""
|
||||
"label": "Zeitplanintervall"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Γραμμές",
|
||||
"width": "Πλάτος",
|
||||
"height": "Ύψος"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iframe",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Περαιτέρω, προσαρμόστε τον πίνακα ελέγχου σας χρησιμοποιώντας CSS, συνιστάται μόνο για έμπειρους χρήστες",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Realm",
|
||||
"newLabel": "New realm"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "",
|
||||
"width": "",
|
||||
"height": ""
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Icon colour"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Realm",
|
||||
"newLabel": "New realm"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "Personal Access Token",
|
||||
"newLabel": "New Personal Access Token"
|
||||
},
|
||||
"topic": {
|
||||
"label": "Topic",
|
||||
"newLabel": "New topic"
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Rows",
|
||||
"width": "Width",
|
||||
"height": "Height"
|
||||
}
|
||||
},
|
||||
"placeholder": "Start writing your notes"
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": "Example"
|
||||
},
|
||||
"invalid": "Invalid repository definition, please check the values"
|
||||
"invalid": "Invalid repository definition, please check the values",
|
||||
"noProvider": {
|
||||
"label": "No Provider",
|
||||
"tooltip": "The provider could not be parsed, please manually set it after importing the images"
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "Not Found",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "Created",
|
||||
"error": {
|
||||
"label": "Error",
|
||||
"options": {
|
||||
"noMatchingVersion": "No matching version found"
|
||||
"messages": {
|
||||
"invalidIdentifier": "Invalid identifier",
|
||||
"noMatchingVersion": "No matching version found",
|
||||
"noReleasesFound": "No releases found",
|
||||
"noProviderSeleceted": "No provider selected",
|
||||
"noProviderResponse": "No response from provider"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Icon color"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": "Clear color"
|
||||
},
|
||||
"customCss": {
|
||||
"label": "Custom css for this board",
|
||||
"description": "Further, customize your dashboard using CSS, only recommended for experienced users",
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
"search": "",
|
||||
"field": {
|
||||
"name": "Nombre",
|
||||
"members": "",
|
||||
"members": "Miembros",
|
||||
"homeBoard": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Filas",
|
||||
"width": "Ancho",
|
||||
"height": "Alto"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Además, personaliza tu panel usando CSS, solo recomendado para usuarios avanzados",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "",
|
||||
"width": "",
|
||||
"height": ""
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "Domaine",
|
||||
"newLabel": "Nouveau domaine"
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Lignes",
|
||||
"width": "Largeur",
|
||||
"height": "Hauteur"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "Couleur de l'icône"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "CSS personnalisé pour ce tableau",
|
||||
"description": "En outre, vous pouvez personnaliser votre tableau de bord à l'aide de CSS. Réservé aux utilisateurs expérimentés.",
|
||||
|
||||
@@ -937,9 +937,13 @@
|
||||
"label": "תחום",
|
||||
"newLabel": "תחום חדש"
|
||||
},
|
||||
"topic": {
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "נושא",
|
||||
"newLabel": "נושא חדש"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "שורות",
|
||||
"width": "רוחב",
|
||||
"height": "גובה"
|
||||
}
|
||||
},
|
||||
"placeholder": "התחל לכתוב את ההערות שלך"
|
||||
},
|
||||
"iframe": {
|
||||
"name": "iFrame",
|
||||
@@ -2093,8 +2098,8 @@
|
||||
"processing": "מעבד",
|
||||
"partiallyAvailable": "חלקי",
|
||||
"available": "זמין",
|
||||
"blacklisted": "",
|
||||
"deleted": ""
|
||||
"blacklisted": "ברשימה השחורה",
|
||||
"deleted": "נמחק"
|
||||
},
|
||||
"status": {
|
||||
"pending": "ממתין",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": "דוגמא"
|
||||
},
|
||||
"invalid": "הגדרת מאגר לא חוקית, אנא בדוק את הערכים"
|
||||
"invalid": "הגדרת מאגר לא חוקית, אנא בדוק את הערכים",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "לא נמצא",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "נוצר",
|
||||
"error": {
|
||||
"label": "שגיאה",
|
||||
"options": {
|
||||
"noMatchingVersion": "לא נמצאה גרסה תואמת"
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "לא נמצאה גרסה תואמת",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2365,9 +2378,9 @@
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"noItems": "",
|
||||
"name": "התראות",
|
||||
"description": "הצגת היסטוריית התראות מאינטגרציה",
|
||||
"noItems": "אין התראות להצגה.",
|
||||
"option": {}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": "צבע סמל"
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "עיצוב מותאם אישית עבור הלוח הזה",
|
||||
"description": "יתר על כן, התאם את לוח המחוונים שלך באמצעות CSS, מומלץ רק למשתמשים מנוסים",
|
||||
@@ -3078,7 +3094,7 @@
|
||||
"idle": "ממתין",
|
||||
"running": "פועל",
|
||||
"error": "שגיאה",
|
||||
"disabled": ""
|
||||
"disabled": "מושבת"
|
||||
},
|
||||
"job": {
|
||||
"minecraftServerStatus": {
|
||||
@@ -3136,25 +3152,25 @@
|
||||
"label": "בקר רשת"
|
||||
},
|
||||
"refreshNotifications": {
|
||||
"label": ""
|
||||
"label": "עדכון התראות"
|
||||
},
|
||||
"dockerContainers": {
|
||||
"label": "מכולות דוקר"
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
"seconds": "",
|
||||
"minutes": "",
|
||||
"hours": "",
|
||||
"midnight": "",
|
||||
"weeklyMonday": ""
|
||||
"seconds": "כל {interval, plural, =1 {second} other {# seconds}}",
|
||||
"minutes": "כל {interval, plural, =1 {minute} other {# minutes}}",
|
||||
"hours": "כל {interval, plural, =1 {hour} other {# hours}}",
|
||||
"midnight": "כל יום בחצות",
|
||||
"weeklyMonday": "כל שבוע ביום שני"
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
"title": "הגדרות משימה עבור {jobName}"
|
||||
},
|
||||
"field": {
|
||||
"interval": {
|
||||
"label": ""
|
||||
"label": "מרווח זמן לתזמון"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "",
|
||||
"width": "Širina",
|
||||
"height": "Visina"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Dodatno, prilagodite svoju nadzornu ploču koristeći CSS, što se preporučuje samo iskusnim korisnicima",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Sorok",
|
||||
"width": "Szélesség",
|
||||
"height": "Magasság"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "Beágyazott keret (iFrame)",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Továbbá, testreszabhatja műszerfalát CSS segítségével, csak tapasztalt felhasználóknak ajánlott",
|
||||
|
||||
@@ -937,6 +937,10 @@
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"personalAccessToken": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
},
|
||||
"topic": {
|
||||
"label": "",
|
||||
"newLabel": ""
|
||||
@@ -1502,7 +1506,8 @@
|
||||
"rows": "Righe",
|
||||
"width": "Larghezza",
|
||||
"height": "Altezza"
|
||||
}
|
||||
},
|
||||
"placeholder": ""
|
||||
},
|
||||
"iframe": {
|
||||
"name": "",
|
||||
@@ -2287,7 +2292,11 @@
|
||||
"example": {
|
||||
"label": ""
|
||||
},
|
||||
"invalid": ""
|
||||
"invalid": "",
|
||||
"noProvider": {
|
||||
"label": "",
|
||||
"tooltip": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"not-found": "",
|
||||
@@ -2303,8 +2312,12 @@
|
||||
"created": "",
|
||||
"error": {
|
||||
"label": "",
|
||||
"options": {
|
||||
"noMatchingVersion": ""
|
||||
"messages": {
|
||||
"invalidIdentifier": "",
|
||||
"noMatchingVersion": "",
|
||||
"noReleasesFound": "",
|
||||
"noProviderSeleceted": "",
|
||||
"noProviderResponse": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2555,6 +2568,9 @@
|
||||
"iconColor": {
|
||||
"label": ""
|
||||
},
|
||||
"clearColor": {
|
||||
"label": ""
|
||||
},
|
||||
"customCss": {
|
||||
"label": "",
|
||||
"description": "Inoltre, personalizza la dashboard utilizzando i CSS, consigliato solo agli utenti esperti",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user