chore(release): automatic release v1.43.1

This commit is contained in:
homarr-releases[bot]
2025-10-31 19:13:48 +00:00
committed by GitHub
75 changed files with 2081 additions and 1265 deletions

View File

@@ -33,6 +33,7 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.43.0
- 1.42.1
- 1.42.0
- 1.41.0

View File

@@ -64,7 +64,7 @@ jobs:
- uses: actions/setup-node@v5
if: env.SKIP_RELEASE == 'false'
with:
node-version: 22.20.0
node-version: 24.10.0
cache: "pnpm"
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'

2
.nvmrc
View File

@@ -1 +1 @@
22.20.0
22.21.0

View File

@@ -1,4 +1,4 @@
FROM node:22.20.0-alpine AS base
FROM node:24.10.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat

View File

@@ -61,10 +61,10 @@
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-query-next-experimental": "^5.90.2",
"@trpc/client": "^11.6.0",
"@trpc/next": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/client": "^11.7.0",
"@trpc/next": "^11.7.0",
"@trpc/react-query": "^11.7.0",
"@trpc/server": "^11.7.0",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -84,7 +84,7 @@
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.93.2",
"superjson": "2.2.2",
"superjson": "2.2.3",
"swagger-ui-react": "^5.29.5",
"use-deep-compare-effect": "^1.8.1",
"zod": "^4.1.12"
@@ -94,7 +94,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.18.11",
"@types/node": "^24.9.1",
"@types/prismjs": "^1.26.5",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",

View File

@@ -40,14 +40,14 @@
"dayjs": "^1.11.18",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"superjson": "2.2.2",
"superjson": "2.2.3",
"undici": "7.16.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.18.11",
"@types/node": "^24.9.1",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.11",
"eslint": "^9.38.0",

View File

@@ -39,28 +39,28 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^13.0.1",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^11.0.6",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/github": "^12.0.0",
"@semantic-release/npm": "^13.1.1",
"@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.7.1",
"@testcontainers/redis": "^11.7.2",
"@turbo/gen": "^2.5.8",
"@vitejs/plugin-react": "^5.0.4",
"@vitejs/plugin-react": "^5.1.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.1.0",
"jsdom": "^27.0.1",
"prettier": "^3.6.2",
"semantic-release": "^24.2.9",
"testcontainers": "^11.7.1",
"semantic-release": "^25.0.1",
"testcontainers": "^11.7.2",
"turbo": "^2.5.8",
"typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.18.3",
"packageManager": "pnpm@10.19.0",
"engines": {
"node": ">=22.20.0"
"node": ">=22.21.0"
},
"pnpm": {
"onlyBuiltDependencies": [
@@ -82,7 +82,7 @@
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.25.11",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
"hono@<4.6.5": ">=4.10.2",
"hono@<4.6.5": ">=4.10.3",
"linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0",
@@ -93,7 +93,7 @@
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.1.11"
"vite@>=5.0.0 <=5.4.18": ">=7.1.12"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",

View File

@@ -26,7 +26,7 @@
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@umami/node": "^0.4.0",
"superjson": "2.2.2"
"superjson": "2.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -44,15 +44,15 @@
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.5",
"@trpc/client": "^11.6.0",
"@trpc/react-query": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"@trpc/client": "^11.7.0",
"@trpc/react-query": "^11.7.0",
"@trpc/server": "^11.7.0",
"@trpc/tanstack-react-query": "^11.7.0",
"lodash.clonedeep": "^4.5.0",
"next": "15.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"superjson": "2.2.2",
"superjson": "2.2.3",
"trpc-to-openapi": "^3.1.0",
"zod": "^4.1.12"
},

View File

@@ -40,15 +40,22 @@ export const releasesRouter = createTRPCRouter({
.query(async ({ ctx, input }) => {
return await Promise.all(
input.repositories.map(async (repository) => {
const innerHandler = releasesRequestHandler.handler(ctx.integration, {
id: repository.id,
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
});
const response = await releasesRequestHandler
.handler(ctx.integration, {
id: repository.id,
identifier: repository.identifier,
versionRegex: formatVersionFilterRegex(repository.versionFilter),
})
.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
return await innerHandler.getCachedOrUpdatedDataAsync({
forceUpdate: false,
});
return {
id: repository.id,
integration: { name: ctx.integration.name, kind: ctx.integration.kind },
timestamp: response.timestamp,
...response.data,
};
}),
);
}),

View File

@@ -23,8 +23,8 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.41.0",
"@auth/drizzle-adapter": "^1.11.0",
"@auth/core": "^0.41.1",
"@auth/drizzle-adapter": "^1.11.1",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
@@ -36,7 +36,7 @@
"cookies": "^0.9.1",
"ldapts": "8.0.9",
"next": "15.5.6",
"next-auth": "5.0.0-beta.29",
"next-auth": "5.0.0-beta.30",
"react": "19.2.0",
"react-dom": "19.2.0",
"zod": "^4.1.12"
@@ -46,7 +46,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.1",
"@types/cookies": "0.9.2",
"eslint": "^9.38.0",
"prettier": "^3.6.2",
"typescript": "^5.9.3"

View File

@@ -29,7 +29,7 @@
"dependencies": {
"@homarr/core": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.18",
"dns-caching": "^0.2.7",
"next": "15.5.6",

View File

@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.8.1",
"ioredis": "5.8.2",
"zod": "^4.1.12"
},
"devDependencies": {

View File

@@ -30,9 +30,9 @@
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.5",
"@trpc/client": "^11.6.0",
"@trpc/server": "^11.6.0",
"@trpc/tanstack-react-query": "^11.6.0",
"@trpc/client": "^11.7.0",
"@trpc/server": "^11.7.0",
"@trpc/tanstack-react-query": "^11.7.0",
"node-cron": "^4.2.1",
"react": "19.2.0",
"zod": "^4.1.12"

View File

@@ -43,24 +43,24 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@auth/core": "^0.41.0",
"@auth/core": "^0.41.1",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.5",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.7.1",
"@testcontainers/postgresql": "^11.7.1",
"@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.7.2",
"@testcontainers/postgresql": "^11.7.2",
"better-sqlite3": "^12.4.1",
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.5",
"drizzle-orm": "^0.44.6",
"drizzle-orm": "^0.44.7",
"drizzle-zod": "^0.8.3",
"mysql2": "3.15.2",
"mysql2": "3.15.3",
"pg": "^8.16.3",
"superjson": "2.2.2"
"superjson": "2.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.44",
"@types/dockerode": "^3.3.45",
"eslint": "^9.38.0",
"typescript": "^5.9.3"
}

View File

@@ -39,11 +39,11 @@
"@homarr/redis": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@jellyfin/sdk": "^0.11.0",
"@jellyfin/sdk": "^0.12.0",
"@octokit/auth-app": "^8.1.1",
"ical.js": "^2.2.1",
"maria2": "^0.4.1",
"node-ical": "^0.22.0",
"node-ical": "^0.22.1",
"octokit": "^5.0.4",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.5",

View File

@@ -11,8 +11,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
DetailsProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
import { detailsResponseSchema, releasesResponseSchema } from "./codeberg-schemas";
@@ -43,22 +42,23 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
private parseIdentifier(identifier: string) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with Codeberg integration`,
{
identifier: repository.identifier,
},
`Invalid identifier format. Expected 'owner/name', for ${identifier} with Codeberg integration`,
{ identifier },
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
return null;
}
return { owner, name };
}
const details = await this.getDetailsAsync(owner, name);
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return await fetchWithTrustedCertificatesAsync(
@@ -66,34 +66,36 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
{ headers },
);
});
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
success: false,
error: {
code: "unexpected",
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);
}
const formattedReleases = data.map((tag) => ({
latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at,
releaseUrl: tag.url,
releaseDescription: tag.body,
isPreRelease: tag.prerelease,
}));
const latestRelease = getLatestRelease(formattedReleases, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
const details = await this.getDetailsAsync(owner, name);
return { success: true, data: { ...details, ...latestRelease } };
}
protected async getDetailsAsync(owner: string, name: string): Promise<DetailsProviderResponse | undefined> {

View File

@@ -14,8 +14,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
DetailsProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
import { accessTokenResponseSchema, detailsResponseSchema, releasesResponseSchema } from "./docker-hub-schemas";
@@ -73,49 +72,61 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
};
}
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,
private parseIdentifier(identifier: string) {
if (!identifier.includes("/")) return { owner: "", name: identifier };
const [owner, name] = identifier.split("/");
if (!owner || !name) {
localLogger.warn(`Invalid identifier format. Expected 'owner/name' or 'name', for ${identifier} on DockerHub`, {
identifier,
});
});
return null;
}
return { owner, name };
}
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const relativeUrl: `/${string}` = owner
? `/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`
: `/v2/repositories/library/${encodeURIComponent(name)}`;
for (let page = 0; page <= 5; page++) {
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return await fetchWithTrustedCertificatesAsync(this.url(`${relativeUrl}/tags?page_size=100&page=${page}`), {
headers,
});
});
if (!releasesResponse.ok) {
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
}
const releasesResponseJson: unknown = await releasesResponse.json();
const releasesResult = releasesResponseSchema.safeParse(releasesResponseJson);
if (!releasesResult.success) {
return {
success: false,
error: {
code: "unexpected",
message: releasesResponseJson
? JSON.stringify(releasesResponseJson, null, 2)
: releasesResult.error.message,
},
};
}
const latestRelease = getLatestRelease(releasesResult.data.results, versionRegex);
if (!latestRelease) continue;
const details = await this.getDetailsAsync(relativeUrl);
return { success: true, data: { ...details, ...latestRelease } };
}
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);
}
return { success: false, error: { code: "noMatchingVersion" } };
}
private async getDetailsAsync(relativeUrl: `/${string}`): Promise<DetailsProviderResponse | undefined> {
@@ -154,18 +165,6 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
};
}
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",

View File

@@ -11,7 +11,7 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
import type { IMediaReleasesIntegration, MediaRelease } from "../types";
import type { IMediaReleasesIntegration, MediaRelease, MediaType } from "../types";
const sessionSchema = z.object({
NowPlayingItem: z
@@ -163,7 +163,7 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
return items.map((item) => ({
id: item.Id,
type: item.Type === "Movie" ? "movie" : item.Type === "Series" ? "tv" : "unknown",
type: this.mapMediaReleaseType(item.Type),
title: item.Name,
subtitle: item.Taglines.at(0),
description: item.Overview,
@@ -179,6 +179,27 @@ export class EmbyIntegration extends Integration implements IMediaServerIntegrat
}));
}
private mapMediaReleaseType(type: string | undefined): MediaType {
switch (type) {
case "Audio":
case "AudioBook":
case "MusicAlbum":
return "music";
case "Book":
return "book";
case "Episode":
case "Series":
case "Season":
return "tv";
case "Movie":
return "movie";
case "Video":
return "video";
default:
return "unknown";
}
}
// https://dev.emby.media/reference/RestAPI/UserService/getUsersPublic.html
private async fetchUsersPublicAsync(): Promise<{ id: string; name: string }[]> {
const apiKey = super.getSecretValue("apiKey");

View File

@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
import type {
DetailsProviderResponse,
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
@@ -43,23 +42,24 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
private parseIdentifier(identifier: string) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with GitHub Container Registry integration`,
{
identifier: repository.identifier,
},
`Invalid identifier format. Expected 'owner/name', for ${identifier} with GitHub Container Registry integration`,
{ identifier },
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
return null;
}
return { owner, name };
}
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const api = this.getApi();
const details = await this.getDetailsAsync(api, owner, name);
try {
const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
@@ -83,20 +83,20 @@ export class GitHubContainerRegistryIntegration extends Integration implements R
return acc;
}, []);
return getLatestRelease(releasesProviderResponse, repository, details);
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
const details = await this.getDetailsAsync(api, owner, name);
return { success: true, data: { ...details, ...latestRelease } };
} catch (error) {
const errorMessage = error instanceof RequestError ? error.message : String(error);
localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, {
owner,
name,
error: errorMessage,
});
return {
id: repository.id,
error: { message: errorMessage },
};
return { success: false, error: { code: "unexpected", message: errorMessage } };
}
}

