chore(release): automatic release v1.19.1

This commit is contained in:
homarr-releases[bot]
2025-05-09 19:14:15 +00:00
committed by GitHub
61 changed files with 1952 additions and 1491 deletions

View File

@@ -31,6 +31,7 @@ body:
label: Version label: Version
description: What version of Homarr are you running? description: What version of Homarr are you running?
options: options:
- 1.19.0
- 1.18.0 - 1.18.0
- 1.17.0 - 1.17.0
- 1.16.0 - 1.16.0

View File

@@ -48,17 +48,17 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0", "@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^7.17.7", "@mantine/colors-generator": "^8.0.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/dropzone": "^7.17.7", "@mantine/dropzone": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"@mantine/modals": "^7.17.7", "@mantine/modals": "^8.0.0",
"@mantine/tiptap": "^7.17.7", "@mantine/tiptap": "^8.0.0",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tanstack/react-query": "^5.75.1", "@tanstack/react-query": "^5.75.7",
"@tanstack/react-query-devtools": "^5.75.1", "@tanstack/react-query-devtools": "^5.75.7",
"@tanstack/react-query-next-experimental": "^5.75.1", "@tanstack/react-query-next-experimental": "^5.75.7",
"@trpc/client": "^11.1.2", "@trpc/client": "^11.1.2",
"@trpc/next": "^11.1.2", "@trpc/next": "^11.1.2",
"@trpc/react-query": "^11.1.2", "@trpc/react-query": "^11.1.2",
@@ -72,33 +72,33 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"flag-icons": "^7.3.2", "flag-icons": "^7.3.2",
"glob": "^11.0.2", "glob": "^11.0.2",
"jotai": "^2.12.3", "jotai": "^2.12.4",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1", "next": "15.3.2",
"postcss-preset-mantine": "^1.17.0", "postcss-preset-mantine": "^1.17.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-error-boundary": "^5.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.87.0", "sass": "^1.87.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"swagger-ui-react": "^5.21.0", "swagger-ui-react": "^5.21.0",
"use-deep-compare-effect": "^1.8.1", "use-deep-compare-effect": "^1.8.1",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1", "@types/chroma-js": "3.1.1",
"@types/node": "^22.15.3", "@types/node": "^22.15.17",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "19.1.2", "@types/react": "19.1.3",
"@types/react-dom": "19.1.3", "@types/react-dom": "19.1.3",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.1.2", "concurrently": "^9.1.2",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"node-loader": "^2.1.0", "node-loader": "^2.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -44,9 +44,9 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.15.3", "@types/node": "^22.15.17",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -27,14 +27,14 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"tsx": "4.19.4", "tsx": "4.19.4",
"ws": "^8.18.1" "ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -38,20 +38,20 @@
"@semantic-release/github": "^11.0.2", "@semantic-release/github": "^11.0.2",
"@semantic-release/npm": "^12.0.1", "@semantic-release/npm": "^12.0.1",
"@semantic-release/release-notes-generator": "^14.0.3", "@semantic-release/release-notes-generator": "^14.0.3",
"@turbo/gen": "^2.5.2", "@turbo/gen": "^2.5.3",
"@vitejs/plugin-react": "^4.4.1", "@vitejs/plugin-react": "^4.4.1",
"@vitest/coverage-v8": "^3.1.2", "@vitest/coverage-v8": "^3.1.3",
"@vitest/ui": "^3.1.2", "@vitest/ui": "^3.1.3",
"conventional-changelog-conventionalcommits": "^8.0.0", "conventional-changelog-conventionalcommits": "^8.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"semantic-release": "^24.2.3", "semantic-release": "^24.2.3",
"testcontainers": "^10.25.0", "testcontainers": "^10.25.0",
"turbo": "^2.5.2", "turbo": "^2.5.3",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2" "vitest": "^3.1.3"
}, },
"packageManager": "pnpm@10.10.0", "packageManager": "pnpm@10.10.0",
"engines": { "engines": {

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -40,25 +40,25 @@
"@homarr/request-handler": "workspace:^0.1.0", "@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.1.2", "@kubernetes/client-node": "^1.2.0",
"@tanstack/react-query": "^5.75.1", "@tanstack/react-query": "^5.75.7",
"@trpc/client": "^11.1.2", "@trpc/client": "^11.1.2",
"@trpc/react-query": "^11.1.2", "@trpc/react-query": "^11.1.2",
"@trpc/server": "^11.1.2", "@trpc/server": "^11.1.2",
"@trpc/tanstack-react-query": "^11.1.2", "@trpc/tanstack-react-query": "^11.1.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"trpc-to-openapi": "^2.2.0", "trpc-to-openapi": "^2.2.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -5,7 +5,7 @@ import { releasesRequestHandler } from "@homarr/request-handler/releases";
import { createTRPCRouter, publicProcedure } from "../../trpc"; import { createTRPCRouter, publicProcedure } from "../../trpc";
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => { const formatVersionFilterRegex = (versionFilter: z.infer<typeof releaseVersionFilterSchema> | undefined) => {
if (!versionFilter) return undefined; if (!versionFilter) return undefined;
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : ""; const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
@@ -15,7 +15,7 @@ const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionF
return `^${escapedPrefix}${precision}${escapedSuffix}$`; return `^${escapedPrefix}${precision}${escapedSuffix}$`;
}; };
const _releaseVersionFilterSchema = z.object({ const releaseVersionFilterSchema = z.object({
prefix: z.string().optional(), prefix: z.string().optional(),
precision: z.number(), precision: z.number(),
suffix: z.string().optional(), suffix: z.string().optional(),
@@ -29,7 +29,7 @@ export const releasesRouter = createTRPCRouter({
z.object({ z.object({
providerKey: z.string(), providerKey: z.string(),
identifier: z.string(), identifier: z.string(),
versionFilter: _releaseVersionFilterSchema.optional(), versionFilter: releaseVersionFilterSchema.optional(),
}), }),
), ),
}), }),

View File

@@ -13,6 +13,7 @@ import { extractProfileName } from "./providers/oidc/oidc-provider";
export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => { export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["events"], undefined>["signIn"] => {
return async ({ user, profile }) => { return async ({ user, profile }) => {
logger.debug(`SignIn EventHandler for user: ${JSON.stringify(user)} . profile: ${JSON.stringify(profile)}`);
if (!user.id) throw new Error("User ID is missing"); if (!user.id) throw new Error("User ID is missing");
const dbUser = await db.query.users.findFirst({ const dbUser = await db.query.users.findFirst({
@@ -28,11 +29,13 @@ export const createSignInEventHandler = (db: Database): Exclude<NextAuthConfig["
const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE; const groupsKey = env.AUTH_OIDC_GROUPS_ATTRIBUTE;
// Groups from oidc provider are provided from the profile, it's not typed. // Groups from oidc provider are provided from the profile, it's not typed.
if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) { if (profile && groupsKey in profile && Array.isArray(profile[groupsKey])) {
logger.debug(`Using profile groups (${groupsKey}): ${JSON.stringify(profile[groupsKey])}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]); await synchronizeGroupsWithExternalForUserAsync(db, user.id, profile[groupsKey] as string[]);
} }
// In ldap-authroization we return the groups from ldap, it's not typed. // In ldap-authroization we return the groups from ldap, it's not typed.
if ("groups" in user && Array.isArray(user.groups)) { if ("groups" in user && Array.isArray(user.groups)) {
logger.debug(`Using profile groups: ${JSON.stringify(user.groups)}`);
await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]); await synchronizeGroupsWithExternalForUserAsync(db, user.id, user.groups as string[]);
} }
await addUserToEveryoneGroupIfNotMemberAsync(db, user.id); await addUserToEveryoneGroupIfNotMemberAsync(db, user.id);

View File

@@ -34,12 +34,12 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "7.4.0", "ldapts": "8.0.0",
"next": "15.3.1", "next": "15.3.2",
"next-auth": "5.0.0-beta.27", "next-auth": "5.0.0-beta.27",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/cookies": "0.9.0", "@types/cookies": "0.9.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -30,17 +30,17 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"undici": "7.8.0", "undici": "7.8.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -5,3 +5,11 @@ export const splitToNChunks = <T>(array: T[], chunks: number): T[][] => {
} }
return result; return result;
}; };
export const splitToChunksWithNItems = <T>(array: T[], itemCount: number): T[][] => {
const result: T[][] = [];
for (let i = 0; i < array.length; i += itemCount) {
result.push(array.slice(i, i + itemCount));
}
return result;
};

View File

@@ -0,0 +1,26 @@
import dayjs from "dayjs";
import type { UnitTypeShort } from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
dayjs.extend(isBetween);
const validUnits = ["h", "d", "w", "M", "y"] as UnitTypeShort[];
export const isDateWithin = (date: Date, relativeDate: string): boolean => {
if (relativeDate.length < 2) {
throw new Error("Relative date must be at least 2 characters long");
}
const amount = parseInt(relativeDate.slice(0, -1), 10);
if (isNaN(amount) || amount <= 0) {
throw new Error("Relative date must be a number greater than 0");
}
const unit = relativeDate.slice(-1) as dayjs.UnitTypeShort;
if (!validUnits.includes(unit)) {
throw new Error("Invalid relative time unit");
}
const startDate = dayjs().subtract(amount, unit);
return dayjs(date).isBetween(startDate, dayjs(), null, "[]");
};

View File

@@ -2,6 +2,7 @@ export * from "./object";
export * from "./string"; export * from "./string";
export * from "./cookie"; export * from "./cookie";
export * from "./array"; export * from "./array";
export * from "./date";
export * from "./stopwatch"; export * from "./stopwatch";
export * from "./hooks"; export * from "./hooks";
export * from "./url"; export * from "./url";

View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { splitToChunksWithNItems, splitToNChunks } from "../array";
describe("splitToNChunks", () => {
it("should split an array into the specified number of chunks", () => {
const array = [1, 2, 3, 4, 5];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const chunks = 3;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[], [], []]);
});
it("should handle more chunks than elements", () => {
const array = [1, 2];
const chunks = 5;
const result = splitToNChunks(array, chunks);
expect(result).toEqual([[1], [2], [], [], []]);
});
});
describe("splitToChunksWithNItems", () => {
it("should split an array into chunks with the specified number of items", () => {
const array = [1, 2, 3, 4, 5];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2], [3, 4], [5]]);
});
it("should handle an empty array", () => {
const array: number[] = [];
const items = 2;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([]);
});
it("should handle more items per chunk than elements", () => {
const array = [1, 2];
const items = 5;
const result = splitToChunksWithNItems(array, items);
expect(result).toEqual([[1, 2]]);
});
});

View File

@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { isDateWithin } from "../date";
describe("isDateWithin", () => {
it("should return true for a date within the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 20);
expect(isDateWithin(date, "100h")).toBe(true);
});
it("should return false for a date outside the specified hours", () => {
const date = new Date();
date.setHours(date.getHours() - 101);
expect(isDateWithin(date, "100h")).toBe(false);
});
it("should return true for a date within the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 5);
expect(isDateWithin(date, "10d")).toBe(true);
});
it("should return false for a date outside the specified days", () => {
const date = new Date();
date.setDate(date.getDate() - 11);
expect(isDateWithin(date, "10d")).toBe(false);
});
it("should return true for a date within the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 10);
expect(isDateWithin(date, "7w")).toBe(true);
});
it("should return false for a date outside the specified weeks", () => {
const date = new Date();
date.setDate(date.getDate() - 50);
expect(isDateWithin(date, "7w")).toBe(false);
});
it("should return true for a date within the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 1);
expect(isDateWithin(date, "2M")).toBe(true);
});
it("should return false for a date outside the specified months", () => {
const date = new Date();
date.setMonth(date.getMonth() - 3);
expect(isDateWithin(date, "2M")).toBe(false);
});
it("should return true for a date within the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
expect(isDateWithin(date, "2y")).toBe(true);
});
it("should return false for a date outside the specified years", () => {
const date = new Date();
date.setFullYear(date.getFullYear() - 3);
expect(isDateWithin(date, "2y")).toBe(false);
});
it("should return false for a date after the specified relative time", () => {
const date = new Date();
date.setDate(date.getDate() + 2);
expect(isDateWithin(date, "1d")).toBe(false);
});
it("should throw an error for an invalid unit", () => {
const date = new Date();
expect(() => isDateWithin(date, "2x")).toThrow("Invalid relative time unit");
});
it("should throw an error if relativeDate is less than 2 characters long", () => {
const date = new Date();
expect(() => isDateWithin(date, "h")).toThrow("Relative date must be at least 2 characters long");
});
it("should throw an error if relativeDate has an invalid number", () => {
const date = new Date();
expect(() => isDateWithin(date, "hh")).toThrow("Relative date must be a number greater than 0");
});
it("should throw an error if relativeDate is set to 0", () => {
const date = new Date();
expect(() => isDateWithin(date, "0y")).toThrow("Relative date must be a number greater than 0");
});
});

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -44,12 +44,12 @@
"@homarr/env": "workspace:^0.1.0", "@homarr/env": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@paralleldrive/cuid2": "^2.2.2", "@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^10.25.0", "@testcontainers/mysql": "^10.25.0",
"better-sqlite3": "^11.9.1", "better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.0", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1", "drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1", "drizzle-zod": "^0.7.1",
"mysql2": "3.14.1" "mysql2": "3.14.1"
@@ -60,7 +60,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tsx": "4.19.4", "tsx": "4.19.4",
"typescript": "^5.8.3" "typescript": "^5.8.3"

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -32,7 +32,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.38", "@types/dockerode": "^3.3.38",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,13 +24,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@t3-oss/env-nextjs": "^0.13.4", "@t3-oss/env-nextjs": "^0.13.4",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -26,14 +26,14 @@
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^7.17.7", "@mantine/form": "^8.0.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0", "@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"react": "19.1.0", "react": "19.1.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -43,7 +43,7 @@
"tsdav": "^2.1.4", "tsdav": "^2.1.4",
"undici": "7.8.0", "undici": "7.8.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
@@ -51,7 +51,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1", "@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -27,13 +27,13 @@
"ioredis": "5.6.1", "ioredis": "5.6.1",
"superjson": "2.2.2", "superjson": "2.2.2",
"winston": "3.17.0", "winston": "3.17.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -22,6 +22,10 @@ export const formatErrorCause = (cause: unknown, iteration = 0): string => {
return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`; return `\ncaused by ${formatErrorTitle(cause)}\n${formatErrorStack(cause.stack)}${formatErrorCause(cause.cause, iteration + 1)}`;
} }
if (cause instanceof Object) {
return `\ncaused by ${JSON.stringify(cause)}`;
}
return `\ncaused by ${cause as string}`; return `\ncaused by ${cause as string}`;
}; };

View File

@@ -33,19 +33,19 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,15 +24,15 @@
"dependencies": { "dependencies": {
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"react": "19.1.0" "react": "19.1.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^7.17.7", "@mantine/notifications": "^8.0.0",
"@tabler/icons-react": "^3.31.0" "@tabler/icons-react": "^3.31.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -37,14 +37,14 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"superjson": "2.2.2", "superjson": "2.2.2",
"zod": "^3.24.3", "zod": "^3.24.4",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7", "@types/adm-zip": "0.5.7",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -22,7 +22,7 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@extractus/feed-extractor": "7.1.4", "@extractus/feed-extractor": "7.1.5",
"@homarr/common": "workspace:^0.1.0", "@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
@@ -37,7 +37,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -41,12 +41,8 @@ export const Providers: ProvidersProps = {
.transform((resp) => ({ .transform((resp) => ({
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`, projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
projectDescription: resp.description, projectDescription: resp.description,
isFork: false,
isArchived: false,
createdAt: resp.date_registered, createdAt: resp.date_registered,
starsCount: resp.star_count, starsCount: resp.star_count,
openIssues: 0,
forksCount: 0,
})) }))
.safeParse(response); .safeParse(response);
}, },
@@ -67,12 +63,7 @@ export const Providers: ProvidersProps = {
), ),
}) })
.transform((resp) => { .transform((resp) => {
return resp.results.map((release) => ({ return resp.results;
...release,
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
}));
}) })
.safeParse(response); .safeParse(response);
}, },
@@ -89,7 +80,7 @@ export const Providers: ProvidersProps = {
return z return z
.object({ .object({
html_url: z.string(), html_url: z.string(),
description: z.string(), description: z.string().nullable(),
fork: z.boolean(), fork: z.boolean(),
archived: z.boolean(), archived: z.boolean(),
created_at: z.string().transform((value) => new Date(value)), created_at: z.string().transform((value) => new Date(value)),
@@ -99,7 +90,7 @@ export const Providers: ProvidersProps = {
}) })
.transform((resp) => ({ .transform((resp) => ({
projectUrl: resp.html_url, projectUrl: resp.html_url,
projectDescription: resp.description, projectDescription: resp.description ?? undefined,
isFork: resp.fork, isFork: resp.fork,
isArchived: resp.archived, isArchived: resp.archived,
createdAt: resp.created_at, createdAt: resp.created_at,
@@ -120,7 +111,7 @@ export const Providers: ProvidersProps = {
tag_name: z.string(), tag_name: z.string(),
published_at: z.string().transform((value) => new Date(value)), published_at: z.string().transform((value) => new Date(value)),
html_url: z.string(), html_url: z.string(),
body: z.string(), body: z.string().nullable(),
prerelease: z.boolean(), prerelease: z.boolean(),
}) })
.transform((tag) => ({ .transform((tag) => ({
@@ -128,7 +119,7 @@ export const Providers: ProvidersProps = {
latestRelease: tag.tag_name, latestRelease: tag.tag_name,
latestReleaseAt: tag.published_at, latestReleaseAt: tag.published_at,
releaseUrl: tag.html_url, releaseUrl: tag.html_url,
releaseDescription: tag.body, releaseDescription: tag.body ?? undefined,
isPreRelease: tag.prerelease, isPreRelease: tag.prerelease,
})), })),
) )
@@ -144,17 +135,17 @@ export const Providers: ProvidersProps = {
.object({ .object({
web_url: z.string(), web_url: z.string(),
description: z.string(), description: z.string(),
forked_from_project: z.object({ id: z.number() }).nullable(), forked_from_project: z.object({ id: z.number() }).optional(),
archived: z.boolean(), archived: z.boolean().optional(),
created_at: z.string().transform((value) => new Date(value)), created_at: z.string().transform((value) => new Date(value)),
star_count: z.number(), star_count: z.number(),
open_issues_count: z.number(), open_issues_count: z.number().optional(),
forks_count: z.number(), forks_count: z.number(),
}) })
.transform((resp) => ({ .transform((resp) => ({
projectUrl: resp.web_url, projectUrl: resp.web_url,
projectDescription: resp.description, projectDescription: resp.description,
isFork: resp.forked_from_project !== null, isFork: resp.forked_from_project !== undefined,
isArchived: resp.archived, isArchived: resp.archived,
createdAt: resp.created_at, createdAt: resp.created_at,
starsCount: resp.star_count, starsCount: resp.star_count,
@@ -217,7 +208,6 @@ export const Providers: ProvidersProps = {
...release, ...release,
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`, releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
releaseDescription: resp.versions[release.latestRelease]?.description ?? "", releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
isPreRelease: false,
})); }));
}) })
.safeParse(response); .safeParse(response);
@@ -282,23 +272,31 @@ export const Providers: ProvidersProps = {
}, },
}; };
const _detailsSchema = z.object({ const _detailsSchema = z
projectUrl: z.string(), .object({
projectDescription: z.string(), projectUrl: z.string().optional(),
isFork: z.boolean(), projectDescription: z.string().optional(),
isArchived: z.boolean(), isFork: z.boolean().optional(),
createdAt: z.date(), isArchived: z.boolean().optional(),
starsCount: z.number(), createdAt: z.date().optional(),
openIssues: z.number(), starsCount: z.number().optional(),
forksCount: z.number(), openIssues: z.number().optional(),
}); forksCount: z.number().optional(),
})
.optional();
const _releasesSchema = z.object({ const _releasesSchema = z.object({
latestRelease: z.string(), latestRelease: z.string(),
latestReleaseAt: z.date(), latestReleaseAt: z.date(),
releaseUrl: z.string(), releaseUrl: z.string().optional(),
releaseDescription: z.string(), releaseDescription: z.string().optional(),
isPreRelease: z.boolean(), 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 DetailsResponse = z.infer<typeof _detailsSchema>;

View File

@@ -8,22 +8,49 @@ import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-ha
import { Providers } from "./releases-providers"; import { Providers } from "./releases-providers";
import type { DetailsResponse } from "./releases-providers"; import type { DetailsResponse } from "./releases-providers";
const errorSchema = z.object({
code: z.string().optional(),
message: z.string().optional(),
});
type ReleasesError = z.infer<typeof errorSchema>;
const _reponseSchema = z.object({ const _reponseSchema = z.object({
identifier: z.string(), identifier: z.string(),
providerKey: z.string(), providerKey: z.string(),
latestRelease: z.string(), latestRelease: z.string().optional(),
latestReleaseAt: z.date(), latestReleaseAt: z.date().optional(),
releaseUrl: z.string(), releaseUrl: z.string().optional(),
releaseDescription: z.string(), releaseDescription: z.string().optional(),
isPreRelease: z.boolean(), isPreRelease: z.boolean().optional(),
projectUrl: z.string(), projectUrl: z.string().optional(),
projectDescription: z.string(), projectDescription: z.string().optional(),
isFork: z.boolean(), isFork: z.boolean().optional(),
isArchived: z.boolean(), isArchived: z.boolean().optional(),
createdAt: z.date(), createdAt: z.date().optional(),
starsCount: z.number(), starsCount: z.number().optional(),
openIssues: z.number(), openIssues: z.number().optional(),
forksCount: z.number(), 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({ export const releasesRequestHandler = createCachedWidgetRequestHandler({
@@ -34,17 +61,7 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
if (!provider) return undefined; if (!provider) return undefined;
let detailsResult: DetailsResponse = { let detailsResult: DetailsResponse;
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
};
const detailsUrl = provider.getDetailsUrl(input.identifier); const detailsUrl = provider.getDetailsUrl(input.identifier);
if (detailsUrl !== undefined) { if (detailsUrl !== undefined) {
const detailsResponse = await fetchWithTimeout(detailsUrl); const detailsResponse = await fetchWithTimeout(detailsUrl);
@@ -53,7 +70,8 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
if (parsedDetails?.success) { if (parsedDetails?.success) {
detailsResult = parsedDetails.data; detailsResult = parsedDetails.data;
} else { } else {
logger.warn("Failed to parse details response", { detailsResult = undefined;
logger.warn(`Failed to parse details response for ${input.identifier} on ${input.providerKey}`, {
provider: input.providerKey, provider: input.providerKey,
identifier: input.identifier, identifier: input.identifier,
detailsUrl, detailsUrl,
@@ -63,43 +81,42 @@ export const releasesRequestHandler = createCachedWidgetRequestHandler({
} }
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier)); const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
const releasesResult = provider.parseReleasesResponse(await releasesResponse.json()); const releasesResponseJson: unknown = await releasesResponse.json();
const releasesResult = provider.parseReleasesResponse(releasesResponseJson);
if (!releasesResult.success) return undefined; if (!releasesResult.success) {
return formatErrorRelease(input.identifier, input.providerKey, {
const latest: ResponseResponse = releasesResult.data message: releasesResponseJson ? JSON.stringify(releasesResponseJson, null, 2) : releasesResult.error.message,
.filter((result) => (input.versionRegex ? new RegExp(input.versionRegex).test(result.latestRelease) : true)) });
.reduce( } else {
(latest, result) => { const releases = releasesResult.data.filter((result) =>
return { input.versionRegex && result.latestRelease ? new RegExp(input.versionRegex).test(result.latestRelease) : true,
...detailsResult,
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
identifier: input.identifier,
providerKey: input.providerKey,
};
},
{
identifier: "",
providerKey: "",
latestRelease: "",
latestReleaseAt: new Date(0),
releaseUrl: "",
releaseDescription: "",
isPreRelease: false,
projectUrl: "",
projectDescription: "",
isFork: false,
isArchived: false,
createdAt: new Date(0),
starsCount: 0,
openIssues: 0,
forksCount: 0,
},
); );
return latest; 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;
}
}, },
cacheDuration: dayjs.duration(5, "minutes"), cacheDuration: dayjs.duration(5, "minutes"),
}); });
export type ResponseResponse = z.infer<typeof _reponseSchema>; export type ReleaseResponse = z.infer<typeof _reponseSchema>;

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -26,8 +26,8 @@
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0", "@homarr/db": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0",
"@mantine/dates": "^7.17.7", "@mantine/dates": "^8.0.0",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
}, },
@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -33,12 +33,12 @@
"@homarr/settings": "workspace:^0.1.0", "@homarr/settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"@mantine/spotlight": "^7.17.7", "@mantine/spotlight": "^8.0.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"jotai": "^2.12.3", "jotai": "^2.12.4",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -32,7 +32,7 @@
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1", "next": "15.3.2",
"next-intl": "4.1.0", "next-intl": "4.1.0",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -2059,11 +2059,11 @@
"option": { "option": {
"newReleaseWithin": { "newReleaseWithin": {
"label": "New Release Within", "label": "New Release Within",
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of new releases." "description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of new releases."
}, },
"staleReleaseWithin": { "staleReleaseWithin": {
"label": "Stale Release Within", "label": "Stale Release Within",
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of stale releases." "description": "Usage example: 1w (1 week), 10M (10 months). Accepted unit types h (hours), d (days), w (weeks), M (months), y (years). Leave empty for no highlighting of stale releases."
}, },
"showOnlyHighlighted": { "showOnlyHighlighted": {
"label": "Show Only Highlighted", "label": "Show Only Highlighted",
@@ -2130,7 +2130,13 @@
"openProjectPage": "Open Project Page", "openProjectPage": "Open Project Page",
"openReleasePage": "Open Release Page", "openReleasePage": "Open Release Page",
"releaseDescription": "Release Description", "releaseDescription": "Release Description",
"created": "Created" "created": "Created",
"error": {
"label": "Error",
"options": {
"noMatchingVersion": "No matching version found"
}
}
}, },
"networkControllerSummary": { "networkControllerSummary": {
"option": {}, "option": {},

View File

@@ -258,7 +258,7 @@
}, },
"toLarge": { "toLarge": {
"title": "Bilden är för stor", "title": "Bilden är för stor",
"message": "" "message": "Maximal bildstorlek är {size}"
} }
} }
}, },
@@ -612,17 +612,17 @@
"select": { "select": {
"label": "", "label": "",
"notFound": "", "notFound": "",
"search": "", "search": "Sök efter en applikation",
"noResults": "", "noResults": "",
"action": "", "action": "Välj {app}",
"title": "" "title": "Välj en applikation att lägga till på tavlan"
}, },
"create": { "create": {
"title": "Addera en ny applikation", "title": "Addera en ny applikation",
"description": "Addera en ny applikation ", "description": "Addera en ny applikation ",
"action": "" "action": "Addera applikation"
}, },
"add": "" "add": "Lägg till en applikation"
} }
}, },
"integration": { "integration": {
@@ -954,7 +954,7 @@
"unsavedChanges": "", "unsavedChanges": "",
"preview": { "preview": {
"show": "Förhandsgranska", "show": "Förhandsgranska",
"hide": "" "hide": "Dölj förhandsgranskning"
}, },
"zod": { "zod": {
"errors": { "errors": {
@@ -990,7 +990,7 @@
"section": { "section": {
"dynamic": { "dynamic": {
"action": { "action": {
"create": "", "create": "Ny dynamisk sektion",
"remove": "" "remove": ""
}, },
"option": { "option": {
@@ -1013,7 +1013,7 @@
} }
}, },
"action": { "action": {
"create": "", "create": "Ny kategori",
"edit": "", "edit": "",
"remove": "", "remove": "",
"moveUp": "Flytta uppåt", "moveUp": "Flytta uppåt",
@@ -1024,7 +1024,7 @@
}, },
"create": { "create": {
"title": "", "title": "",
"submit": "" "submit": "Lägg till kategori"
}, },
"remove": { "remove": {
"title": "", "title": "",
@@ -1048,7 +1048,7 @@
}, },
"item": { "item": {
"action": { "action": {
"create": "", "create": "Nytt objekt",
"import": "", "import": "",
"edit": "Redigera objekt", "edit": "Redigera objekt",
"moveResize": "", "moveResize": "",
@@ -1063,7 +1063,7 @@
"create": { "create": {
"title": "Välj objekt du vill lägga till", "title": "Välj objekt du vill lägga till",
"search": "", "search": "",
"addToBoard": "" "addToBoard": "Lägg till på tavlan"
}, },
"moveResize": { "moveResize": {
"title": "", "title": "",
@@ -1138,7 +1138,7 @@
}, },
"bookmarks": { "bookmarks": {
"name": "Bokmärken", "name": "Bokmärken",
"description": "", "description": "Visar länkar till flera applikationer",
"option": { "option": {
"title": { "title": {
"label": "Titel" "label": "Titel"
@@ -1252,7 +1252,7 @@
} }
}, },
"clock": { "clock": {
"name": "", "name": "Datum och tid",
"description": "Visar aktuellt datum och tid.", "description": "Visar aktuellt datum och tid.",
"option": { "option": {
"customTitleToggle": { "customTitleToggle": {
@@ -2298,16 +2298,16 @@
"label": "Namn på sidan" "label": "Namn på sidan"
}, },
"metaTitle": { "metaTitle": {
"label": "" "label": "Metarubrik (visas i huvudet eller fliken i webbläsaren)"
}, },
"logoImageUrl": { "logoImageUrl": {
"label": "URL-adress till logo för tavlan" "label": "URL-adress till logo för tavlan"
}, },
"faviconImageUrl": { "faviconImageUrl": {
"label": "" "label": "URL-adress till bilden som visas som favoritbild"
}, },
"backgroundImageUrl": { "backgroundImageUrl": {
"label": "", "label": "URL-adress till bakgrundsbilden",
"placeholder": "", "placeholder": "",
"group": { "group": {
"your": "", "your": "",
@@ -2315,49 +2315,49 @@
} }
}, },
"backgroundImageAttachment": { "backgroundImageAttachment": {
"label": "Bilaga med bakgrundsbild", "label": "Bakgrundsbildens beteende",
"option": { "option": {
"fixed": { "fixed": {
"label": "", "label": "Fast",
"description": "" "description": "Bakgrunden stannar i samma läge."
}, },
"scroll": { "scroll": {
"label": "", "label": "Förflyttas",
"description": "" "description": "Bakgrunden förflyttas med musens rörelse."
} }
} }
}, },
"backgroundImageRepeat": { "backgroundImageRepeat": {
"label": "", "label": "Upprepa bakgrundsbilden",
"option": { "option": {
"repeat": { "repeat": {
"label": "", "label": "Upprepa",
"description": "" "description": "Bilden kommer att upprepas så mycket som krävs för att täcka bakgrunden."
}, },
"no-repeat": { "no-repeat": {
"label": "", "label": "Ingen upprepning",
"description": "" "description": "Bilden upprepas inte och kommer eventuellt inte fylla hela bakgrunden."
}, },
"repeat-x": { "repeat-x": {
"label": "", "label": "Upprepa horisontellt",
"description": "" "description": "Samma sak om 'Upprepa' men endast horisontellt."
}, },
"repeat-y": { "repeat-y": {
"label": "", "label": "Upprepa vertikalt",
"description": "" "description": "Samma sak som 'Upprepa' men endast vertikalt."
} }
} }
}, },
"backgroundImageSize": { "backgroundImageSize": {
"label": "Storlek på bakgrundsbild", "label": "Storlek på bakgrundsbilden",
"option": { "option": {
"cover": { "cover": {
"label": "", "label": "Täck",
"description": "" "description": "Gör bilden så liten som möjligt för att täcka hela tavlan genom att beskära överflödig den av bilden."
}, },
"contain": { "contain": {
"label": "", "label": "Maximera",
"description": "" "description": "Gör bilden så stor som möjligt för att täcka hela tavlan utan att beskära eller sträcka ut bilden."
} }
} }
}, },
@@ -2411,7 +2411,7 @@
"metaTitle": "" "metaTitle": ""
}, },
"setting": { "setting": {
"title": "Inställningar för tavlan {boardName}", "title": "Inställningar för tavlan \"{boardName}\"",
"section": { "section": {
"general": { "general": {
"title": "Generellt", "title": "Generellt",
@@ -2430,7 +2430,7 @@
"title": "Bakgrund" "title": "Bakgrund"
}, },
"appearance": { "appearance": {
"title": "" "title": "Utseende"
}, },
"customCss": { "customCss": {
"title": "" "title": ""
@@ -3388,11 +3388,11 @@
"label": "Grupp" "label": "Grupp"
}, },
"permission": { "permission": {
"label": "" "label": "Behörighet"
} }
}, },
"action": { "action": {
"saveUser": "", "saveUser": "Spara behörighet",
"saveGroup": "" "saveGroup": ""
} }
}, },

View File

@@ -1950,7 +1950,7 @@
"approved": "Onaylandı", "approved": "Onaylandı",
"declined": "Reddedildi", "declined": "Reddedildi",
"failed": "Başarısız", "failed": "Başarısız",
"completed": "" "completed": "Tamamlandı"
}, },
"toBeDetermined": "-Yapım Aşamasında-" "toBeDetermined": "-Yapım Aşamasında-"
}, },

View File

@@ -29,12 +29,12 @@
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/dates": "^7.17.7", "@mantine/dates": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0" "react-dom": "19.1.0"
}, },
@@ -43,7 +43,7 @@
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/css-modules": "^1.0.5", "@types/css-modules": "^1.0.5",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -24,14 +24,14 @@
"dependencies": { "dependencies": {
"@homarr/definitions": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"zod": "^3.24.3", "zod": "^3.24.4",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -47,42 +47,42 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "^7.17.7", "@mantine/charts": "^8.0.0",
"@mantine/core": "^7.17.7", "@mantine/core": "^8.0.0",
"@mantine/hooks": "^7.17.7", "@mantine/hooks": "^8.0.0",
"@tabler/icons-react": "^3.31.0", "@tabler/icons-react": "^3.31.0",
"@tiptap/extension-color": "2.11.9", "@tiptap/extension-color": "2.12.0",
"@tiptap/extension-highlight": "2.11.9", "@tiptap/extension-highlight": "2.12.0",
"@tiptap/extension-image": "2.11.9", "@tiptap/extension-image": "2.12.0",
"@tiptap/extension-link": "^2.11.9", "@tiptap/extension-link": "^2.12.0",
"@tiptap/extension-table": "2.11.9", "@tiptap/extension-table": "2.12.0",
"@tiptap/extension-table-cell": "2.11.9", "@tiptap/extension-table-cell": "2.12.0",
"@tiptap/extension-table-header": "2.11.9", "@tiptap/extension-table-header": "2.12.0",
"@tiptap/extension-table-row": "2.11.9", "@tiptap/extension-table-row": "2.12.0",
"@tiptap/extension-task-item": "2.11.9", "@tiptap/extension-task-item": "2.12.0",
"@tiptap/extension-task-list": "2.11.9", "@tiptap/extension-task-list": "2.12.0",
"@tiptap/extension-text-align": "2.11.9", "@tiptap/extension-text-align": "2.12.0",
"@tiptap/extension-text-style": "2.11.9", "@tiptap/extension-text-style": "2.12.0",
"@tiptap/extension-underline": "2.11.9", "@tiptap/extension-underline": "2.12.0",
"@tiptap/react": "^2.11.9", "@tiptap/react": "^2.12.0",
"@tiptap/starter-kit": "^2.11.9", "@tiptap/starter-kit": "^2.12.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "15.3.1", "next": "15.3.2",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"video.js": "^8.22.0", "video.js": "^8.22.0",
"zod": "^3.24.3" "zod": "^3.24.4"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58", "@types/video.js": "^7.3.58",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -63,7 +63,7 @@ export const WidgetMultiReleasesRepositoriesInput = ({
const item = { const item = {
providerKey: "DockerHub", providerKey: "DockerHub",
identifier: "", identifier: "",
} as ReleasesRepository; };
form.setValues((previous) => { form.setValues((previous) => {
const previousValues = previous.options?.[property] as ReleasesRepository[]; const previousValues = previous.options?.[property] as ReleasesRepository[];
@@ -98,7 +98,6 @@ export const WidgetMultiReleasesRepositoriesInput = ({
}; };
}); });
}; };
return ( return (
<Fieldset legend={t("label")}> <Fieldset legend={t("label")}>
<Stack gap="5"> <Stack gap="5">

View File

@@ -72,8 +72,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
return ( return (
<Calendar <Calendar
defaultDate={new Date()} defaultDate={new Date()}
onPreviousMonth={setMonth} onPreviousMonth={(month) => setMonth(new Date(month))}
onNextMonth={setMonth} onNextMonth={(month) => setMonth(new Date(month))}
highlightToday highlightToday
locale={locale} locale={locale}
hideWeekdays={false} hideWeekdays={false}
@@ -126,7 +126,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
.filter((event): event is CalendarEvent => Boolean(event.date)); .filter((event): event is CalendarEvent => Boolean(event.date));
return ( return (
<CalendarDay <CalendarDay
date={tileDate} date={new Date(tileDate)}
events={eventsForDate} events={eventsForDate}
disabled={isEditMode || eventsForDate.length === 0} disabled={isEditMode || eventsForDate.length === 0}
rootWidth={width} rootWidth={width}

View File

@@ -10,6 +10,7 @@ import {
IconGitFork, IconGitFork,
IconProgressCheck, IconProgressCheck,
IconStar, IconStar,
IconTriangleFilled,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import combineClasses from "clsx"; import combineClasses from "clsx";
import { useFormatter, useNow } from "next-intl"; import { useFormatter, useNow } from "next-intl";
@@ -17,118 +18,116 @@ import ReactMarkdown from "react-markdown";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context"; import { useRequiredBoard } from "@homarr/boards/context";
import { isDateWithin, splitToChunksWithNItems } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client"; import { useScopedI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui"; import { MaskedOrNormalImage } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss"; import classes from "./component.module.scss";
import { Providers } from "./releases-providers"; import { Providers } from "./releases-providers";
import type { ReleasesRepository } from "./releases-repository"; import type { ReleasesRepositoryResponse } from "./releases-repository";
function isDateWithin(date: Date, relativeDate: string): boolean { const formatRelativeDate = (value: string): string => {
const amount = parseInt(relativeDate.slice(0, -1), 10); const isMonths = /\d+m/g.test(value);
const unit = relativeDate.slice(-1); const isOtherUnits = /\d+[HDWY]/g.test(value);
return isMonths ? value.toUpperCase() : isOtherUnits ? value.toLowerCase() : value;
const startTime = new Date().getTime(); };
const endTime = new Date(date).getTime();
const diffTime = Math.abs(endTime - startTime);
const diffHours = Math.ceil(diffTime / (1000 * 60 * 60));
switch (unit) {
case "h":
return diffHours < amount;
case "d":
return diffHours / 24 < amount;
case "w":
return diffHours / (24 * 7) < amount;
case "m":
return diffHours / (24 * 30) < amount;
case "y":
return diffHours / (24 * 365) < amount;
default:
throw new Error("Invalid unit");
}
}
export default function ReleasesWidget({ options }: WidgetComponentProps<"releases">) { export default function ReleasesWidget({ options }: WidgetComponentProps<"releases">) {
const t = useScopedI18n("widget.releases"); const t = useScopedI18n("widget.releases");
const now = useNow(); const now = useNow();
const formatter = useFormatter(); const formatter = useFormatter();
const board = useRequiredBoard(); const board = useRequiredBoard();
const [expandedRepository, setExpandedRepository] = useState(""); const [expandedRepository, setExpandedRepository] = useState({ providerKey: "", identifier: "" });
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]); const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
const relativeDateOptions = useMemo(
() => ({
newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
staleReleaseWithin: formatRelativeDate(options.staleReleaseWithin),
}),
[options.newReleaseWithin, options.staleReleaseWithin],
);
const [results] = clientApi.widget.releases.getLatest.useSuspenseQuery( const batchedRepositories = useMemo(() => splitToChunksWithNItems(options.repositories, 5), [options.repositories]);
{ const [results] = clientApi.useSuspenseQueries((t) =>
repositories: options.repositories.map((repository) => ({ batchedRepositories.flatMap((chunk) =>
providerKey: repository.providerKey, t.widget.releases.getLatest({
identifier: repository.identifier, repositories: chunk.map((repository) => ({
versionFilter: repository.versionFilter, providerKey: repository.providerKey,
})), identifier: repository.identifier,
}, versionFilter: repository.versionFilter,
{ })),
refetchOnMount: false, }),
refetchOnWindowFocus: false, ),
refetchOnReconnect: false,
retry: false,
},
); );
const repositories = useMemo(() => { const repositories = useMemo(() => {
return results return results
.flat()
.map(({ data }) => { .map(({ data }) => {
if (data === undefined) return undefined; if (data === undefined) return undefined;
const repository = options.repositories.find( const repository = options.repositories.find(
(repository: ReleasesRepository) => (repository) => repository.providerKey === data.providerKey && repository.identifier === data.identifier,
repository.providerKey === data.providerKey && repository.identifier === data.identifier,
); );
if (repository === undefined) return undefined; if (repository === undefined) return undefined;
return { return {
...repository,
...data, ...data,
iconUrl: repository.iconUrl,
isNewRelease: isNewRelease:
options.newReleaseWithin !== "" ? isDateWithin(data.latestReleaseAt, options.newReleaseWithin) : false, relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt
? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin)
: false,
isStaleRelease: isStaleRelease:
options.staleReleaseWithin !== "" ? !isDateWithin(data.latestReleaseAt, options.staleReleaseWithin) : false, relativeDateOptions.staleReleaseWithin !== "" && data.latestReleaseAt
? !isDateWithin(data.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
: false,
}; };
}) })
.filter( .filter(
(repository) => (repository) =>
repository !== undefined && repository !== undefined &&
(!options.showOnlyHighlighted || repository.isNewRelease || repository.isStaleRelease), (repository.error !== undefined ||
!options.showOnlyHighlighted ||
repository.isNewRelease ||
repository.isStaleRelease),
) )
.sort((repoA, repoB) => { .sort((repoA, repoB) => {
if (repoA?.latestReleaseAt === undefined) return 1; if (repoA?.latestReleaseAt === undefined) return 1;
if (repoB?.latestReleaseAt === undefined) return -1; if (repoB?.latestReleaseAt === undefined) return -1;
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1; return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
}) as ReleasesRepository[]; }) as ReleasesRepositoryResponse[];
}, [ }, [
results, results,
options.repositories, options.repositories,
options.showOnlyHighlighted, options.showOnlyHighlighted,
options.newReleaseWithin, relativeDateOptions.newReleaseWithin,
options.staleReleaseWithin, relativeDateOptions.staleReleaseWithin,
]); ]);
const toggleExpandedRepository = useCallback( const toggleExpandedRepository = useCallback(
(identifier: string) => { (repository: ReleasesRepositoryResponse) => {
setExpandedRepository(expandedRepository === identifier ? "" : identifier); if (
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier
) {
setExpandedRepository({ providerKey: "", identifier: "" });
} else {
setExpandedRepository({ providerKey: repository.providerKey, identifier: repository.identifier });
}
}, },
[expandedRepository], [expandedRepository],
); );
return ( return (
<Stack gap={0}> <Stack gap={0}>
{repositories.map((repository: ReleasesRepository) => { {repositories.map((repository: ReleasesRepositoryResponse) => {
const isActive = expandedRepository === repository.identifier; const isActive =
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier;
const hasError = repository.error !== undefined;
return ( return (
<Stack <Stack
@@ -141,7 +140,7 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
[classes.active ?? ""]: isActive, [classes.active ?? ""]: isActive,
})} })}
p="xs" p="xs"
onClick={() => toggleExpandedRepository(repository.identifier)} onClick={() => toggleExpandedRepository(repository)}
> >
<MaskedOrNormalImage <MaskedOrNormalImage
imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl} imageUrl={repository.iconUrl ?? Providers[repository.providerKey]?.iconUrl}
@@ -155,9 +154,14 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
<Group gap={5} justify="space-between" style={{ flex: 1 }}> <Group gap={5} justify="space-between" style={{ flex: 1 }}>
<Text size="xs">{repository.identifier}</Text> <Text size="xs">{repository.identifier}</Text>
<Tooltip label={repository.latestRelease ?? t("not-found")}> <Tooltip
<Text size="xs" fw={700} truncate="end" style={{ flexShrink: 1 }}> withArrow
{repository.latestRelease ?? t("not-found")} arrowSize={5}
label={repository.latestRelease}
events={{ hover: repository.latestRelease !== undefined, focus: false, touch: false }}
>
<Text size="xs" fw={700} truncate="end" c={hasError ? "red" : "text"} style={{ flexShrink: 1 }}>
{hasError ? t("error.label") : (repository.latestRelease ?? t("not-found"))}
</Text> </Text>
</Tooltip> </Tooltip>
</Group> </Group>
@@ -168,20 +172,25 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"} c={repository.isNewRelease ? "primaryColor" : repository.isStaleRelease ? "secondaryColor" : "dimmed"}
> >
{repository.latestReleaseAt && {repository.latestReleaseAt &&
!hasError &&
formatter.relativeTime(repository.latestReleaseAt, { formatter.relativeTime(repository.latestReleaseAt, {
now, now,
style: "narrow", style: "narrow",
})} })}
</Text> </Text>
{(repository.isNewRelease || repository.isStaleRelease) && ( {!hasError ? (
<IconCircleFilled (repository.isNewRelease || repository.isStaleRelease) && (
size={10} <IconCircleFilled
color={ size={10}
repository.isNewRelease color={
? "var(--mantine-color-primaryColor-filled)" repository.isNewRelease
: "var(--mantine-color-secondaryColor-filled)" ? "var(--mantine-color-primaryColor-filled)"
} : "var(--mantine-color-secondaryColor-filled)"
/> }
/>
)
) : (
<IconTriangleFilled size={10} color={"var(--mantine-color-red-filled)"} />
)} )}
</Group> </Group>
</Group> </Group>
@@ -198,8 +207,8 @@ export default function ReleasesWidget({ options }: WidgetComponentProps<"releas
} }
interface DetailsDisplayProps { interface DetailsDisplayProps {
repository: ReleasesRepository; repository: ReleasesRepositoryResponse;
toggleExpandedRepository: (identifier: string) => void; toggleExpandedRepository: (repository: ReleasesRepositoryResponse) => void;
} }
const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => { const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => {
@@ -208,15 +217,15 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
return ( return (
<> <>
<Divider onClick={() => toggleExpandedRepository(repository.identifier)} /> <Divider onClick={() => toggleExpandedRepository(repository)} />
<Group <Group
className={classes.releasesRepositoryDetails} className={classes.releasesRepositoryDetails}
justify="space-between" justify="space-between"
p={5} p={5}
onClick={() => toggleExpandedRepository(repository.identifier)} onClick={() => toggleExpandedRepository(repository)}
> >
<Group> <Group>
<Tooltip label={t("pre-release")}> <Tooltip label={t("pre-release")} withArrow arrowSize={5}>
<IconProgressCheck <IconProgressCheck
size={13} size={13}
color={ color={
@@ -225,14 +234,14 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
/> />
</Tooltip> </Tooltip>
<Tooltip label={t("archived")}> <Tooltip label={t("archived")} withArrow arrowSize={5}>
<IconArchive <IconArchive
size={13} size={13}
color={repository.isArchived ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"} color={repository.isArchived ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"}
/> />
</Tooltip> </Tooltip>
<Tooltip label={t("forked")}> <Tooltip label={t("forked")} withArrow arrowSize={5}>
<IconGitFork <IconGitFork
size={13} size={13}
color={repository.isFork ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"} color={repository.isFork ? "var(--mantine-color-secondaryColor-text)" : "var(--mantine-color-dimmed)"}
@@ -240,16 +249,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Tooltip> </Tooltip>
</Group> </Group>
<Group> <Group>
<Tooltip label={t("starsCount")}> <Tooltip label={t("starsCount")} withArrow arrowSize={5}>
<Group gap={5}> <Group gap={5}>
<IconStar <IconStar
size={12} size={12}
color={repository.starsCount === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"} color={!repository.starsCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/> />
<Text size="xs" c={repository.starsCount === 0 ? "dimmed" : ""}> <Text size="xs" c={!repository.starsCount ? "dimmed" : ""}>
{repository.starsCount === 0 {!repository.starsCount
? "-" ? "-"
: formatter.number(repository.starsCount ?? 0, { : formatter.number(repository.starsCount, {
notation: "compact", notation: "compact",
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
@@ -257,16 +266,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Group> </Group>
</Tooltip> </Tooltip>
<Tooltip label={t("forksCount")}> <Tooltip label={t("forksCount")} withArrow arrowSize={5}>
<Group gap={5}> <Group gap={5}>
<IconGitFork <IconGitFork
size={12} size={12}
color={repository.forksCount === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"} color={!repository.forksCount ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/> />
<Text size="xs" c={repository.forksCount === 0 ? "dimmed" : ""}> <Text size="xs" c={!repository.forksCount ? "dimmed" : ""}>
{repository.forksCount === 0 {!repository.forksCount
? "-" ? "-"
: formatter.number(repository.forksCount ?? 0, { : formatter.number(repository.forksCount, {
notation: "compact", notation: "compact",
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
@@ -274,16 +283,16 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
</Group> </Group>
</Tooltip> </Tooltip>
<Tooltip label={t("issuesCount")}> <Tooltip label={t("issuesCount")} withArrow arrowSize={5}>
<Group gap={5}> <Group gap={5}>
<IconCircleDot <IconCircleDot
size={12} size={12}
color={repository.openIssues === 0 ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"} color={!repository.openIssues ? "var(--mantine-color-dimmed)" : "var(--mantine-color-text)"}
/> />
<Text size="xs" c={repository.openIssues === 0 ? "dimmed" : ""}> <Text size="xs" c={!repository.openIssues ? "dimmed" : ""}>
{repository.openIssues === 0 {!repository.openIssues
? "-" ? "-"
: formatter.number(repository.openIssues ?? 0, { : formatter.number(repository.openIssues, {
notation: "compact", notation: "compact",
maximumFractionDigits: 1, maximumFractionDigits: 1,
})} })}
@@ -297,7 +306,7 @@ const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplay
}; };
interface ExtendedDisplayProps { interface ExtendedDisplayProps {
repository: ReleasesRepository; repository: ReleasesRepositoryResponse;
hasIconColor: boolean; hasIconColor: boolean;
} }
@@ -337,17 +346,32 @@ const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) =>
</Text> </Text>
)} )}
</Group> </Group>
<Divider my={10} mx="30%" /> {(repository.releaseUrl ?? repository.projectUrl) && (
<Button <>
variant="light" <Divider my={10} mx="30%" />
component="a" <Button
href={repository.releaseUrl ?? repository.projectUrl} variant="light"
target="_blank" component="a"
rel="noreferrer" href={repository.releaseUrl ?? repository.projectUrl}
> target="_blank"
<IconExternalLink /> rel="noreferrer"
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")} >
</Button> <IconExternalLink />
{repository.releaseUrl ? t("openReleasePage") : t("openProjectPage")}
</Button>
</>
)}
{repository.error && (
<>
<Divider my={10} mx="30%" />
<Title order={4} ta="center">
{t("error.label")}
</Title>
<Text size="xs" ff="monospace" c="red" style={{ whiteSpace: "pre-wrap" }}>
{repository.error.code ? t(`error.options.${repository.error.code}` as never) : repository.error.message}
</Text>
</>
)}
{repository.releaseDescription && ( {repository.releaseDescription && (
<> <>
<Divider my={10} mx="30%" /> <Divider my={10} mx="30%" />

View File

@@ -4,6 +4,11 @@ import { z } from "zod";
import { createWidgetDefinition } from "../definition"; import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options"; import { optionsBuilder } from "../options";
const relativeDateSchema = z
.string()
.regex(/^\d+[hdwmyHDWMY]$/)
.or(z.literal(""));
export const { definition, componentLoader } = createWidgetDefinition("releases", { export const { definition, componentLoader } = createWidgetDefinition("releases", {
icon: IconRocket, icon: IconRocket,
createOptions() { createOptions() {
@@ -11,18 +16,12 @@ export const { definition, componentLoader } = createWidgetDefinition("releases"
newReleaseWithin: factory.text({ newReleaseWithin: factory.text({
defaultValue: "1w", defaultValue: "1w",
withDescription: true, withDescription: true,
validate: z validate: relativeDateSchema,
.string()
.regex(/^\d+[hdwmy]$/)
.or(z.literal("")),
}), }),
staleReleaseWithin: factory.text({ staleReleaseWithin: factory.text({
defaultValue: "6m", defaultValue: "6M",
withDescription: true, withDescription: true,
validate: z validate: relativeDateSchema,
.string()
.regex(/^\d+[hdwmy]$/)
.or(z.literal("")),
}), }),
showOnlyHighlighted: factory.switch({ showOnlyHighlighted: factory.switch({
withDescription: true, withDescription: true,

View File

@@ -9,7 +9,9 @@ export interface ReleasesRepository {
identifier: string; identifier: string;
versionFilter?: ReleasesVersionFilter; versionFilter?: ReleasesVersionFilter;
iconUrl?: string; iconUrl?: string;
}
export interface ReleasesRepositoryResponse extends ReleasesRepository {
latestRelease?: string; latestRelease?: string;
latestReleaseAt?: Date; latestReleaseAt?: Date;
isNewRelease: boolean; isNewRelease: boolean;
@@ -27,4 +29,6 @@ export interface ReleasesRepository {
starsCount?: number; starsCount?: number;
forksCount?: number; forksCount?: number;
openIssues?: number; openIssues?: number;
error?: { code?: string; message?: string };
} }

2441
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,19 +17,19 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "15.3.1", "@next/eslint-plugin-next": "15.3.2",
"eslint-config-prettier": "^10.1.2", "eslint-config-prettier": "^10.1.5",
"eslint-config-turbo": "^2.5.2", "eslint-config-turbo": "^2.5.3",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.31.1" "typescript-eslint": "^8.32.0"
}, },
"devDependencies": { "devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0", "@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.25.1", "eslint": "^9.26.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }

View File

@@ -15,7 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"prettier-plugin-packagejson": "^2.5.10", "prettier-plugin-packagejson": "^2.5.11",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} }
} }