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