chore(release): automatic release v1.46.0

This commit is contained in:
homarr-releases[bot]
2025-12-12 19:16:55 +00:00
committed by GitHub
41 changed files with 1248 additions and 1115 deletions

View File

@@ -0,0 +1,43 @@
name: Extract Build Artifact
description: Extracts artifacts from an existing Docker image to be used for from source installation
inputs:
digest:
description: Digest of Docker image to use
required: true
architecture:
description: Name of architecture, will be used to create directories (e.g. amd64, arm64)
required: true
release-tag:
description: Tag of the release to which the artifact will be attached
required: true
repository:
description: Repository to which the release belongs, e.g. owner/repo
required: true
token:
description: GitHub token with permissions to upload release assets
required: true
runs:
using: "composite"
steps:
- name: Start docker container for ${{ inputs.architecture }}
run: |
docker run --name homarr \
-e "SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000" \
--detach --rm ${{ inputs.digest }}
shell: bash
- name: Extract build from ${{ inputs.architecture }} container
run: |
docker exec homarr cp /etc/nginx/templates/nginx.conf /app && \
docker exec homarr tar -czf extraction.tar.gz -C /app . && \
mkdir -p ${{ runner.temp }}/extraction/${{ inputs.architecture }} && \
docker cp homarr:/app/extraction.tar.gz ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ inputs.architecture }}.tar.gz
shell: bash
- name: Stop ${{ inputs.architecture }} container
if: always()
run: docker container remove --force --volumes homarr
shell: bash
- name: Add build archive to release
env:
GH_TOKEN: ${{ inputs.token }}
run: gh release upload --repo ${{ inputs.repository }} ${{ inputs.release-tag }} ${{ runner.temp }}/extraction/${{ inputs.architecture }}/build-${{ inputs.architecture }}.tar.gz --clobber
shell: bash

View File

@@ -37,7 +37,7 @@ jobs:
outputs: outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }} version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }} git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
skipped: ${{ env.SKIP_RELEASE }}
steps: steps:
- run: echo "Skipping release for workflow_dispatch event" - run: echo "Skipping release for workflow_dispatch event"
if: env.SKIP_RELEASE == 'true' if: env.SKIP_RELEASE == 'true'
@@ -146,7 +146,6 @@ jobs:
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
env: env:
SKIP_ENV_VALIDATION: true SKIP_ENV_VALIDATION: true
build-arm64: build-arm64:
name: Build docker image for arm64 name: Build docker image for arm64
needs: release needs: release
@@ -185,10 +184,38 @@ jobs:
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true
env: env:
SKIP_ENV_VALIDATION: true SKIP_ENV_VALIDATION: true
extract-asset-amd64:
name: Extract amd64 asset from docker image
needs: [release, build-amd64]
runs-on: ubuntu-latest
steps:
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }}"
architecture: amd64
release-tag: ${{ needs.release.outputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
extract-asset-arm64:
name: Extract arm64 asset from docker image
needs: [release, build-arm64]
runs-on: ubuntu-24.04-arm
steps:
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
uses: homarr-labs/homarr/.github/actions/extract-build-artifact@dev
with:
digest: "${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}"
architecture: arm64
release-tag: ${{ needs.release.outputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
publish: publish:
name: Complete deployment and notify name: Complete deployment and notify
needs: [release, build-amd64, build-arm64] needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NEXT_VERSION: ${{ needs.release.outputs.version }} NEXT_VERSION: ${{ needs.release.outputs.version }}
@@ -222,6 +249,13 @@ jobs:
${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }} \ ${{ env.GHCR_REPO }}@${{ needs.build-amd64.outputs.digest }} \
${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }} ${{ env.GHCR_REPO }}@${{ needs.build-arm64.outputs.digest }}
- name: Publish release
if: needs.release.outputs.skipped == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit --repo ${{ github.repository }} ${{ needs.release.outputs.version }} --draft=false
- name: Discord notification - name: Discord notification
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}

View File

@@ -44,6 +44,7 @@
{ {
"successComment": false, "successComment": false,
"failComment": false, "failComment": false,
"draftRelease": true,
"releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>" "releaseBodyTemplate": "<%= _.truncate(nextRelease.notes, { 'length': 124000, 'omission': '' }) %>"
} }
] ]