View File

@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
import type {
DetailsProviderResponse,
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
const localLogger = logger.child({ module: "GithubIntegration" });
@@ -43,38 +42,32 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
private parseIdentifier(identifier: string) {
const [owner, name] = 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" },
};
localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Github integration`, {
identifier,
});
return null;
}
return { owner, name };
}
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const api = this.getApi();
const details = await this.getDetailsAsync(api, owner, name);
try {
const releasesResponse = await api.rest.repos.listReleases({
owner,
repo: name,
});
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,
localLogger.warn(`No releases found, for ${owner}/${name} with Github integration`, {
identifier: `${owner}/${name}`,
});
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
return { success: false, error: { code: "noMatchingVersion" } };
}
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
@@ -90,20 +83,20 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
return acc;
}, []);
return getLatestRelease(releasesProviderResponse, repository, details);
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
const details = await this.getDetailsAsync(api, owner, name);
return { success: true, data: { ...details, ...latestRelease } };
} catch (error) {
const errorMessage = error instanceof OctokitRequestError ? 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 },
};
return { success: false, error: { code: "unexpected", message: errorMessage } };
}
}

View File

@@ -15,8 +15,7 @@ import { getLatestRelease } from "../interfaces/releases-providers/releases-prov
import type {
DetailsProviderResponse,
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
const localLogger = logger.child({ module: "GitlabIntegration" });
@@ -40,25 +39,20 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const api = this.getApi();
const details = await this.getDetailsAsync(api, repository.identifier);
try {
const releasesResponse = await api.ProjectReleases.all(repository.identifier, {
const releasesResponse = await api.ProjectReleases.all(identifier, {
perPage: 100,
});
if (releasesResponse instanceof Error) {
localLogger.warn(`Failed to get releases for ${repository.identifier} with Gitlab integration`, {
identifier: repository.identifier,
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
identifier,
error: releasesResponse.message,
});
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
return { success: false, error: { code: "noReleasesFound" } };
}
const releasesProviderResponse = releasesResponse.reduce<ReleaseProviderResponse[]>((acc, release) => {
@@ -76,17 +70,19 @@ export class GitlabIntegration extends Integration implements ReleasesProviderIn
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),
});
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
const details = await this.getDetailsAsync(api, identifier);
return { success: true, data: { ...details, ...latestRelease } };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
localLogger.warn(`Failed to get releases for ${identifier} with Gitlab integration`, {
identifier,
error: errorMessage,
});
return { success: false, error: { code: "unexpected", message: errorMessage } };
}
}

View File

@@ -47,7 +47,7 @@ export type {
TdarrStatistics,
TdarrWorker,
} from "./interfaces/media-transcoding/media-transcoding-types";
export type { ReleasesResponse } from "./interfaces/releases-providers/releases-providers-types";
export type { ReleasesRepository, ReleaseResponse } from "./interfaces/releases-providers/releases-providers-types";
export type { Notification } from "./interfaces/notifications/notification-types";
// Schemas

View File

@@ -1,47 +1,21 @@
import type {
DetailsProviderResponse,
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
} from "./releases-providers-types";
import type { ReleaseProviderResponse, ReleaseResponse } from "./releases-providers-types";
export const getLatestRelease = (
releases: ReleaseProviderResponse[],
repository: ReleasesRepository,
details?: DetailsProviderResponse,
): ReleasesResponse => {
versionRegex?: string,
): ReleaseProviderResponse | null => {
const validReleases = releases.filter((result) => {
if (result.latestRelease) {
return repository.versionRegex ? new RegExp(repository.versionRegex).test(result.latestRelease) : true;
return versionRegex ? new RegExp(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;
return validReleases.length === 0
? null
: validReleases.reduce((latest, current) => (current.latestReleaseAt > latest.latestReleaseAt ? current : latest));
};
export interface ReleasesProviderIntegration {
getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse>;
getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse>;
}

View File

@@ -1,5 +1,11 @@
import type { TranslationObject } from "@homarr/translation";
export interface ReleasesRepository extends Record<string, unknown> {
id: string;
identifier: string;
versionRegex?: string;
}
export interface DetailsProviderResponse {
projectUrl?: string;
projectDescription?: string;
@@ -19,35 +25,10 @@ export interface ReleaseProviderResponse {
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;
export type ReleaseData = DetailsProviderResponse & ReleaseProviderResponse;
error?:
| {
code: ReleasesErrorKeys;
}
| {
message: string;
};
}
export type ReleaseError = { code: ReleasesErrorKeys } | { code: "unexpected"; message: string };
export type ReleaseResponse = { success: true; data: ReleaseData } | { success: false; error: ReleaseError };

View File

@@ -15,7 +15,7 @@ import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
import type { IMediaReleasesIntegration, MediaRelease } from "../types";
import type { IMediaReleasesIntegration, MediaRelease, MediaType } from "../types";
@HandleIntegrationErrors([integrationAxiosHttpErrorHandler])
export class JellyfinIntegration extends Integration implements IMediaServerIntegration, IMediaReleasesIntegration {
@@ -122,7 +122,7 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
return result.data.map((item) => ({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: item.Id!,
type: item.Type === "Movie" ? "movie" : item.Type === "Series" ? "tv" : "unknown",
type: this.mapMediaReleaseType(item.Type),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
title: item.Name!,
subtitle: item.Taglines?.at(0),
@@ -140,6 +140,27 @@ export class JellyfinIntegration extends Integration implements IMediaServerInte
}));
}
private mapMediaReleaseType(type: BaseItemKind | undefined): MediaType {
switch (type) {
case "Audio":
case "AudioBook":
case "MusicAlbum":
return "music";
case "Book":
return "book";
case "Episode":
case "Series":
case "Season":
return "tv";
case "Movie":
return "movie";
case "Video":
return "video";
default:
return "unknown";
}
}
/**
* Constructs an ApiClient synchronously with an ApiKey or asynchronously
* with a username and password.

View File

@@ -6,7 +6,7 @@ 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 type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
import type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./linuxserverio-schemas";
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
@@ -24,56 +24,44 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
private parseIdentifier(identifier: string) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
{
identifier: repository.identifier,
},
`Invalid identifier format. Expected 'owner/name', for ${identifier} with LinuxServerIO integration`,
{ identifier },
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
return null;
}
return { owner, name };
}
public async getLatestMatchingReleaseAsync(identifier: string): Promise<ReleaseResponse> {
const { name } = this.parseIdentifier(identifier) ?? {};
if (!name) return { success: false, error: { code: "invalidIdentifier" } };
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images"));
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
error: {
message: error.message,
},
};
} else {
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
if (!release) {
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
owner,
name,
});
return { success: false, error: { code: "unexpected", message: error.message } };
}
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
}
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
if (!release) {
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
name,
});
return { success: false, error: { code: "noMatchingVersion" } };
}
return {
id: repository.id,
return {
success: true,
data: {
latestRelease: release.version,
latestReleaseAt: release.version_timestamp,
releaseDescription: release.changelog?.shift()?.desc,
@@ -82,7 +70,7 @@ export class LinuxServerIOIntegration extends Integration implements ReleasesPro
isArchived: release.deprecated,
createdAt: release.initial_date ? new Date(release.initial_date) : undefined,
starsCount: release.stars,
};
}
},
};
}
}

View File

@@ -6,7 +6,7 @@ import { TestConnectionError } from "../base/test-connection/test-connection-err
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 type { ReleaseResponse } from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./npm-schemas";
export class NPMIntegration extends Integration implements ReleasesProviderIntegration {
@@ -22,35 +22,35 @@ export class NPMIntegration extends Integration implements ReleasesProviderInteg
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const releasesResponse = await fetchWithTrustedCertificatesAsync(
this.url(`/${encodeURIComponent(repository.identifier)}`),
);
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
if (!identifier) return { success: false, error: { code: "invalidIdentifier" } };
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url(`/${encodeURIComponent(identifier)}`));
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
success: false,
error: {
code: "unexpected",
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);
}
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 ?? "",
}));
const latestRelease = getLatestRelease(formattedReleases, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
return { success: true, data: latestRelease };
}
}

View File

@@ -11,8 +11,7 @@ import type { ReleasesProviderIntegration } from "../interfaces/releases-provide
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
ReleaseResponse,
} from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./quay-schemas";
@@ -43,20 +42,22 @@ export class QuayIntegration extends Integration implements ReleasesProviderInte
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
private parseIdentifier(identifier: string) {
const [owner, name] = identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
{
identifier: repository.identifier,
},
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
localLogger.warn(`Invalid identifier format. Expected 'owner/name', for ${identifier} with Quay integration`, {
identifier,
});
return null;
}
return { owner, name };
}
public async getLatestMatchingReleaseAsync(identifier: string, versionRegex?: string): Promise<ReleaseResponse> {
const parsedIdentifier = this.parseIdentifier(identifier);
if (!parsedIdentifier) return { success: false, error: { code: "invalidIdentifier" } };
const { owner, name } = parsedIdentifier;
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return await fetchWithTrustedCertificatesAsync(
@@ -68,42 +69,29 @@ export class QuayIntegration extends Integration implements ReleasesProviderInte
},
);
});
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
return { success: false, error: { code: "unexpected", message: releasesResponse.statusText } };
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
error: {
message: error.message,
},
};
} else {
const details = {
projectDescription: data.description,
};
const releasesProviderResponse = Object.entries(data.tags).reduce<ReleaseProviderResponse[]>((acc, [_, tag]) => {
if (!tag.name || !tag.last_modified) return acc;
acc.push({
latestRelease: tag.name,
latestReleaseAt: new Date(tag.last_modified),
releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`,
});
return acc;
}, []);
return getLatestRelease(releasesProviderResponse, repository, details);
return { success: false, error: { code: "unexpected", message: error.message } };
}
const releasesProviderResponse = Object.entries(data.tags).reduce<ReleaseProviderResponse[]>((acc, [_, tag]) => {
if (!tag.name || !tag.last_modified) return acc;
acc.push({
latestRelease: tag.name,
latestReleaseAt: new Date(tag.last_modified),
releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`,
});
return acc;
}, []);
const latestRelease = getLatestRelease(releasesProviderResponse, versionRegex);
if (!latestRelease) return { success: false, error: { code: "noMatchingVersion" } };
return { success: true, data: { projectDescription: data.description, ...latestRelease } };
}
}

View File

@@ -25,7 +25,7 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/core": "workspace:^0.1.0",
"superjson": "2.2.2",
"superjson": "2.2.3",
"winston": "3.18.3",
"zod": "^4.1.12"
},

View File

@@ -43,7 +43,7 @@
"next": "15.5.6",
"react": "19.2.0",
"react-dom": "19.2.0",
"superjson": "2.2.2",
"superjson": "2.2.3",
"zod": "^4.1.12",
"zod-form-data": "^3.0.1"
},

View File

@@ -27,8 +27,8 @@
"@homarr/db": "workspace:^",
"@homarr/definitions": "workspace:^",
"@homarr/log": "workspace:^",
"ioredis": "5.8.1",
"superjson": "2.2.2"
"ioredis": "5.8.2",
"superjson": "2.2.3"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -31,7 +31,7 @@
"@homarr/redis": "workspace:^0.1.0",
"dayjs": "^1.11.18",
"octokit": "^5.0.4",
"superjson": "2.2.2",
"superjson": "2.2.3",
"undici": "7.16.0",
"zod": "^4.1.12"
},

View File

@@ -1,36 +1,19 @@
import dayjs from "dayjs";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIconUrl } from "@homarr/definitions";
import type { ReleasesResponse } from "@homarr/integrations";
import type { ReleaseResponse, ReleasesRepository } from "@homarr/integrations";
import { createIntegrationAsync } from "@homarr/integrations";
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
export const releasesRequestHandler = createCachedIntegrationRequestHandler<
ReleasesResponse,
ReleaseResponse,
IntegrationKindByCategory<"releasesProvider">,
{
id: string;
identifier: string;
versionRegex?: string;
}
ReleasesRepository
>({
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
const response = await integrationInstance.getLatestMatchingReleaseAsync({
id: input.id,
identifier: input.identifier,
versionRegex: input.versionRegex,
});
return {
...response,
integration: {
name: integration.name,
iconUrl: getIconUrl(integration.kind),
},
};
requestAsync: async (integration, input) => {
const instance = await createIntegrationAsync(integration);
return instance.getLatestMatchingReleaseAsync(input.identifier, input.versionRegex);
},
cacheDuration: dayjs.duration(5, "minutes"),
queryKey: "repositoriesReleases",

View File

@@ -33,7 +33,7 @@
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.5.6",
"next-intl": "4.3.12",
"next-intl": "4.4.0",
"react": "19.2.0",
"react-dom": "19.2.0"
},

View File

@@ -645,6 +645,21 @@
"title": "Creació fallida",
"message": "No s'ha pogut crear la integració"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "No s'han pogut aplicar els canvis",
"message": "No s'ha pogut desar la integració"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Crea un motor de cerca",
"description": "La integració \"{kind}\" es pot utilitzar amb els motors de cerca. Seleccioneu aquesta opció per configurar el motor de cerca automàticament."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "",
"apply": "",
"backToOverview": "",
"change": "",
"create": "",
"createAnother": "",
"edit": "",

View File

@@ -645,6 +645,21 @@
"title": "创建失败",
"message": "无法创建组件"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "无法应用更改",
"message": "无法保存此集成"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "创建搜索引擎",
"description": "集成“{kind}”可以与搜索引擎一起使用。勾选此项可自动配置搜索引擎。"
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "创建应用",
"description": "创建一个具有与集成相同名称和图标的应用程序。 留空下面的输入字段使用集成URL创建应用程序。"
@@ -1026,6 +1051,7 @@
"add": "添加",
"apply": "应用",
"backToOverview": "返回概览",
"change": "",
"create": "创建",
"createAnother": "创建并重新开始",
"edit": "编辑",

View File

@@ -645,6 +645,21 @@
"title": "crwdns346:0crwdne346:0",
"message": "crwdns348:0crwdne348:0"
}
},
"app": {
"option": {
"existing": {
"title": "crwdns3786:0crwdne3786:0",
"label": "crwdns3788:0crwdne3788:0"
},
"new": {
"title": "crwdns3790:0crwdne3790:0",
"url": {
"label": "crwdns3792:0crwdne3792:0",
"description": "crwdns3794:0crwdne3794:0"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "crwdns356:0crwdne356:0",
"message": "crwdns358:0crwdne358:0"
}
},
"app": {
"action": {
"add": "crwdns3796:0crwdne3796:0",
"remove": "crwdns3798:0crwdne3798:0",
"select": "crwdns3800:0crwdne3800:0"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "crwdns2512:0crwdne2512:0",
"description": "crwdns2514:0{kind}crwdne2514:0"
},
"app": {
"sectionTitle": "crwdns3802:0crwdne3802:0"
},
"createApp": {
"label": "crwdns3050:0crwdne3050:0",
"description": "crwdns3052:0crwdne3052:0"
@@ -1026,6 +1051,7 @@
"add": "crwdns512:0crwdne512:0",
"apply": "crwdns514:0crwdne514:0",
"backToOverview": "crwdns516:0crwdne516:0",
"change": "crwdns3804:0crwdne3804:0",
"create": "crwdns518:0crwdne518:0",
"createAnother": "crwdns2720:0crwdne2720:0",
"edit": "crwdns520:0crwdne520:0",

View File

@@ -645,6 +645,21 @@
"title": "Vytvoření se nezdařilo",
"message": "Integraci se nepodařilo vytvořit"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Nepodařilo se uložit změny",
"message": "Integraci se nepodařilo uložit"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Přidat",
"apply": "Použít",
"backToOverview": "Zpět na přehled",
"change": "",
"create": "Vytvořit",
"createAnother": "",
"edit": "Upravit",

View File

@@ -645,6 +645,21 @@
"title": "Oprettelse mislykkedes",
"message": "Integration kunne ikke oprettes"
}
},
"app": {
"option": {
"existing": {
"title": "Eksisterende",
"label": "Vælg eksisterende app"
},
"new": {
"title": "Ny",
"url": {
"label": "App url",
"description": "Webadressen som app'en vil åbne, når den tilgås fra betjeningspanelet"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Kan ikke anvende ændringer",
"message": "Integration kunne ikke gemmes"
}
},
"app": {
"action": {
"add": "Link en app",
"remove": "Fjern link",
"select": "Vælg en app der skal linkes"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Opret Søgemaskine",
"description": "Integration \"{kind}\" kan bruges med søgemaskinerne. Markér dette for automatisk at konfigurere søgemaskinen."
},
"app": {
"sectionTitle": "Linket App"
},
"createApp": {
"label": "Opret app",
"description": "Opret en app med samme navn og ikon som integrationen. Lad input-feltet være tomt for at oprette app'en med integrations-URL'en."
@@ -1026,6 +1051,7 @@
"add": "Tilføj",
"apply": "Anvend",
"backToOverview": "Tilbage til oversigt",
"change": "Skift",
"create": "Opret",
"createAnother": "Opret og start forfra",
"edit": "Rediger",

View File

@@ -645,6 +645,21 @@
"title": "Erstellig isch fählgschlage",
"message": "D Integration het nid chönne erstäut werde"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Änderige chöi nöd aawendet werde",
"message": "D Integration het nöd chönne gspichert werde"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Suechmaschine erstelle",
"description": "Integration '{kind}' cha als Suechmaschine verwendet werde. Wähl das us, um se outomatisch als Suechmaschine z konfiguriere."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "App ersteue",
"description": "Mach en App mit de gleiche Name und dem gleiche Icon wie d'Integration. Lass d'Eingabefeld unger im Fall leer, um d'App mit em Integrations-URL z'schaffe."
@@ -1026,6 +1051,7 @@
"add": "Hinzufügen",
"apply": "Übernehmen",
"backToOverview": "Zurück zur Übersicht",
"change": "",
"create": "Erstellen",
"createAnother": "Erstellen und neu starten",
"edit": "Bearbeiten",

View File

@@ -645,6 +645,21 @@
"title": "Erstellung fehlgeschlagen",
"message": "Die Integration konnte nicht erstellt werden"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Änderungen konnten nicht angewendet werden",
"message": "Die Integration konnte nicht gespeichert werden"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Suchmaschinen anlegen",
"description": "Integration \"{kind}\" kann mit den Suchmaschinen verwendet werden. Wählen Sie dies, um die Suchmaschine automatisch zu konfigurieren."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "App erstellen",
"description": "Eine App mit dem gleichen Namen und Symbol wie die Integration erstellen. Lassen Sie das Eingabefeld unter leer, um die App mit der Integrations URL zu erstellen."
@@ -1026,6 +1051,7 @@
"add": "Hinzufügen",
"apply": "Übernehmen",
"backToOverview": "Zurück zur Übersicht",
"change": "",
"create": "Erstellen",
"createAnother": "Erstellen und neu starten",
"edit": "Bearbeiten",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Προσθήκη",
"apply": "Εφαρμογή",
"backToOverview": "",
"change": "",
"create": "Δημιουργία",
"createAnother": "",
"edit": "Επεξεργασία",

View File

@@ -612,17 +612,17 @@
"select": {
"label": "Select an application",
"notFound": "No application found",
"search": "",
"noResults": "",
"action": "",
"title": ""
"search": "Search for an app",
"noResults": "No results",
"action": "Select {app}",
"title": "Select an app to add to this board"
},
"create": {
"title": "",
"description": "",
"action": ""
"title": "Create new app",
"description": "Create a new app ",
"action": "Open app creation"
},
"add": ""
"add": "Add an app"
}
},
"integration": {
@@ -645,6 +645,21 @@
"title": "Creation failed",
"message": "The integration was unable to be created"
}
},
"app": {
"option": {
"existing": {
"title": "Existing",
"label": "Select existing app"
},
"new": {
"title": "New",
"url": {
"label": "App URL",
"description": "The URL the app will open when accessed from the dashboard"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Changes have not been applied",
"message": "The integration was unable to be saved"
}
},
"app": {
"action": {
"add": "Link an app",
"remove": "Unlink",
"select": "Select an app to link"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Create a search engine",
"description": "Integration, {kind} can be used with the search engine. Please check this to automatically configure the search engine."
},
"app": {
"sectionTitle": "Linked App"
},
"createApp": {
"label": "Create application",
"description": "Create an application with the same information as the integration. Please leave the input field below empty to create the application with the integration URL."
@@ -705,30 +730,30 @@
"error": {
"common": {
"cause": {
"title": ""
"title": "Cause with more details"
}
},
"unknown": {
"title": "",
"description": ""
"title": "Unknown error",
"description": "An unknown error occurred, open the cause below to see more details"
},
"parse": {
"title": "",
"description": ""
"title": "Parse error",
"description": "The response could not be parsed. Please verify that the URL is pointing to the base URL of the service."
},
"authorization": {
"title": "",
"description": ""
"title": "Authorisation error",
"description": "The request was not authorised. Please verify that the credentials are correct and you have them configured with enough permissions."
},
"statusCode": {
"title": "",
"description": "",
"otherDescription": "",
"title": "Response error",
"description": "Received unexpected {statusCode} ({reason}) response from <url></url>. Please verify that the URL is pointing to the base URL of the integration.",
"otherDescription": "Received unexpected {statusCode} response from <url></url>. Please verify that the URL is pointing to the base URL of the integration.",
"reason": {
"badRequest": "",
"notFound": "",
"tooManyRequests": "",
"internalServerError": "",
"badRequest": "Bad request",
"notFound": "Not found",
"tooManyRequests": "Too many requests",
"internalServerError": "Internal server error",
"serviceUnavailable": "",
"gatewayTimeout": ""
}
@@ -1026,6 +1051,7 @@
"add": "Add",
"apply": "Apply",
"backToOverview": "Back to overview",
"change": "",
"create": "Create",
"createAnother": "Create and start over",
"edit": "Edit",

View File

@@ -645,6 +645,21 @@
"title": "No se ha podido crear",
"message": "La integración no pudo ser creada"
}
},
"app": {
"option": {
"existing": {
"title": "Existente",
"label": "Seleccionar aplicación existente"
},
"new": {
"title": "Nueva",
"url": {
"label": "URL de aplicación",
"description": "La URL que abrirá la aplicación cuando se acceda desde el panel de control"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "No se pudieron aplicar los cambios",
"message": "La integración no pudo ser creada"
}
},
"app": {
"action": {
"add": "Vincular una aplicación",
"remove": "Desvincular",
"select": "Selecciona una aplicación para vincular"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Crear motor de búsqueda",
"description": "La integración \"{kind}\" se puede utilizar con los motores de búsqueda. Marque esta opción para configurar automáticamente el motor de búsqueda."
},
"app": {
"sectionTitle": "Aplicación vinculada"
},
"createApp": {
"label": "Crear aplicación",
"description": "Crea una aplicación con el mismo nombre e icono que la integración. Deja vacío el campo de entrada de abajo para crear la aplicación con la URL de la integración."
@@ -1026,6 +1051,7 @@
"add": "Añadir",
"apply": "Aplicar",
"backToOverview": "Volver a la vista general",
"change": "Modificar",
"create": "Crear",
"createAnother": "Crear e iniciar de nuevo",
"edit": "Editar",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "",
"apply": "",
"backToOverview": "",
"change": "",
"create": "",
"createAnother": "",
"edit": "",

View File

@@ -645,6 +645,21 @@
"title": "Échec de la création",
"message": "L'intégration n'a pas pu être créée"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Impossible d'appliquer les modifications",
"message": "L'intégration n'a pas pu être enregistrée"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Créer un moteur de recherche",
"description": "L'intégration \"{kind}\" peut être utilisée avec les moteurs de recherche. Cochez ceci pour configurer automatiquement le moteur de recherche."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "Créer une application",
"description": "Créer une application avec le même nom et l'icône que l'intégration. Laissez le champ de saisie ci-dessous vide pour créer l'application avec l'URL d'intégration."
@@ -1026,6 +1051,7 @@
"add": "Ajouter",
"apply": "Appliquer",
"backToOverview": "Retourner à l'aperçu",
"change": "",
"create": "Créer",
"createAnother": "Créer et recommencer",
"edit": "Modifier",

View File

@@ -645,6 +645,21 @@
"title": "היצירה נכשלה",
"message": "לא ניתן ליצור את האינטגרציה"
}
},
"app": {
"option": {
"existing": {
"title": "קיים",
"label": "בחירת אפליקציה קיימת"
},
"new": {
"title": "חדש",
"url": {
"label": "כתובת אתר של אפליקציה",
"description": "כתובת האתר שהאפליקציה תפתח בעת גישה אליה מלוח המחוונים"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "לא ניתן להחיל שינויים",
"message": "לא ניתן היה לשמור את האינטגרציה"
}
},
"app": {
"action": {
"add": "קישור אפליקציה",
"remove": "בטל קישור",
"select": "בחר אפליקציה לקישור"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "יצירת מנוע חיפוש",
"description": "אינטגרציה {kind} יכולה לשמש עם מנועי חיפוש. סמן עבור הגדרה אוטומטית של מנוע חיפוש."
},
"app": {
"sectionTitle": "אפליקציה מקושרת"
},
"createApp": {
"label": "יצירת אפליקציה",
"description": "יצירת אפליקציה עם אותו שם וסמל כמו האינטגרציה. יש להשאיר את שדה הקלט למטה ריק כדי ליצור את האפליקציה עם כתובת האתר לאינטגרציה."
@@ -946,8 +971,8 @@
"newLabel": "נושא חדש"
},
"url": {
"label": "",
"newLabel": ""
"label": "כתובת אתר",
"newLabel": "כתובת אתר חדשה"
},
"opnsenseApiKey": {
"label": "מפתח API (מפתח)",
@@ -1026,6 +1051,7 @@
"add": "הוסף",
"apply": "החל",
"backToOverview": "חזרה לסקירה כללית",
"change": "לשנות",
"create": "צור",
"createAnother": "צור והתחל מחדש",
"edit": "עריכה",
@@ -1148,8 +1174,8 @@
},
"unit": {
"speed": {
"kilometersPerHour": "",
"milesPerHour": ""
"kilometersPerHour": "קמ\"ש",
"milesPerHour": "מייל/שעה"
}
}
},
@@ -1164,7 +1190,7 @@
"label": "כותרת"
},
"customCssClasses": {
"label": ""
"label": "מחלקות עיצוב מותאמות אישית"
},
"borderColor": {
"label": "צבע מסגרת"
@@ -1717,7 +1743,7 @@
"name": "לוח שנה",
"description": "הצג אירועים מהאינטגרציות שלך בתצוגת לוח שנה בתוך פרק זמן יחסי מסוים",
"duration": {
"allDay": ""
"allDay": "כל היום"
},
"option": {
"releaseType": {
@@ -1754,7 +1780,7 @@
"description": "רק במזג אוויר נוכחי"
},
"useImperialSpeed": {
"label": ""
"label": "השתמש במייל לשעה עבור מהירות הרוח"
},
"location": {
"label": "מיקום מזג האוויר"
@@ -1994,21 +2020,21 @@
"name": "שם",
"id": "מזהה",
"metadata": {
"title": "",
"title": "סטטיסטיקות לחנונים",
"video": {
"title": "",
"resolution": ""
"title": "וידאו",
"resolution": "רזולוציה"
},
"audio": {
"title": "",
"channelCount": "",
"codec": ""
"title": "אודיו",
"channelCount": "ערוצי אודיו",
"codec": "מקודדי אודיו"
},
"transcoding": {
"title": "",
"container": "",
"resolution": "",
"target": ""
"title": "המרה",
"container": "מיכל",
"resolution": "רזולוציה",
"target": "קידוד יעד"
}
}
}
@@ -2537,7 +2563,7 @@
"description": "שימוש במעבד, זיכרון, דיסק וחומרה אחרת של המערכת שלך",
"option": {
"hasShadow": {
"label": ""
"label": "הפעלת הצללת תרשים"
},
"visibleCharts": {
"label": "תרשימים גלויים",
@@ -2549,12 +2575,12 @@
}
},
"labelDisplayMode": {
"label": "",
"label": "מצב תצוגת תוויות",
"option": {
"textWithIcon": "",
"text": "",
"icon": "",
"hidden": ""
"textWithIcon": "הצג טקסט עם סמל",
"text": "הצג רק טקסט",
"icon": "הצג רק אייקון",
"hidden": "הסתר תוויות"
}
}
},
@@ -2987,8 +3013,8 @@
"integration": "אינטגרציות",
"app": "אפליקציות",
"group": "קבוצות",
"searchEngine": "",
"media": ""
"searchEngine": "מנועי חיפוש",
"media": "מדיה"
},
"statisticLabel": {
"boards": "לוחות",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Dodaj",
"apply": "",
"backToOverview": "",
"change": "",
"create": "Stvoriti",
"createAnother": "",
"edit": "Uredi",

View File

@@ -645,6 +645,21 @@
"title": "A létrehozás nem sikerült",
"message": "Az integráció nem hozható létre"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Nem sikerült alkalmazni a változtatásokat",
"message": "Az integrációt nem sikerült menteni"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Hozzáadás",
"apply": "Alkalmaz",
"backToOverview": "Vissza az áttekintéshez",
"change": "",
"create": "Létrehozás",
"createAnother": "",
"edit": "Szerkesztés",

View File

@@ -645,6 +645,21 @@
"title": "Creazione fallita",
"message": "Non è stato possibile creare l'integrazione"
}
},
"app": {
"option": {
"existing": {
"title": "Esistente",
"label": "Seleziona app esistente"
},
"new": {
"title": "",
"url": {
"label": "Url app",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Impossibile applicare le modifiche",
"message": "Non è stato possibile salvare l'integrazione"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Crea Motore Di Ricerca",
"description": "Integrazione \"{kind}\" può essere utilizzato con i motori di ricerca. Selezionare questa opzione per configurare automaticamente il motore di ricerca."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "Crea app",
"description": "Crea un'app con lo stesso nome ed icona dell'integrazione. Lasciare vuoto il campo di input qui sotto per creare l'applicazione con l'URL di integrazione."
@@ -1026,6 +1051,7 @@
"add": "Aggiungi",
"apply": "Applica",
"backToOverview": "Torna alla panoramica",
"change": "",
"create": "Crea",
"createAnother": "",
"edit": "Modifica",

View File

@@ -645,6 +645,21 @@
"title": "作成失敗",
"message": "連携機能を作成できません"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "変更の適用失敗",
"message": "連携機能を保存できません"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "検索エンジンを作成",
"description": "連携機能 \"{kind}\" は、検索エンジンで使用できます。検索エンジンを自動的に設定するには、これにチェックを入れてください。"
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "アプリの作成",
"description": "連携機能と同じ名前とアイコンを持つアプリを作成します。 アプリケーションを作成するには、以下の入力フィールドを空のままにしてください。"
@@ -1026,6 +1051,7 @@
"add": "追加",
"apply": "適用",
"backToOverview": "概要に戻る",
"change": "",
"create": "作成",
"createAnother": "作成・新規入力",
"edit": "編集",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "추가",
"apply": "",
"backToOverview": "",
"change": "",
"create": "만들기",
"createAnother": "",
"edit": "수정",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "",
"apply": "",
"backToOverview": "",
"change": "",
"create": "Sukurti",
"createAnother": "",
"edit": "",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Pievienot",
"apply": "Lietot",
"backToOverview": "",
"change": "",
"create": "Izveidot",
"createAnother": "",
"edit": "Rediģēt",

View File

@@ -645,6 +645,21 @@
"title": "Aanmaken mislukt",
"message": "De integratie kon niet worden aangemaakt"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Kan wijzigingen niet toepassen",
"message": "De integratie kon niet worden opgeslagen"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Zoekmachine aanmaken",
"description": "Integratie \"{kind}\" kan worden gebruikt met de zoekmachines. Controleer dit om automatisch de zoekmachine te configureren."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "App aanmaken",
"description": "Maak een app n met dezelfde naam en icoon als de integratie. Laat het invoerveld hieronder leeg om de app te maken met de integratie-URL."
@@ -1026,6 +1051,7 @@
"add": "Toevoegen",
"apply": "Toepassen",
"backToOverview": "Terug naar overzicht",
"change": "",
"create": "Aanmaken",
"createAnother": "Aanmaken en opnieuw beginnen",
"edit": "Bewerken",

View File

@@ -645,6 +645,21 @@
"title": "Opprettelse mislyktes",
"message": "Integrasjonen kunne ikke opprettes"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Kunne ikke fullføre endringer",
"message": "Integrasjonen kunne ikke lagres"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Opprett søkemotor",
"description": "Integrasjon \"{kind}\" kan brukes med søkemotorene. Huk av her for å automatisk konfigurere søkemotor."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Legg til",
"apply": "Bruk",
"backToOverview": "Tilbake til oversikt",
"change": "",
"create": "Opprett",
"createAnother": "Opprett og start på nytt",
"edit": "Rediger",

View File

@@ -645,6 +645,21 @@
"title": "Nie udało się utworzyć",
"message": "Integracja nie może zostać utworzona"
}
},
"app": {
"option": {
"existing": {
"title": "Istniejąca",
"label": "Wybierz istniejącą aplikację"
},
"new": {
"title": "Nowy",
"url": {
"label": "Adres URL aplikacji",
"description": "Adres URL, który zostanie otwarty po kliknięciu aplikacji na pulpicie"
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Nie można wprowadzić zmian",
"message": "Integracja nie mogła zostać zapisana"
}
},
"app": {
"action": {
"add": "Połącz aplikację",
"remove": "Odłącz",
"select": "Wybierz aplikację do połączenia"
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Utwórz wyszukiwarkę",
"description": "Integracja \"{kind}\" może być używana z wyszukiwarkami. Zaznacz to, aby automatycznie skonfigurować wyszukiwarkę."
},
"app": {
"sectionTitle": "Połączona aplikacja"
},
"createApp": {
"label": "Stwórz aplikację",
"description": "Utwórz aplikację o tej samej nazwie i ikonie, co integracja. Poniższe pole pozostaw puste, aby utworzyć aplikację z URL integracji."
@@ -1026,6 +1051,7 @@
"add": "Dodaj",
"apply": "Zastosuj",
"backToOverview": "Powrót do widoku ogólnego",
"change": "Zmień",
"create": "Utwórz",
"createAnother": "Utwórz i zacznij od nowa",
"edit": "Edytuj",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Adicionar",
"apply": "Aplicar",
"backToOverview": "",
"change": "",
"create": "Criar",
"createAnother": "",
"edit": "Editar",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Adaugă",
"apply": "Aplică",
"backToOverview": "",
"change": "",
"create": "Creează",
"createAnother": "",
"edit": "Editare",

View File

@@ -645,6 +645,21 @@
"title": "Не удалось создать",
"message": "Не удалось создать интеграцию"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Не удалось сохранить",
"message": "Не удалось сохранить интеграцию"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Создать поисковую систему",
"description": "Интеграцию \"{kind}\" можно использовать с поисковыми системами. Включите эту опцию для автоматической настройки поисковой системы."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Добавить",
"apply": "Применить",
"backToOverview": "Вернуться к обзору",
"change": "",
"create": "Создать",
"createAnother": "Создать и начать заново",
"edit": "Редактировать",

View File

@@ -645,6 +645,21 @@
"title": "Vytvorenie zlyhalo",
"message": "Integráciu nebolo možné vytvoriť"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Nie je možné použiť zmeny",
"message": "Integráciu sa nepodarilo uložiť"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Vytvorte vyhľadávače",
"description": "Integrácia \"{kind}\" môže byť použitá s vyhľadávačmi. Toto začiarknite, ak chcete automaticky nakonfigurovať vyhľadávací nástroj."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "Vytvoriť aplikáciu",
"description": "Vytvorte aplikáciu s rovnakým názvom a ikonou ako integrácia. Ak chcete vytvoriť aplikáciu s integračnou webovou adresou, ponechajte vstupné pole nižšie prázdne."
@@ -1026,6 +1051,7 @@
"add": "Pridať",
"apply": "Použiť",
"backToOverview": "Späť na prehľad",
"change": "",
"create": "Vytvoriť",
"createAnother": "Vytvorte a začnite odznova",
"edit": "Upraviť",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Dodaj",
"apply": "Uporabi",
"backToOverview": "",
"change": "",
"create": "Ustvarite spletno stran",
"createAnother": "",
"edit": "Uredi",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Lägg till",
"apply": "Använd",
"backToOverview": "Tillbaka till översikten",
"change": "",
"create": "Addera",
"createAnother": "Addera och börja om",
"edit": "Redigera",

View File

@@ -645,6 +645,21 @@
"title": "Oluşturma başarısız oldu",
"message": "Entegrasyon oluşturulamadı"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Değişiklikler uygulanamıyor",
"message": "Entegrasyon kaydedilemedi"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Arama Motoru Oluştur",
"description": "\"{kind}\" entegrasyonu arama motorlarıyla kullanılabilir. Arama motorunu otomatik olarak yapılandırmak için bunu işaretleyin."
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "Uygulama oluştur",
"description": "Entegrasyon ile aynı adı ve simgeyi taşıyan bir uygulama oluşturun. Uygulamayı entegrasyon URL'si ile oluşturmak için aşağıdaki alanını boş bırakın."
@@ -1026,6 +1051,7 @@
"add": "Ekle",
"apply": "Uygula",
"backToOverview": "Genel bakışa dön",
"change": "",
"create": "Oluştur",
"createAnother": "Kaydet ve Yeni Oluştur",
"edit": "Düzenle",

View File

@@ -645,6 +645,21 @@
"title": "Не вдалося створити",
"message": "Не вдалося створити інтеграцію"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "Не вдалося застосувати зміни",
"message": "Не вдалося зберегти інтеграцію"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Створення пошукових систем",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Додати",
"apply": "Застосувати",
"backToOverview": "Назад до огляду",
"change": "",
"create": "Створити",
"createAnother": "",
"edit": "Редагувати",

View File

@@ -645,6 +645,21 @@
"title": "",
"message": ""
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "",
"message": ""
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "Tạo công cụ tìm kiếm",
"description": ""
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "Tạo ứng dụng",
"description": ""
@@ -1026,6 +1051,7 @@
"add": "Thêm",
"apply": "Áp dụng",
"backToOverview": "Trở về mục tổng quan",
"change": "",
"create": "Tạo nên",
"createAnother": "",
"edit": "Sửa",

View File

@@ -645,6 +645,21 @@
"title": "創建失敗",
"message": "此集成無法被創建"
}
},
"app": {
"option": {
"existing": {
"title": "",
"label": ""
},
"new": {
"title": "",
"url": {
"label": "",
"description": ""
}
}
}
}
},
"edit": {
@@ -658,6 +673,13 @@
"title": "無法應用變更",
"message": "集成無法被儲存"
}
},
"app": {
"action": {
"add": "",
"remove": "",
"select": ""
}
}
},
"delete": {
@@ -686,6 +708,9 @@
"label": "創建搜尋引擎",
"description": "集成 {kind} 可以與搜尋引擎共同使用,勾選此選項可自動設定搜尋引擎"
},
"app": {
"sectionTitle": ""
},
"createApp": {
"label": "創建應用程式",
"description": "建立一個與集成相同名稱和圖示的應用程式,將下方輸入欄位留空,以使用集成網址創建應用程式"
@@ -1026,6 +1051,7 @@
"add": "新增",
"apply": "應用",
"backToOverview": "返回總覽",
"change": "",
"create": "創建",
"createAnother": "創建並重新開始",
"edit": "編輯",

View File

@@ -52,22 +52,22 @@
"@mantine/core": "^8.3.5",
"@mantine/hooks": "^8.3.5",
"@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "2.26.3",
"@tiptap/extension-highlight": "2.26.3",
"@tiptap/extension-image": "2.26.3",
"@tiptap/extension-link": "^2.26.3",
"@tiptap/extension-placeholder": "^2.26.3",
"@tiptap/extension-table": "2.26.3",
"@tiptap/extension-table-cell": "2.26.3",
"@tiptap/extension-table-header": "2.26.3",
"@tiptap/extension-table-row": "2.26.3",
"@tiptap/extension-task-item": "2.26.3",
"@tiptap/extension-task-list": "2.26.3",
"@tiptap/extension-text-align": "2.26.3",
"@tiptap/extension-text-style": "2.26.3",
"@tiptap/extension-underline": "2.26.3",
"@tiptap/react": "^2.26.3",
"@tiptap/starter-kit": "^2.26.3",
"@tiptap/extension-color": "2.26.4",
"@tiptap/extension-highlight": "2.26.4",
"@tiptap/extension-image": "2.26.4",
"@tiptap/extension-link": "^2.26.4",
"@tiptap/extension-placeholder": "^2.26.4",
"@tiptap/extension-table": "2.26.4",
"@tiptap/extension-table-cell": "2.26.4",
"@tiptap/extension-table-header": "2.26.4",
"@tiptap/extension-table-row": "2.26.4",
"@tiptap/extension-task-item": "2.26.4",
"@tiptap/extension-task-list": "2.26.4",
"@tiptap/extension-text-align": "2.26.4",
"@tiptap/extension-text-style": "2.26.4",
"@tiptap/extension-underline": "2.26.4",
"@tiptap/react": "^2.26.4",
"@tiptap/starter-kit": "^2.26.4",
"clsx": "^2.1.1",
"dayjs": "^1.11.18",
"mantine-form-zod-resolver": "^1.3.0",

View File

@@ -21,6 +21,7 @@ import ReactMarkdown from "react-markdown";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { isDateWithin, isNullOrWhitespace, splitToChunksWithNItems } from "@homarr/common";
import { getIconUrl } from "@homarr/definitions";
import { useScopedI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui";
@@ -96,55 +97,33 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
const repositories = useMemo(() => {
const formattedResults = options.repositories
.map((repository) => {
if (repository.providerIntegrationId === undefined) {
return {
...repository,
isNewRelease: false,
isStaleRelease: false,
latestReleaseAt: undefined,
error: {
code: "noProviderSeleceted",
},
};
}
if (!repository.providerIntegrationId) return { ...repository, error: { code: "noProviderSeleceted" } };
const response = results.flat().find(({ data }) => data.id === repository.id)?.data;
const repositoryResult = results.flat().find(({ id }) => id === repository.id);
if (!repositoryResult) return { ...repository, error: { code: "noProviderResponse" } };
if (!repositoryResult.success) return { ...repository, error: repositoryResult.error };
if (response === undefined)
return {
...repository,
isNewRelease: false,
isStaleRelease: false,
latestReleaseAt: undefined,
error: {
code: "noProviderResponse",
},
};
const { data: release, integration } = repositoryResult;
const isReleaseWithin = (relativeDate: string) =>
Boolean(relativeDate) && isDateWithin(release.latestReleaseAt, relativeDate);
return {
...repository,
...response,
isNewRelease:
relativeDateOptions.newReleaseWithin !== "" && response.latestReleaseAt
? isDateWithin(response.latestReleaseAt, relativeDateOptions.newReleaseWithin)
: false,
isStaleRelease:
relativeDateOptions.staleReleaseWithin !== "" && response.latestReleaseAt
? !isDateWithin(response.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
: false,
viewed: releasesViewedList[repository.id] === response.latestRelease,
...release,
integration: { name: integration.name, iconUrl: getIconUrl(integration.kind) },
isNewRelease: isReleaseWithin(relativeDateOptions.newReleaseWithin),
isStaleRelease: !isReleaseWithin(relativeDateOptions.staleReleaseWithin),
viewed: releasesViewedList[repository.id] === release.latestRelease,
};
})
.filter(
(repository) =>
repository.error !== undefined ||
!options.showOnlyHighlighted ||
repository.isNewRelease ||
repository.isStaleRelease,
"error" in repository || !options.showOnlyHighlighted || repository.isNewRelease || repository.isStaleRelease,
)
.sort((repoA, repoB) => {
if (repoA.latestReleaseAt === undefined) return -1;
if (repoB.latestReleaseAt === undefined) return 1;
if ("error" in repoA) return -1;
if ("error" in repoB) return 1;
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
}) as ReleasesRepositoryResponse[];

1531
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,7 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^6.1.1",
"typescript-eslint": "^8.46.1"
"typescript-eslint": "^8.46.2"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",

View File

@@ -7,7 +7,7 @@ runs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22.20.0
node-version: 24.10.0
cache: "pnpm"
- shell: bash