Compare commits

..
Author SHA1 Message Date
ajnart e18a040686 style: auto-fix lint and format 2026-07-15 09:31:02 +00:00
ajnart 789e71096e fix(widgets): add per-widget refetchIntervalSeconds to WidgetDefinition
The v1.69.0 caching refactor replaced Redis pub/sub push with a global
30s client-side refetchInterval. This caused fast-refresh widgets like
health-monitoring (Dashdot, 5s) to only update every 30s.

Adds refetchIntervalSeconds field to WidgetDefinition. Each widget
declares its own refresh rate alongside its other config. The TRPC
provider iterates all widget definitions and calls setQueryDefaults
with the widget's queryKey (or auto-derived from widget kind).

Widgets whose kind doesn't match their tRPC path set queryKey
explicitly (e.g. dnsHoleSummary -> widget.dnsHole, systemResources ->
widget.healthMonitoring). Beszel already does this for cache sharing.

Removed inline refetchInterval from docker widget component since it's
now on the definition.

Fixes #6268
2026-07-15 11:29:24 +02:00
homarr-renovate[bot] a0034b2cfc chore(deps): update dependency @immich/sdk to ^3.0.2 (#6339)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 21:01:58 +00:00
homarr-renovate[bot] 4a297180fd chore(deps): update dependency lucide-react to ^1.24.0 (#6334)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 19:23:26 +00:00
Thomas 5fa3a3313a Merge pull request #6337 from homarr-labs/fix/ts7-nextjs-build
fix(docker): pin TypeScript to v6 to fix Next.js 16.2.10 build in CI
2026-07-14 18:11:38 +02:00
Thomas Camlong 29ee87c4aa fix(docker): pin typescript to v6 via alias to fix Next.js 16.2.10 build 2026-07-14 18:11:15 +02:00
Thomas 94a247e24a fix(docker): remove --offline flag from pnpm install step (#6335) 2026-07-14 17:19:51 +02:00
homarr-renovate[bot] 6d37710fec chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.1.4 (#6333)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 11:26:55 +00:00
homarr-renovate[bot] c016beb2e5 chore(deps): update dependency vite to ^8.1.4 (#6332)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 09:06:17 +00:00
homarr-renovate[bot] f9e1b85c6a chore(deps): update dependency posthog-node to ^5.40.0 (#6314)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 06:14:41 +00:00
homarr-renovate[bot] 38ba19e665 chore(deps): update dependency oxlint to ^1.73.0 (#6306)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 03:22:16 +00:00
homarr-renovate[bot] f55c99d2d6 chore(deps): update dependency posthog-js to ^1.399.0 (#6331)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-14 00:00:38 +00:00
homarr-renovate[bot] c5e8b83d8a chore(deps): update vitest monorepo to ^4.1.10 (#6286)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 23:01:00 +00:00
homarr-renovate[bot] d52d9cfed2 chore(deps): update dependency posthog-js to ^1.398.7 (#6319)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 21:58:26 +00:00
Thomas 93325642aa Merge pull request #6324 from homarr-labs/renovate/major-github-artifact-actions 2026-07-13 22:12:32 +02:00
Thomas 165e94abbc Merge pull request #6328 from homarr-labs/fix-pnpm-fetch-build 2026-07-13 22:12:11 +02:00
homarr-renovate[bot] 2e3127e766 chore(deps): update docker/dockerfile docker tag to v1.25 (#6321) 2026-07-13 22:10:35 +02:00
Thomas d37326d905 Merge pull request #6322 from homarr-labs/renovate/actions-cache-6-x 2026-07-13 22:09:57 +02:00
homarr-renovate[bot] e51e39ff38 chore(deps): update dependency typescript to v7 (#6325) 2026-07-13 22:09:30 +02:00
homarr-renovate[bot] 6c075b6d74 chore(deps): update dependency mysql2 to v3.22.6 (#6310)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-07-13 18:45:08 +00:00
ajnart b2f63c6e0f fix(docker): drop --frozen-lockfile from install step per pnpm fetch docs
The pnpm fetch docs recommend `pnpm install -r --offline` without
--frozen-lockfile. The --offline flag already enforces no registry
access, making --frozen-lockfile redundant.

https://pnpm.io/cli/fetch
2026-07-13 20:24:59 +02:00
ajnart 8c2d73e8af fix(docker): resolve pnpm fetch crash with patchedDependencies
PR #6296 introduced pnpm fetch in the Dockerfile for Docker cache
optimization. However, pnpm fetch crashes with ENOENT when
patchedDependencies are configured with nodeLinker: hoisted because
applyPatchToDir tries to chdir into node_modules/<pkg> which doesn't
exist during fetch (only the content-addressable store is populated).

Fix: temporarily switch nodeLinker to isolated during pnpm fetch so
patches apply inside the virtual store (node_modules/.pnpm/...) which
IS created by pnpm fetch. Restore hoisted before the install step so
the final node_modules layout stays flat.

Refs: pnpm/pnpm#5268, pnpm/pnpm#8726
2026-07-13 20:06:28 +02:00
homarr-renovate[bot] 1c0ff8930e chore(deps): update actions/upload-artifact action to v7 2026-07-13 16:39:27 +00:00
homarr-renovate[bot] e2b8355639 chore(deps): update actions/cache action to v6 2026-07-13 16:39:10 +00:00
52 changed files with 861 additions and 903 deletions

No files matched your search

+1 -1
View File
@@ -317,7 +317,7 @@ jobs:
- name: Delete GHCR package version
if: '!env.ACT'
uses: actions/github-script@v9
uses: actions/github-script@v8
with:
script: |
const packageName = "homarr-test";
+88 -15
View File
@@ -14,18 +14,91 @@ permissions:
pull-requests: write
jobs:
release:
# Permissions must be set on the calling job: a reusable workflow receives
# the default GITHUB_TOKEN permissions unless the caller job grants them here.
permissions:
contents: write
pull-requests: write
uses: ./.github/workflows/deployment-release-pr.yml
secrets: inherit
with:
release-label: "Beta release"
version-suffix: "-beta"
semver-branch: next
head: next
base: beta
send-notifications: ${{ github.event.inputs.send-notifications == 'true' }}
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: next
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}-beta (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to beta
MERGE_COMMIT_MESSAGE: "chore(release): release ${{ steps.semver.outputs.next }}-beta"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The beta release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
@@ -15,9 +15,9 @@ on:
default: true
description: Send notifications
# Least-privilege default; each job widens only what it needs.
permissions:
contents: read
contents: write # Required to update package.json version
packages: write # Required for pushing to GHCR
env:
SKIP_ENV_VALIDATION: true
@@ -34,8 +34,6 @@ jobs:
name: Create tag and release
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # commit package.json/changelog, push tags, update dev/next
env:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
@@ -63,9 +61,19 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
persist-credentials: false
- name: Setup
- uses: pnpm/action-setup@v6
if: env.SKIP_RELEASE == 'false'
uses: ./tooling/github/setup
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.18.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
if: env.SKIP_RELEASE == 'false'
run: |
pnpm install
- name: Run Semantic Release
if: env.SKIP_RELEASE == 'false'
env:
@@ -108,9 +116,6 @@ jobs:
needs: release
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read # checkout the repository
packages: write # push image by digest to GHCR
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
@@ -155,9 +160,6 @@ jobs:
needs: release
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read # checkout the repository
packages: write # push image by digest to GHCR
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
@@ -202,8 +204,6 @@ jobs:
needs: [release, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # upload build artifacts as release assets
steps:
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
@@ -220,8 +220,6 @@ jobs:
needs: [release, build-arm64]
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
permissions:
contents: write # upload build artifacts as release assets
steps:
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
@@ -237,9 +235,6 @@ jobs:
needs: [release, build-amd64, build-arm64, extract-asset-amd64, extract-asset-arm64]
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write # mark GitHub release as published (undraft)
packages: write # create multi-arch image tags in GHCR
env:
NEXT_VERSION: ${{ needs.release.outputs.version }}
DEPLOY_LATEST: ${{ github.ref_name == 'main' }}
-155
View File
@@ -1,155 +0,0 @@
name: "[Deployment] Release PR (reusable)"
# Reusable workflow that opens a release PR from `head` into `base`, auto-approves
# it and (for non-major bumps) merges it. Used by the beta and weekly release
# workflows, which differ only in branch, version suffix and notification wording.
on:
workflow_call:
inputs:
release-label:
description: 'Human label used in notifications, e.g. "Beta release"'
required: true
type: string
title-prefix:
description: 'Text inserted before "release" in the PR title/commit, e.g. "automatic "'
required: false
default: ""
type: string
version-suffix:
description: 'Suffix appended to the computed version, e.g. "-beta"'
required: false
default: ""
type: string
body-prefix:
description: "Extra HTML prepended to the PR body"
required: false
default: ""
type: string
semver-branch:
description: "Branch used to compute the next version"
required: true
type: string
head:
description: "PR head branch"
required: true
type: string
base:
description: "PR base branch"
required: true
type: string
send-notifications:
description: "Whether to send Discord notifications"
required: false
default: true
type: boolean
jobs:
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
permissions:
contents: write
pull-requests: write
steps:
- name: Discord notification
if: inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "${{ inputs.release-label }} has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: ${{ inputs.semver-branch }}
- name: Create pull request
id: create-pull-request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: "${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
run: >-
gh pr create
--title "chore(release): ${{ inputs.title-prefix }}release ${VERSION}"
--body "${{ inputs.body-prefix }}Detected change to be \`${{ steps.semver.outputs.bump }}\`<br/>Bump version from \`${{ steps.semver.outputs.current }}\` to \`${VERSION}\`"
--base ${{ inputs.base }}
--head ${{ inputs.head }}
--label automerge
- name: Discord notification
if: inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to the base branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for release branches
MERGE_COMMIT_MESSAGE: "chore(release): ${{ inputs.title-prefix }}release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}${{ inputs.version-suffix }}"
- name: Discord Fail Notification
if: failure() && inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The ${{ inputs.release-label }} workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+88 -17
View File
@@ -17,20 +17,91 @@ permissions:
pull-requests: write
jobs:
release:
# Permissions must be set on the calling job: a reusable workflow receives
# the default GITHUB_TOKEN permissions unless the caller job grants them here.
permissions:
contents: write
pull-requests: write
uses: ./.github/workflows/deployment-release-pr.yml
secrets: inherit
with:
release-label: "Automatic release"
title-prefix: "automatic "
body-prefix: "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>"
semver-branch: dev
head: dev
base: main
# Scheduled runs have no inputs, so notifications stay off unless manually dispatched.
send-notifications: ${{ github.event.inputs.send-notifications == 'true' }}
create-and-merge-pr:
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: dev
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to main
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+9 -4
View File
@@ -15,6 +15,9 @@ jobs:
update-contributors:
runs-on: ubuntu-latest
timeout-minutes: 2
strategy:
matrix:
node-version: [22]
steps:
- name: Obtain token
id: obtainToken
@@ -30,11 +33,13 @@ jobs:
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Install Node with mise
uses: jdx/mise-action@v4
- uses: pnpm/action-setup@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
# This script only uses Node built-ins (fs/promises), so no dependency
# install is needed.
- name: Run update script
run: node ./scripts/update-contributors.mjs
+1
View File
@@ -0,0 +1 @@
24.18.0
+18 -8
View File
@@ -7,16 +7,26 @@ WORKDIR /app
RUN apk add --no-cache libc6-compat curl bash && apk update
RUN corepack enable pnpm
# NOTE: We intentionally do NOT use `pnpm fetch` here. pnpm 11.10.0 applies
# `patchedDependencies` patches during the fetch phase, before packages are
# unpacked into node_modules, which crashes with ENOENT (chdir into a
# not-yet-existing node_modules path). Installing directly from the full
# context with the store cache mounted keeps builds fast without that break.
COPY . .
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY patches ./patches
# Workaround for pnpm/pnpm#5268: pnpm fetch crashes when patchedDependencies
# are configured with nodeLinker: hoisted. The applyPatchToDir function tries
# to chdir into node_modules/<pkg> which doesn't exist during fetch (only the
# content-addressable store is populated). By temporarily switching to the
# isolated linker, patches apply inside the virtual store (node_modules/.pnpm/...)
# which IS created by pnpm fetch. The original hoisted linker is restored
# before the install step so the final node_modules layout stays flat.
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm config set store-dir /pnpm/store && \
pnpm install --recursive --frozen-lockfile
sed -i 's/nodeLinker: hoisted/nodeLinker: isolated/' pnpm-workspace.yaml && \
pnpm fetch && \
sed -i 's/nodeLinker: isolated/nodeLinker: hoisted/' pnpm-workspace.yaml
COPY . .
# Follow the pnpm fetch pattern from https://pnpm.io/cli/fetch
# --frozen-lockfile is omitted as recommended by the pnpm fetch docs
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm install --recursive
ARG SKIP_ENV_VALIDATION='true'
ARG CI='true'
@@ -2,6 +2,7 @@
import type { PropsWithChildren } from "react";
import { useState } from "react";
import type { QueryKey } from "@tanstack/react-query";
import { QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
@@ -33,6 +34,7 @@ import {
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "@homarr/common/env";
import { showWarningNotification } from "@homarr/notifications";
import { widgetImports } from "@homarr/widgets";
import { createWidgetQueryPersister } from "./query-cache-persister";
@@ -90,6 +92,12 @@ export function TRPCReactProvider(props: PropsWithChildren) {
},
});
client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs });
for (const { definition } of Object.values(widgetImports)) {
const def = definition as { refetchIntervalSeconds?: number; queryKey?: QueryKey; kind: string };
if (!def.refetchIntervalSeconds) continue;
const key = def.queryKey ?? [["widget", def.kind]];
client.setQueryDefaults(key, { refetchInterval: def.refetchIntervalSeconds * 1000 });
}
return client;
});
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("anchorNote", {
icon: IconNotes,
queryKey: [["widget", "anchorNotes"]],
refetchIntervalSeconds: 15,
supportedIntegrations: ["anchor"],
createOptions() {
return optionsBuilder.from((factory) => ({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("archiveTeamWarrior", {
icon: IconArchive,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
showBroadcastMessage: factory.switch({
@@ -14,6 +14,7 @@ const hideUnlessAudiobookshelf = {
export const { definition, componentLoader } = createWidgetDefinition("audioStats", {
icon: IconHeadphones,
refetchIntervalSeconds: 600,
supportedIntegrations: ["navidrome", "audiobookshelf"],
integrationsRequired: true,
maxIntegrations: 1,
+1
View File
@@ -9,6 +9,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("calendar", {
icon: IconCalendar,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
releaseType: factory.multiSelect({
+1
View File
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("coolify", {
icon: IconCloud,
refetchIntervalSeconds: 30,
createOptions() {
return optionsBuilder.from((factory) => ({
showServers: factory.switch({
+1
View File
@@ -70,6 +70,7 @@ export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition ext
export interface WidgetDefinition {
icon: TablerIcon;
queryKey?: QueryKey;
refetchIntervalSeconds?: number;
supportedIntegrations?: IntegrationKind[];
integrationsRequired?: boolean;
maxIntegrations?: number;
@@ -9,6 +9,8 @@ export const widgetKind = "dnsHoleControls";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconDeviceGamepad,
queryKey: [["widget", "dnsHole"]],
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showToggleAllButtons: factory.switch({
@@ -9,6 +9,8 @@ export const widgetKind = "dnsHoleSummary";
export const { definition, componentLoader } = createWidgetDefinition(widgetKind, {
icon: IconAd,
queryKey: [["widget", "dnsHole"]],
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
usePiHoleColors: factory.switch({
+2 -10
View File
@@ -184,13 +184,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
const t = useScopedI18n("docker");
const isTiny = width <= 256;
const {
data,
refetch,
isFetching,
} = clientApi.docker.getContainers.useQuery(undefined, {
refetchInterval: 30_000,
});
const { data, refetch, isFetching } = clientApi.docker.getContainers.useQuery(undefined, {});
const containers = data?.containers ?? [];
const timestamp = useMemo(() => data?.timestamp ?? new Date(), [data?.timestamp]);
const relativeTime = useTimeAgo(timestamp);
@@ -304,9 +298,7 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
{t("table.totalMemory", { memory: formatBytes(totals.memory) })}
</Text>
<Tooltip
label={t("table.refresh.lastUpdated", { when: relativeTime })}
>
<Tooltip label={t("table.refresh.lastUpdated", { when: relativeTime })}>
<ActionIcon
size="sm"
variant="transparent"
+1
View File
@@ -26,6 +26,7 @@ const columnTranslationKeyMap = {
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
icon: IconBrandDocker,
queryKey: [["docker", "getContainers"]],
refetchIntervalSeconds: 20,
createOptions() {
return optionsBuilder.from((factory) => ({
columns: factory.multiSelect({
+1
View File
@@ -33,6 +33,7 @@ const columnsSort = columnsList.filter((column) =>
export const { definition, componentLoader } = createWidgetDefinition("downloads", {
icon: IconDownload,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(
(factory) => ({
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("firewall", {
icon: IconWall,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(() => ({}));
},
@@ -8,6 +8,7 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("healthMonitoring", {
icon: IconHeartRateMonitor,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from(
(factory) => ({
@@ -8,6 +8,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
queryKey: [["widget", "immich"]],
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"],
integrationsRequired: true,
createOptions() {
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("immich-serverStats", {
icon: IconGraphFilled,
queryKey: [["widget", "immich"]],
refetchIntervalSeconds: 900,
supportedIntegrations: ["immich"],
integrationsRequired: true,
createOptions() {
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("indexerManager", {
icon: IconReportSearch,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
openIndexerSiteInNewTab: factory.switch({
@@ -5,6 +5,8 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("mediaReleases", {
icon: IconTicket,
queryKey: [["widget", "mediaRelease"]],
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
layout: factory.select({
@@ -8,6 +8,8 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestList", {
icon: IconZoomQuestion,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
linksTargetNewTab: factory.switch({
@@ -7,6 +7,8 @@ import { createWidgetDefinition } from "../../definition";
export const { componentLoader, definition } = createWidgetDefinition("mediaRequests-requestStats", {
icon: IconChartBar,
queryKey: [["widget", "mediaRequests"]],
refetchIntervalSeconds: 60,
createOptions() {
return {};
},
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("mediaServer", {
icon: IconVideo,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
@@ -11,6 +11,7 @@ export const views = ["workers", "queue", "statistics"] as const;
export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", {
icon: IconTransform,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
defaultView: factory.select({
@@ -6,6 +6,7 @@ import { optionsBuilder } from "../../options";
export const { componentLoader, definition } = createWidgetDefinition("minecraftServerStatus", {
icon: IconBrandMinecraft,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
title: factory.text({ defaultValue: "" }),
@@ -7,6 +7,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerStatus", {
icon: IconTopologyFull,
queryKey: [["widget", "networkController"]],
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
content: factory.select({
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("networkControllerSummary", {
icon: IconTopologyFull,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from(() => ({}));
},
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("notifications", {
icon: IconMessage,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
hideLogos: factory.switch({ defaultValue: false }),
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("paperlessNgx", {
icon: IconFileText,
refetchIntervalSeconds: 900,
supportedIntegrations: ["paperlessNgx"],
integrationsRequired: true,
createOptions() {
+1
View File
@@ -14,6 +14,7 @@ const relativeDateSchema = z
export const { definition, componentLoader } = createWidgetDefinition("releases", {
icon: IconRocket,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
newReleaseWithin: factory.text({
+1
View File
@@ -12,6 +12,7 @@ import { optionsBuilder } from "../options";
*/
export const { definition, componentLoader } = createWidgetDefinition("rssFeed", {
icon: IconRss,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
feedUrls: factory.multiText({
@@ -7,6 +7,8 @@ import { optionsBuilder } from "../../options";
export const { definition, componentLoader } = createWidgetDefinition("smartHome-entityState", {
icon: IconBinaryTree,
queryKey: [["widget", "smartHome"]],
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
entityId: factory.text({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("speedtestTracker", {
icon: IconSpeedboat,
refetchIntervalSeconds: 600,
createOptions() {
return optionsBuilder.from(
(factory) => ({
+1
View File
@@ -13,6 +13,7 @@ const timeIntervalOptions = stockPriceTimeFrames.interval;
export const { definition, componentLoader } = createWidgetDefinition("stockPrice", {
icon: IconBuildingBank,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from((factory) => ({
stock: factory.text({
@@ -6,6 +6,8 @@ import { createStorageVolumeMultiSelectOptions } from "../storage-volume-options
export const { definition, componentLoader } = createWidgetDefinition("systemDisks", {
icon: IconServer2,
queryKey: [["widget", "healthMonitoring"]],
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "synology"],
createOptions() {
return optionsBuilder.from(
@@ -14,6 +14,8 @@ const labelDisplayModeOptions = {
export const { definition, componentLoader } = createWidgetDefinition("systemResources", {
icon: IconGraphFilled,
queryKey: [["widget", "healthMonitoring"]],
refetchIntervalSeconds: 5,
supportedIntegrations: ["dashDot", "openmediavault", "truenas", "unraid", "glances", "synology"],
createOptions() {
return optionsBuilder.from((factory) => ({
+1
View File
@@ -8,6 +8,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("timetable", {
icon: IconBusStop,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
baseUrl: factory.text({
+1
View File
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("tracearr", {
icon: IconActivityHeartbeat,
refetchIntervalSeconds: 5,
createOptions() {
return optionsBuilder.from((factory) => ({
showStreams: factory.switch({
+1
View File
@@ -8,6 +8,7 @@ export const timeFrameValues = ["today", "24h", "7d", "30d", "month", "lastMonth
export const { definition, componentLoader } = createWidgetDefinition("umami", {
icon: IconChartBar,
refetchIntervalSeconds: 300,
supportedIntegrations: ["umami"],
createOptions() {
return optionsBuilder.from(
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("ups", {
icon: IconBatteryCharging,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
showBattery: factory.switch({
@@ -5,6 +5,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("uptimeKuma", {
icon: IconHeartbeat,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from((factory) => ({
showAverageUptime: factory.switch({
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { componentLoader, definition } = createWidgetDefinition("vpn", {
icon: IconShieldLock,
refetchIntervalSeconds: 300,
createOptions() {
return optionsBuilder.from(() => ({}));
},
+1
View File
@@ -7,6 +7,7 @@ import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("weather", {
icon: IconCloud,
refetchIntervalSeconds: 60,
createOptions() {
return optionsBuilder.from(
(factory) => ({
+564 -642
View File
File diff suppressed because it is too large. Load diff
+12 -12
View File
@@ -18,7 +18,7 @@ catalog:
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
"@homarr/node-unifi": ^2.6.0
"@immich/sdk": ^3.0.1
"@immich/sdk": ^3.0.2
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@modelcontextprotocol/sdk": 1.29.0
@@ -93,11 +93,11 @@ catalog:
"@types/video.js": ^7.3.58
"@types/ws": ^8.18.1
"@types/xml2js": ^0.4.14
posthog-js: ^1.398.2
posthog-node: ^5.39.2
posthog-js: ^1.399.0
posthog-node: ^5.40.0
"@vitejs/plugin-react": ^6.0.3
"@vitest/coverage-v8": ^4.1.9
"@vitest/ui": ^4.1.9
"@vitest/coverage-v8": ^4.1.10
"@vitest/ui": ^4.1.10
"@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0
@@ -139,20 +139,20 @@ catalog:
jsonpath-plus: ^10.4.0
ldapts: 8.1.8
lodash.clonedeep: ^4.5.0
lucide-react: ^1.23.0
lucide-react: ^1.24.0
"mantine-contextmenu": ^9.3.1
mantine-datatable: ^9.3.1
mantine-react-table: npm:mantine-react-table-open@9.0.3
mcp-handler: ^1.1.0
maria2: ^0.4.1
mysql2: 3.22.5
mysql2: 3.22.6
next: 16.2.10
next-auth: 5.0.0-beta.31
next-intl: 4.13.1
node-loader: ^2.1.0
octokit: ^5.0.5
oxfmt: ^0.58.0
oxlint: ^1.72.0
oxlint: ^1.73.0
pg: ^8.22.0
postcss-preset-mantine: ^1.18.0
prismjs: ^1.30.0
@@ -177,13 +177,13 @@ catalog:
tsdav: ^2.3.0
tsx: 4.20.4
turbo: ^2.10.4
typescript: ^6.0.3
vite: ^8.1.2
typescript: "npm:@typescript/typescript6@^6.0.2"
vite: ^8.1.4
undici: 7.28.0
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.9
vite-tsconfig-paths: ^6.1.1
vitest: ^4.1.9
vitest: ^4.1.10
winston: 3.19.0
ws: ^8.21.0
xml2js: ^0.6.2
@@ -229,7 +229,7 @@ overrides:
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.3"
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.3"
"tmp@<=0.2.3": ">=0.2.7"
"vite@>=5.0.0 <=5.4.18": ">=8.1.3"
"vite@>=5.0.0 <=5.4.18": ">=8.1.4"
patchedDependencies:
"@types/node-unifi": "patches/@types__node-unifi.patch"
"trpc-to-openapi": "patches/trpc-to-openapi.patch"
+5 -20
View File
@@ -4,26 +4,11 @@ description: "Common setup steps for Actions"
runs:
using: composite
steps:
# Node (and any other tools) are provisioned from mise.toml, the single
# source of truth for tool versions.
- name: Install tools with mise
uses: jdx/mise-action@v4
- name: Install pnpm
uses: pnpm/action-setup@v6
- name: Locate pnpm store
shell: bash
run: echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV"
- name: Cache pnpm store
uses: actions/cache@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with:
path: ${{ env.PNPM_STORE_PATH }}
key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
pnpm-store-${{ runner.os }}-
node-version: 24.18.0
cache: "pnpm"
- name: Install dependencies
shell: bash
- shell: bash
run: pnpm install --frozen-lockfile