View File

@@ -58,7 +58,7 @@
"@mantine/tiptap": "^8.3.9", "@mantine/tiptap": "^8.3.9",
"@million/lint": "1.0.14", "@million/lint": "1.0.14",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.1", "@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-query-next-experimental": "^5.91.0", "@tanstack/react-query-next-experimental": "^5.91.0",
"@trpc/client": "^11.7.2", "@trpc/client": "^11.7.2",
@@ -78,11 +78,11 @@
"isomorphic-dompurify": "^2.33.0", "isomorphic-dompurify": "^2.33.0",
"jotai": "^2.15.2", "jotai": "^2.15.2",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7", "next": "16.0.10",
"postcss-preset-mantine": "^1.18.0", "postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1", "react-simple-code-editor": "^0.14.1",
"sass": "^1.94.2", "sass": "^1.94.2",

View File

@@ -64,16 +64,23 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
<Stack gap="xl"> <Stack gap="xl">
<form onSubmit={form.onSubmit(handleSubmit)}> <form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="lg"> <Stack gap="lg">
<TextInput label={t("field.username.label")} autoComplete="off" {...form.getInputProps("username")} /> <TextInput
label={t("field.username.label")}
id="username"
autoComplete="username"
{...form.getInputProps("username")}
/>
<CustomPasswordInput <CustomPasswordInput
withPasswordRequirements withPasswordRequirements
label={t("field.password.label")} label={t("field.password.label")}
id="password"
autoComplete="new-password" autoComplete="new-password"
{...form.getInputProps("password")} {...form.getInputProps("password")}
/> />
<PasswordInput <PasswordInput
label={t("field.passwordConfirm.label")} label={t("field.passwordConfirm.label")}
id="password-confirm"
autoComplete="new-password" autoComplete="new-password"
{...form.getInputProps("confirmPassword")} {...form.getInputProps("confirmPassword")}
/> />

View File

@@ -116,8 +116,18 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
<> <>
<form onSubmit={form.onSubmit((credentials) => void signInAsync(credentials.provider, credentials))}> <form onSubmit={form.onSubmit((credentials) => void signInAsync(credentials.provider, credentials))}>
<Stack gap="lg"> <Stack gap="lg">
<TextInput label={t("field.username.label")} {...form.getInputProps("name")} /> <TextInput
<PasswordInput label={t("field.password.label")} {...form.getInputProps("password")} /> label={t("field.username.label")}
id="username"
autoComplete="username"
{...form.getInputProps("name")}
/>
<PasswordInput
label={t("field.password.label")}
id="password"
autoComplete="current-password"
{...form.getInputProps("password")}
/>
{providers.includes("credentials") && ( {providers.includes("credentials") && (
<Stack gap="sm"> <Stack gap="sm">

View File

@@ -49,7 +49,7 @@
"@homarr/tsconfig": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0", "esbuild": "^0.27.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",

View File

@@ -34,7 +34,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/ws": "^8.18.1", "@types/ws": "^8.18.1",
"esbuild": "^0.27.0", "esbuild": "^0.27.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"

View File

@@ -43,7 +43,7 @@
"@semantic-release/npm": "^13.1.2", "@semantic-release/npm": "^13.1.2",
"@semantic-release/release-notes-generator": "^14.1.0", "@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.9.0", "@testcontainers/redis": "^11.9.0",
"@turbo/gen": "^2.6.1", "@turbo/gen": "^2.6.3",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.15", "@vitest/coverage-v8": "^4.0.15",
"@vitest/ui": "^4.0.15", "@vitest/ui": "^4.0.15",
@@ -54,7 +54,7 @@
"prettier": "^3.7.4", "prettier": "^3.7.4",
"semantic-release": "^25.0.2", "semantic-release": "^25.0.2",
"testcontainers": "^11.9.0", "testcontainers": "^11.9.0",
"turbo": "^2.6.1", "turbo": "^2.6.3",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^4.0.15" "vitest": "^4.0.15"
@@ -82,14 +82,14 @@
"axios@>=1.0.0 <1.8.2": ">=1.13.2", "axios@>=1.0.0 <1.8.2": ">=1.13.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1", "brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1", "brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.27.0", "esbuild@<=0.24.2": ">=0.27.1",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5", "form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.10.7", "hono@<4.6.5": ">=4.10.7",
"linkifyjs@<4.3.2": ">=4.3.2", "linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.6", "nanoid@>=4.0.0 <5.0.9": ">=5.1.6",
"prismjs@<1.30.0": ">=1.30.0", "prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0", "proxmox-api>undici": "7.16.0",
"react-is": "^19.2.0", "react-is": "^19.2.1",
"rollup@>=4.0.0 <4.22.4": ">=4.53.3", "rollup@>=4.0.0 <4.22.4": ">=4.53.3",
"sha.js@<=2.4.11": ">=2.4.12", "sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1", "tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",

View File

@@ -43,15 +43,15 @@
"@homarr/translation": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.4.0", "@kubernetes/client-node": "^1.4.0",
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.7.2", "@trpc/client": "^11.7.2",
"@trpc/react-query": "^11.7.2", "@trpc/react-query": "^11.7.2",
"@trpc/server": "^11.7.2", "@trpc/server": "^11.7.2",
"@trpc/tanstack-react-query": "^11.7.2", "@trpc/tanstack-react-query": "^11.7.2",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"superjson": "2.2.6", "superjson": "2.2.6",
"trpc-to-openapi": "^3.1.0", "trpc-to-openapi": "^3.1.0",
"zod": "^4.1.13" "zod": "^4.1.13"

View File

@@ -34,11 +34,11 @@
"@homarr/validation": "workspace:^0.1.0", "@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cookies": "^0.9.1", "cookies": "^0.9.1",
"ldapts": "8.0.9", "ldapts": "8.0.14",
"next": "16.0.7", "next": "16.0.10",
"next-auth": "5.0.0-beta.30", "next-auth": "5.0.0-beta.30",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -25,8 +25,8 @@
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@homarr/api": "workspace:^0.1.0", "@homarr/api": "workspace:^0.1.0",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0" "react-dom": "19.2.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

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",
"esbuild": "^0.27.0", "esbuild": "^0.27.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }

View File

@@ -32,10 +32,10 @@
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dns-caching": "^0.2.7", "dns-caching": "^0.2.7",
"next": "16.0.7", "next": "16.0.10",
"octokit": "^5.0.5", "octokit": "^5.0.5",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"undici": "7.16.0", "undici": "7.16.0",
"zod": "^4.1.13", "zod": "^4.1.13",
"zod-validation-error": "^5.0.0" "zod-validation-error": "^5.0.0"

View File

@@ -29,12 +29,12 @@
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0", "@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0", "@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.12",
"@trpc/client": "^11.7.2", "@trpc/client": "^11.7.2",
"@trpc/server": "^11.7.2", "@trpc/server": "^11.7.2",
"@trpc/tanstack-react-query": "^11.7.2", "@trpc/tanstack-react-query": "^11.7.2",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"react": "19.2.0", "react": "19.2.1",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -53,10 +53,10 @@
"@paralleldrive/cuid2": "^3.1.0", "@paralleldrive/cuid2": "^3.1.0",
"@testcontainers/mysql": "^11.9.0", "@testcontainers/mysql": "^11.9.0",
"@testcontainers/postgresql": "^11.9.0", "@testcontainers/postgresql": "^11.9.0",
"better-sqlite3": "^12.4.1", "better-sqlite3": "^12.5.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"drizzle-kit": "^0.31.7", "drizzle-kit": "^0.31.8",
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.45.0",
"drizzle-zod": "^0.8.3", "drizzle-zod": "^0.8.3",
"mysql2": "3.15.3", "mysql2": "3.15.3",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -69,7 +69,7 @@
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.15.6", "@types/pg": "^8.15.6",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"esbuild": "^0.27.0", "esbuild": "^0.27.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"tsx": "4.20.4", "tsx": "4.20.4",

View File

@@ -31,7 +31,7 @@
"@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": "^8.3.9", "@mantine/core": "^8.3.9",
"react": "19.2.0", "react": "19.2.1",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -46,6 +46,7 @@ export class TransmissionIntegration extends Integration implements IDownloadCli
name: torrent.name, name: torrent.name,
size: torrent.totalSize, size: torrent.totalSize,
sent: torrent.uploadedEver, sent: torrent.uploadedEver,
received: torrent.downloadedEver,
downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined, downSpeed: torrent.percentDone !== 1 ? torrent.rateDownload : undefined,
upSpeed: torrent.rateUpload, upSpeed: torrent.rateUpload,
time: time:

View File

@@ -26,6 +26,8 @@ export const downloadClientItemSchema = z.object({
size: z.number(), size: z.number(),
/** Total uploaded in Bytes, only required for Torrent items */ /** Total uploaded in Bytes, only required for Torrent items */
sent: z.number().optional(), sent: z.number().optional(),
/** Total downloaded in Bytes, only required for Torrent items */
received: z.number().optional(),
/** Download speed in Bytes/s, only required if not complete /** Download speed in Bytes/s, only required if not complete
* (Says 0 only if it should be downloading but isn't) */ * (Says 0 only if it should be downloading but isn't) */
downSpeed: z.number().optional(), downSpeed: z.number().optional(),

View File

@@ -169,17 +169,15 @@ export class PlexIntegration extends Integration implements IMediaServerIntegrat
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> { protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const token = super.getSecretValue("apiKey"); const token = super.getSecretValue("apiKey");
const response = await input.fetchAsync(this.url("/"), { const response = await input.fetchAsync(super.url("/prefs"), {
headers: { headers: {
"X-Plex-Token": token, "X-Plex-Token": token,
Accept: "application/json",
}, },
}); });
if (!response.ok) return TestConnectionError.StatusResult(response); if (!response.ok) return TestConnectionError.StatusResult(response);
const result = await response.text();
await PlexIntegration.parseXmlAsync<PlexResponse>(result);
return { success: true }; return { success: true };
} }

View File

@@ -26,7 +26,7 @@
"dependencies": { "dependencies": {
"@homarr/core": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0",
"superjson": "2.2.6", "superjson": "2.2.6",
"winston": "3.18.3", "winston": "3.19.0",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -36,9 +36,9 @@
"@mantine/core": "^8.3.9", "@mantine/core": "^8.3.9",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"zod": "^4.1.13" "zod": "^4.1.13"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -26,7 +26,7 @@
"@homarr/ui": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.3.9", "@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9", "@mantine/hooks": "^8.3.9",
"react": "19.2.0" "react": "19.2.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -40,9 +40,9 @@
"@mantine/core": "^8.3.9", "@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9", "@mantine/hooks": "^8.3.9",
"adm-zip": "0.5.16", "adm-zip": "0.5.16",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"superjson": "2.2.6", "superjson": "2.2.6",
"zod": "^4.1.13", "zod": "^4.1.13",
"zod-form-data": "^3.0.1" "zod-form-data": "^3.0.1"

View File

@@ -27,9 +27,9 @@
"@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": "^8.3.9", "@mantine/dates": "^8.3.9",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0" "react-dom": "19.2.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -38,9 +38,9 @@
"@mantine/spotlight": "^8.3.9", "@mantine/spotlight": "^8.3.9",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"jotai": "^2.15.2", "jotai": "^2.15.2",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"use-deep-compare-effect": "^1.8.1" "use-deep-compare-effect": "^1.8.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -32,10 +32,10 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"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": "16.0.7", "next": "16.0.10",
"next-intl": "4.5.8", "next-intl": "4.5.8",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0" "react-dom": "19.2.1"
}, },
"devDependencies": { "devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0", "@homarr/eslint-config": "workspace:^0.2.0",

View File

@@ -1939,7 +1939,23 @@
"dockerContainers": { "dockerContainers": {
"name": "Docker stats", "name": "Docker stats",
"description": "Stats of your containers (This widget can only be added with administrator privileges)", "description": "Stats of your containers (This widget can only be added with administrator privileges)",
"option": {}, "option": {
"enableRowSorting": {
"label": "Enable items sorting"
},
"defaultSort": {
"label": "Column used for sorting by default",
"option": {
"name": "Name",
"state": "State",
"cpuUsage": "CPU usage",
"memoryUsage": "Memory usage"
}
},
"descendingDefaultSort": {
"label": "Invert sorting"
}
},
"error": { "error": {
"internalServerError": "Failed to fetch containers stats" "internalServerError": "Failed to fetch containers stats"
} }

View File

@@ -341,7 +341,7 @@
"description": "" "description": ""
}, },
"full-all": { "full-all": {
"label": "", "label": "Acesso completo a aplicativos",
"description": "" "description": ""
} }
} }

View File

@@ -35,9 +35,9 @@
"@mantine/hooks": "^8.3.9", "@mantine/hooks": "^8.3.9",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"svgson": "^5.3.1" "svgson": "^5.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -52,29 +52,29 @@
"@mantine/core": "^8.3.9", "@mantine/core": "^8.3.9",
"@mantine/hooks": "^8.3.9", "@mantine/hooks": "^8.3.9",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "2.27.1", "@tiptap/extension-color": "3.13.0",
"@tiptap/extension-highlight": "2.27.1", "@tiptap/extension-highlight": "3.13.0",
"@tiptap/extension-image": "2.27.1", "@tiptap/extension-image": "3.13.0",
"@tiptap/extension-link": "^2.27.1", "@tiptap/extension-link": "^3.13.0",
"@tiptap/extension-placeholder": "^2.27.1", "@tiptap/extension-placeholder": "^3.13.0",
"@tiptap/extension-table": "2.27.1", "@tiptap/extension-table": "3.13.0",
"@tiptap/extension-table-cell": "2.27.1", "@tiptap/extension-table-cell": "3.13.0",
"@tiptap/extension-table-header": "2.27.1", "@tiptap/extension-table-header": "3.13.0",
"@tiptap/extension-table-row": "2.27.1", "@tiptap/extension-table-row": "3.13.0",
"@tiptap/extension-task-item": "2.27.1", "@tiptap/extension-task-item": "3.13.0",
"@tiptap/extension-task-list": "2.27.1", "@tiptap/extension-task-list": "3.13.0",
"@tiptap/extension-text-align": "2.27.1", "@tiptap/extension-text-align": "3.13.0",
"@tiptap/extension-text-style": "2.27.1", "@tiptap/extension-text-style": "3.13.0",
"@tiptap/extension-underline": "2.27.1", "@tiptap/extension-underline": "3.13.0",
"@tiptap/react": "^2.27.1", "@tiptap/react": "^3.13.0",
"@tiptap/starter-kit": "^2.27.1", "@tiptap/starter-kit": "^3.13.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"mantine-form-zod-resolver": "^1.3.0", "mantine-form-zod-resolver": "^1.3.0",
"mantine-react-table": "2.0.0-beta.9", "mantine-react-table": "2.0.0-beta.9",
"next": "16.0.7", "next": "16.0.10",
"react": "19.2.0", "react": "19.2.1",
"react-dom": "19.2.0", "react-dom": "19.2.1",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"recharts": "^2.15.4", "recharts": "^2.15.4",
"video.js": "^8.23.4", "video.js": "^8.23.4",

View File

@@ -56,6 +56,7 @@ const createColumns = (
t: ReturnType<typeof useScopedI18n<"docker">>, t: ReturnType<typeof useScopedI18n<"docker">>,
): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [ ): MRT_ColumnDef<RouterOutputs["docker"]["getContainers"]["containers"][number]>[] => [
{ {
id: "name",
accessorKey: "name", accessorKey: "name",
header: t("field.name.label"), header: t("field.name.label"),
Cell({ renderedCellValue, row }) { Cell({ renderedCellValue, row }) {
@@ -70,6 +71,7 @@ const createColumns = (
}, },
}, },
{ {
id: "state",
accessorKey: "state", accessorKey: "state",
size: 100, size: 100,
header: t("field.state.label"), header: t("field.state.label"),
@@ -78,6 +80,13 @@ const createColumns = (
}, },
}, },
{ {
id: "cpuUsage",
sortingFn: (rowA, rowB) => {
const cpuUsageA = safeValue(rowA.original.cpuUsage);
const cpuUsageB = safeValue(rowB.original.cpuUsage);
return cpuUsageA - cpuUsageB;
},
accessorKey: "cpuUsage", accessorKey: "cpuUsage",
size: 80, size: 80,
header: t("field.stats.cpu.label"), header: t("field.stats.cpu.label"),
@@ -92,6 +101,13 @@ const createColumns = (
}, },
}, },
{ {
id: "memoryUsage",
sortingFn: (rowA, rowB) => {
const memoryUsageA = safeValue(rowA.original.memoryUsage);
const memoryUsageB = safeValue(rowB.original.memoryUsage);
return memoryUsageA - memoryUsageB;
},
accessorKey: "memoryUsage", accessorKey: "memoryUsage",
size: 80, size: 80,
header: t("field.stats.memory.label"), header: t("field.stats.memory.label"),
@@ -106,9 +122,11 @@ const createColumns = (
}, },
}, },
{ {
id: "actions",
accessorKey: "actions", accessorKey: "actions",
size: 80, size: 80,
header: t("action.title"), header: t("action.title"),
enableSorting: false,
Cell({ row }) { Cell({ row }) {
const utils = clientApi.useUtils(); const utils = clientApi.useUtils();
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
@@ -168,7 +186,7 @@ const createColumns = (
}, },
]; ];
export default function DockerWidget({ width }: WidgetComponentProps<"dockerContainers">) { export default function DockerWidget({ options, width, isEditMode }: WidgetComponentProps<"dockerContainers">) {
const t = useScopedI18n("docker"); const t = useScopedI18n("docker");
const isTiny = width <= 256; const isTiny = width <= 256;
@@ -192,8 +210,8 @@ export default function DockerWidget({ width }: WidgetComponentProps<"dockerCont
enablePagination: false, enablePagination: false,
enableTopToolbar: false, enableTopToolbar: false,
enableBottomToolbar: false, enableBottomToolbar: false,
enableSorting: false,
enableColumnActions: false, enableColumnActions: false,
enableSorting: options.enableRowSorting && !isEditMode,
enableStickyHeader: false, enableStickyHeader: false,
enableColumnOrdering: false, enableColumnOrdering: false,
enableRowSelection: false, enableRowSelection: false,
@@ -203,6 +221,7 @@ export default function DockerWidget({ width }: WidgetComponentProps<"dockerCont
enableFilters: false, enableFilters: false,
enableHiding: false, enableHiding: false,
initialState: { initialState: {
sorting: [{ id: options.defaultSort, desc: options.descendingDefaultSort }],
density: "xs", density: "xs",
}, },
mantinePaperProps: { mantinePaperProps: {

View File

@@ -1,12 +1,35 @@
import { IconBrandDocker, IconServerOff } from "@tabler/icons-react"; import { IconBrandDocker, IconServerOff } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { createWidgetDefinition } from "../definition"; import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options"; import { optionsBuilder } from "../options";
const columnsList = [
"name",
"state",
"cpuUsage",
"memoryUsage",
] as const satisfies (keyof RouterOutputs["docker"]["getContainers"]["containers"][number])[];
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", { export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker, icon: IconBrandDocker,
createOptions() { createOptions() {
return optionsBuilder.from(() => ({})); return optionsBuilder.from((factory) => ({
enableRowSorting: factory.switch({
defaultValue: false,
}),
defaultSort: factory.select({
defaultValue: "name",
options: columnsList.map((value) => ({
value,
label: (t) => t(`widget.dockerContainers.option.defaultSort.option.${value}`),
})),
}),
descendingDefaultSort: factory.switch({
defaultValue: false,
}),
}));
}, },
errors: { errors: {
INTERNAL_SERVER_ERROR: { INTERNAL_SERVER_ERROR: {

View File

@@ -36,6 +36,7 @@ import {
IconX, IconX,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table"; import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table";
import { MantineReactTable, useMantineReactTable } from "mantine-react-table"; import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
@@ -48,6 +49,8 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition"; import type { WidgetComponentProps } from "../definition";
dayjs.extend(relativeTime);
interface QuickFilter { interface QuickFilter {
integrationKinds: string[]; integrationKinds: string[];
statuses: ExtendedDownloadClientItem["state"][]; statuses: ExtendedDownloadClientItem["state"][];
@@ -188,14 +191,14 @@ export default function DownloadClientsWidget({
) )
//Add extrapolated data and actions if user is allowed interaction //Add extrapolated data and actions if user is allowed interaction
.map((item): ExtendedDownloadClientItem => { .map((item): ExtendedDownloadClientItem => {
const received = Math.floor(item.size * item.progress); const received = item.received ?? Math.floor(item.size * item.progress);
const integrationIds = [pair.integration.id]; const integrationIds = [pair.integration.id];
return { return {
integration: pair.integration, integration: pair.integration,
...item, ...item,
category: item.category !== undefined && item.category.length > 0 ? item.category : undefined, category: item.category !== undefined && item.category.length > 0 ? item.category : undefined,
received, received,
ratio: item.sent !== undefined ? item.sent / (received || 1) : undefined, ratio: item.sent !== undefined ? item.sent / item.size : undefined,
//Only add if permission to use mutations //Only add if permission to use mutations
actions: integrationsWithInteractions.includes(pair.integration.id) actions: integrationsWithInteractions.includes(pair.integration.id)
? { ? {
@@ -714,7 +717,10 @@ const ItemInfoModal = ({ items, currentIndex, opened, onClose }: ItemInfoModalPr
/> />
{item.type !== "miscellaneous" && <NormalizedLine itemKey="ratio" values={item.ratio} />} {item.type !== "miscellaneous" && <NormalizedLine itemKey="ratio" values={item.ratio} />}
<NormalizedLine itemKey="added" values={item.added === undefined ? "unknown" : dayjs(item.added).format()} /> <NormalizedLine itemKey="added" values={item.added === undefined ? "unknown" : dayjs(item.added).format()} />
<NormalizedLine itemKey="time" values={item.time !== 0 ? dayjs().add(item.time).format() : "∞"} /> <NormalizedLine
itemKey="time"
values={item.time !== 0 ? dayjs().add(item.time, "milliseconds").fromNow() : "∞"}
/>
<NormalizedLine itemKey="category" values={item.category} /> <NormalizedLine itemKey="category" values={item.category} />
</Stack> </Stack>
)} )}

View File

@@ -41,21 +41,21 @@ import {
IconX, IconX,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { Color } from "@tiptap/extension-color"; import { Color } from "@tiptap/extension-color";
import Highlight from "@tiptap/extension-highlight"; import { Highlight } from "@tiptap/extension-highlight";
import Image from "@tiptap/extension-image"; import { Image } from "@tiptap/extension-image";
import Placeholder from "@tiptap/extension-placeholder"; import { Placeholder } from "@tiptap/extension-placeholder";
import Table from "@tiptap/extension-table"; import { Table } from "@tiptap/extension-table";
import TableCell from "@tiptap/extension-table-cell"; import { TableCell } from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header"; import { TableHeader } from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row"; import { TableRow } from "@tiptap/extension-table-row";
import TaskItem from "@tiptap/extension-task-item"; import { TaskItem } from "@tiptap/extension-task-item";
import TaskList from "@tiptap/extension-task-list"; import { TaskList } from "@tiptap/extension-task-list";
import TextAlign from "@tiptap/extension-text-align"; import { TextAlign } from "@tiptap/extension-text-align";
import TextStyle from "@tiptap/extension-text-style"; import { TextStyle } from "@tiptap/extension-text-style";
import Underline from "@tiptap/extension-underline";
import type { Editor } from "@tiptap/react"; import type { Editor } from "@tiptap/react";
import { BubbleMenu, useEditor } from "@tiptap/react"; import { useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit"; import { BubbleMenu } from "@tiptap/react/menus";
import { StarterKit } from "@tiptap/starter-kit";
import type { Node } from "prosemirror-model"; import type { Node } from "prosemirror-model";
import { clientApi } from "@homarr/api/client"; import { clientApi } from "@homarr/api/client";
@@ -133,7 +133,8 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
}; };
}, },
}), }),
StarterKit, // we use a custom link implementation from mantine
StarterKit.configure({ link: false }),
Table.configure({ Table.configure({
resizable: true, resizable: true,
lastColumnResizable: false, lastColumnResizable: false,
@@ -170,8 +171,9 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
TaskList.configure({ itemTypeName: "taskItem" }), TaskList.configure({ itemTypeName: "taskItem" }),
TextAlign.configure({ types: ["heading", "paragraph"] }), TextAlign.configure({ types: ["heading", "paragraph"] }),
TextStyle, TextStyle,
Underline,
], ],
shouldRerenderOnTransaction: true,
immediatelyRender: false,
content, content,
onUpdate: ({ editor }) => { onUpdate: ({ editor }) => {
setContent(editor.getHTML()); setContent(editor.getHTML());

1965
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,4 +5,4 @@ packages:
minimumReleaseAge: 4320 # Only download deps if they are at least 3 days old (in minutes) minimumReleaseAge: 4320 # Only download deps if they are at least 3 days old (in minutes)
minimumReleaseAgeExclude: minimumReleaseAgeExclude:
- next - next
- "@next/eslint-plugin-next" - "@next/*"

View File

@@ -1,10 +1,12 @@
#!/usr/bin/env bash
# Create sub directories in volume # Create sub directories in volume
mkdir -p /appdata/db mkdir -p /appdata/db
mkdir -p /appdata/redis mkdir -p /appdata/redis
mkdir -p /appdata/trusted-certificates mkdir -p /appdata/trusted-certificates
# Run migrations # Run migrations
if [ $DB_MIGRATIONS_DISABLED = "true" ]; then if [ "$DB_MIGRATIONS_DISABLED" = "true" ]; then
echo "DB migrations are disabled, skipping" echo "DB migrations are disabled, skipping"
else else
echo "Running DB migrations" echo "Running DB migrations"
@@ -20,13 +22,15 @@ export CRON_JOB_API_KEY=$(openssl rand -base64 32)
# 1. Replace the HOSTNAME in the nginx template file # 1. Replace the HOSTNAME in the nginx template file
# 2. Create the nginx configuration file from the template # 2. Create the nginx configuration file from the template
# 3. Start the nginx server # 3. Start the nginx server
export HOSTNAME
envsubst '${HOSTNAME}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf envsubst '${HOSTNAME}' < /etc/nginx/templates/nginx.conf > /etc/nginx/nginx.conf
# Start services in the background and store their PIDs # Start services in the background and store their PIDs
nginx -g 'daemon off;' & nginx -g 'daemon off;' &
NGINX_PID=$! NGINX_PID=$!
if [ $REDIS_IS_EXTERNAL = "true" ]; then if [ "$REDIS_IS_EXTERNAL" = "true" ]; then
echo "Using external Redis server at redis://$REDIS_HOST:$REDIS_PORT" echo "Using external Redis server at redis://$REDIS_HOST:$REDIS_PORT"
REDIS_PID=""
else else
echo "Starting internal Redis server" echo "Starting internal Redis server"
redis-server /app/redis.conf & redis-server /app/redis.conf &
@@ -49,9 +53,11 @@ terminate() {
echo "Received SIGTERM. Shutting down..." echo "Received SIGTERM. Shutting down..."
kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null
wait wait
# kill redis-server last because of logging of other services # kill redis-server last because of logging of other services and only if $REDIS_PID is set
kill -TERM $REDIS_PID 2>/dev/null if [ -n "$REDIS_PID" ]; then
wait kill -TERM $REDIS_PID 2>/dev/null
wait
fi
echo "Shutdown complete." echo "Shutdown complete."
exit 0 exit 0
} }
@@ -61,4 +67,4 @@ trap terminate TERM INT
# Wait for all processes # Wait for all processes
wait $NEXTJS_PID wait $NEXTJS_PID
terminate terminate

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -17,9 +17,9 @@
}, },
"prettier": "@homarr/prettier-config", "prettier": "@homarr/prettier-config",
"dependencies": { "dependencies": {
"@next/eslint-plugin-next": "16.0.7", "@next/eslint-plugin-next": "16.0.10",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-config-turbo": "^2.6.1", "eslint-config-turbo": "^2.6.3",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.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",