Compare commits

...
72 Commits
Author SHA1 Message Date
Releases Homarr faf2525de5 chore(release): 1.32.0 [skip ci] 2025-08-08 19:17:25 +00:00
homarr-releases[bot] 3343468c55 chore(release): automatic release v1.32.0 2025-08-08 19:15:45 +00:00
ManuelandMeier Lukas 27bd93fa8a ci: update integrations in readme (#3740)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-08 20:14:58 +02:00
homarr-renovate[bot] e636e02d6a chore(deps): update dependency @types/node to ^22.17.1 (#3811)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-08 17:19:35 +00:00
Meier Lukas 7f474683a5 fix(opnsense): switch secrets to api-key key and secret for easier understanding (#3744) 2025-08-08 17:53:03 +02:00
homarr-update-contributors[bot] 4a07b3f866 chore: update contributors (#3807)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-08-08 15:22:45 +00:00
homarr-renovate[bot] d23196b430 fix(deps): update tanstack-query monorepo to ^5.84.2 (#3810)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-08 14:25:43 +00:00
homarr-renovate[bot] ef40e75818 fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.1 (#3809)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-08 14:25:01 +00:00
homarr-renovate[bot] 60accd66f6 fix(deps): update dependency video.js to ^8.23.4 (#3808)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-08 13:42:08 +00:00
homarr-renovate[bot] 4b6c69d48a chore(deps): update dependency node to v22.18.0 (#3762)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-07 17:23:48 +00:00
homarr-renovate[bot] c73ce1f704 fix(deps): update dependency @gitbeaker/rest to ^43.4.0 (#3806)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-07 16:27:10 +00:00
homarr-renovate[bot] ffa629064c fix(deps): update mantine monorepo to ^8.2.4 (#3803)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-07 10:23:56 +00:00
homarr-crowdin[bot] dde41f6baf chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-07 00:58:54 +00:00
homarr-renovate[bot] d32703910e fix(deps): update nextjs monorepo to v15.4.6 (#3799)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-06 20:25:09 +00:00
homarr-renovate[bot] a4914d09fd fix(deps): update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.0 (#3797)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-06 14:26:59 +00:00
homarr-renovate[bot] ed950e9302 fix(deps): update testcontainers-node monorepo to ^11.5.1 (#3796)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-06 12:40:05 +00:00
homarr-renovate[bot] bb681f7b5e fix(deps): update dependency sass to ^1.90.0 (#3794)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-05 22:23:21 +00:00
Meier Lukas 72e976ee5e fix: some integrations handle parse errors wrong (#3746) 2025-08-05 20:24:23 +02:00
homarr-renovate[bot] e6be8e1db3 fix(deps): update mantine monorepo to ^8.2.3 (#3789)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-05 18:06:59 +00:00
homarr-crowdin[bot] 5d494111df chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-05 00:59:38 +00:00
3ee408bf53 feat: system resources widget (#3538)
* feat: add system resources widget

* Update packages/widgets/src/system-resources/index.ts

Co-authored-by: Andre Silva <32734153+Aandree5@users.noreply.github.com>

* fix: system resources not updating

* refactor: improve logic in component

* fix: tooltip overflow

* feat: add label with last value

* feat: hide label when hovering

* fix: formatting

* fix: lint

* fix: formatting

* fix: wrong redis channel used for opnsense

---------

Co-authored-by: Andre Silva <32734153+Aandree5@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-04 20:12:28 +00:00
homarr-renovate[bot] 1b5ccb5293 fix(deps): update dependency typescript-eslint to ^8.39.0 (#3781)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-04 18:30:20 +00:00
homarr-renovate[bot] cc6d421e7d fix(deps): update trpc monorepo to ^11.4.4 (#3777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-04 17:26:56 +00:00
homarr-renovate[bot] c8d59d88e5 fix(deps): update dependency jotai to ^2.13.0 (#3763)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-04 14:27:15 +00:00
homarr-crowdin[bot] 3dbf085e59 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-04 01:02:42 +00:00
Meier Lukas ff03220960 chore(docs): update sitemap 2025-08-03 18:47:01 +02:00
Manuel 001f7a2a16 fix: not hardcode hero banner scroll (#3741) 2025-08-03 12:01:38 +02:00
homarr-crowdin[bot] 2780b1e794 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-03 09:52:05 +00:00
Meier Lukas 59c092f800 fix(groups): saving permissions with none activated causes error (#3739) 2025-08-02 19:43:47 +02:00
homarr-renovate[bot] 4bc053cc17 fix(deps): update testcontainers-node monorepo to ^11.5.0 (#3738)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-02 15:47:10 +00:00
Meier Lukas baa652f314 feat(api): add info endpoint (#3737) 2025-08-02 17:42:12 +02:00
homarr-crowdin[bot] 2465676713 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-08-02 00:54:53 +00:00
homarr-renovate[bot] e45bef317a fix(deps): update dependency trpc-to-openapi to ^2.4.0 (#3734)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 23:22:05 +00:00
Meier Lukas 5d0966f466 chore(issue-template): add version 1.31.0 2025-08-01 22:41:53 +02:00
Releases Homarr 9c377957ec chore(release): 1.31.0 [skip ci] 2025-08-01 19:18:12 +00:00
homarr-releases[bot] ec3d560ad9 chore(release): automatic release v1.31.0 2025-08-01 19:17:21 +00:00
1dc1854cbf feat: OPNsense integration and widget (#3424)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: deepsource-io[bot] <42547082+deepsource-io[bot]@users.noreply.github.com>
2025-08-01 18:34:06 +02:00
homarr-renovate[bot] 511551aee7 fix(deps): update tanstack-query monorepo to ^5.84.1 (#3729)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 12:38:37 +00:00
homarr-update-contributors[bot] a9de2c39ff chore: update contributors (#3728)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2025-08-01 12:38:17 +00:00
homarr-renovate[bot] a14c3a2095 chore(deps): update dependency dotenv-cli to v10 (#3713)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 14:03:08 +02:00
Andre Silva f63e64627c feat(releases-widget): add Mark as read action to mark releases as seen (#3676) 2025-08-01 14:02:07 +02:00
homarr-renovate[bot]andMeier Lukas d66cccb0db chore(deps): update dependency typescript to ^5.9.2 (#3722)
* chore(deps): update dependency typescript to ^5.9.2

* fix: typecheck issue

---------

Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-01 13:59:35 +02:00
homarr-renovate[bot] 0bfb8d845b chore(deps): update dependency @types/bcrypt to v6 (#3656)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 13:59:06 +02:00
Andre Silva 3f3d31eb6a refactor: renamed GitHubPackages integration to GitHubContainerRegistry, and all related code (#3727) 2025-08-01 12:37:00 +02:00
Meier Lukas 9a69644732 chore(docs): update sitemap (#3726) 2025-08-01 12:08:52 +02:00
ManuelandMeier Lukas 8eb44c9f23 revert: "feat(ping): ignore certificate error and show request durati… (#3680)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-01 12:08:34 +02:00
Andre SilvaandMeier Lukas c92bbd2da0 feat(releases-widget): add new providers, Github Packages, linuxserver.io and Quay (#3607)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-01 11:13:20 +02:00
Andre Silva 03adf538b0 feat(releases-widget): Allow to display only the icon (#3695) 2025-08-01 11:13:05 +02:00
Andre Silva 949c7a40d9 feat(releases-widget): add Select/Deselect All to import from docker screen (#3674) 2025-08-01 11:12:56 +02:00
NikeLaosClericusandMeier Lukas 6a819e38ed fix: characters are not considered Symbols when entering a new password (#3689)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-08-01 11:12:25 +02:00
homarr-renovate[bot] 4b1699ab33 chore(deps): update node.js to v22.18.0 (#3724)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 08:36:47 +00:00
homarr-renovate[bot] ab5e08a4b0 fix(deps): update dependency swagger-ui-react to ^5.27.1 (#3725)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-08-01 08:28:32 +00:00
homarr-renovate[bot] e7b21697ef fix(deps): update tanstack-query monorepo to ^5.84.0 (#3723)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 20:23:56 +00:00
homarr-renovate[bot] f4e37134d8 fix(deps): update tanstack-query monorepo to ^5.83.1 (#3719)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 15:23:39 +00:00
homarr-renovate[bot] 58e686765c chore(deps): update pnpm to v10.14.0 (#3720)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 14:25:03 +00:00
homarr-renovate[bot] 4f9ecae104 fix(deps): update dependency ioredis to v5.7.0 (#3718)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 11:21:21 +00:00
homarr-renovate[bot] 002063f6e1 chore(deps): update dependency proxmox-api&gt;undici to v7.13.0 (#3716)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 11:20:30 +00:00
homarr-renovate[bot] 183564176c fix(deps): update mantine monorepo to ^8.2.2 (#3715)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 10:24:24 +00:00
homarr-renovate[bot] b0bbd0d87a fix(deps): update dependency undici to v7.13.0 (#3717)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-31 09:29:01 +00:00
homarr-renovate[bot] d0a112c791 fix(deps): update nextjs monorepo to v15.4.5 (#3712)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-29 21:22:03 +00:00
homarr-renovate[bot] 952d4f71c9 fix(deps): update dependency mysql2 to v3.14.3 (#3711)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-29 17:24:35 +00:00
homarr-renovate[bot] 6126fac915 fix(deps): update dependency drizzle-orm to ^0.44.4 (#3708)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-29 16:26:51 +00:00
homarr-renovate[bot] a358377578 chore(deps): update react monorepo (#3707)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-29 16:26:08 +00:00
homarr-renovate[bot] 8a9bca39ea chore(deps): update dependency @types/node to ^22.17.0 (#3710)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-29 15:25:32 +00:00
homarr-renovate[bot] 0dc1f7cf14 fix(deps): update react monorepo to v19.1.1 (#3706)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-28 15:24:23 +00:00
homarr-renovate[bot] 1aecf4f90d fix(deps): update dependency ldapts to v8.0.9 (#3703)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-28 06:31:57 +00:00
homarr-crowdin[bot] 851a75aa54 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-07-27 01:01:59 +00:00
homarr-renovate[bot] 91ae3ec84c chore(deps): update dependency cross-env to v10 (#3690)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-26 20:19:24 +02:00
homarr-renovate[bot] 6996f6d1a9 chore(deps): update node.js to v22.17.1 (#3640)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-26 20:06:46 +02:00
homarr-crowdin[bot] a16e9032f4 chore(lang): update translations from crowdin (#3646)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2025-07-26 20:04:40 +02:00
Manuel 4b57d7b269 chore: update issue template for bug 2025-07-25 21:28:24 +02:00
homarr-renovate[bot] 6d677828d4 chore(deps): update dependency eslint to ^9.32.0 (#3687)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2025-07-25 19:20:19 +00:00
178 changed files with 8101 additions and 2150 deletions

No files matched your search

+2
View File
@@ -31,6 +31,8 @@ body:
label: Version
description: What version of Homarr are you running?
options:
- 1.31.0
- 1.30.1
- 1.30.0
- 1.29.0
- 1.28.1
@@ -60,7 +60,7 @@ jobs:
- uses: actions/setup-node@v4
if: env.SKIP_RELEASE == 'false'
with:
node-version: 22
node-version: 22.18.0
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
@@ -0,0 +1,62 @@
name: Update integration list
on:
workflow_dispatch: { }
push:
paths:
- packages/definitions/src/integration.ts
branches:
- dev
permissions:
pull-requests: write
jobs:
update-readme:
concurrency:
group: update-integration
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- name: Obtain token
id: obtainToken
uses: tibdex/github-app-token@v2
with:
private_key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
app_id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
- name: Checkout code
uses: actions/checkout@v4
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Setup
uses: ./tooling/github/setup
- run: pnpm run scripts:update-readme-integrations
- name: Commit changes
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
git config --global user.email "210161987+homarr-update-contributors[bot]@users.noreply.github.com"
git config --global user.name "Homarr Update Contributors"
git add .
git commit -m "chore: update integrations list readme"
- name: Create Pull Request
id: create-pull-request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.obtainToken.outputs.token }}
branch: update-integrations-readme
base: dev
title: "chore: update integrations list readme"
delete-branch: true
body: |
This PR automatically updates the list of integrations of Homarr in the README.md
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Enable auto-merge
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
gh pr merge ${{steps.create-pull-request.outputs.pull-request-number}} --auto --squash
+1 -1
View File
@@ -1 +1 @@
22.17.1
22.18.0
+59
View File
@@ -1,3 +1,62 @@
## [1.32.0](https://github.com/homarr-labs/homarr/compare/v1.31.0...v1.32.0) (2025-08-08)
### Features
* **api:** add info endpoint ([#3737](https://github.com/homarr-labs/homarr/issues/3737)) ([baa652f](https://github.com/homarr-labs/homarr/commit/baa652f31493aad1490cf8e5d50bb04dc0006da1))
* system resources widget ([#3538](https://github.com/homarr-labs/homarr/issues/3538)) ([3ee408b](https://github.com/homarr-labs/homarr/commit/3ee408bf53c45f0b2abf04449b3653ed3e9cf225))
### Bug Fixes
* **deps:** update dependency @gitbeaker/rest to ^43.4.0 ([#3806](https://github.com/homarr-labs/homarr/issues/3806)) ([c73ce1f](https://github.com/homarr-labs/homarr/commit/c73ce1f704aa1fcbc6beac4cfa80b080b6800373))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.0 ([#3797](https://github.com/homarr-labs/homarr/issues/3797)) ([a4914d0](https://github.com/homarr-labs/homarr/commit/a4914d09fd6c9fa6a602f9f76b71c096369a34cd))
* **deps:** update dependency @ianvs/prettier-plugin-sort-imports to ^4.6.1 ([#3809](https://github.com/homarr-labs/homarr/issues/3809)) ([ef40e75](https://github.com/homarr-labs/homarr/commit/ef40e7581896df4371c1f13487f3830bb57f6886))
* **deps:** update dependency jotai to ^2.13.0 ([#3763](https://github.com/homarr-labs/homarr/issues/3763)) ([c8d59d8](https://github.com/homarr-labs/homarr/commit/c8d59d88e5872783e767156eb063f329901fff6f))
* **deps:** update dependency sass to ^1.90.0 ([#3794](https://github.com/homarr-labs/homarr/issues/3794)) ([bb681f7](https://github.com/homarr-labs/homarr/commit/bb681f7b5ebb4a0858dac40eacfcc809c04b3d5e))
* **deps:** update dependency trpc-to-openapi to ^2.4.0 ([#3734](https://github.com/homarr-labs/homarr/issues/3734)) ([e45bef3](https://github.com/homarr-labs/homarr/commit/e45bef317a6ffae05d7a1685b1e6164e2ed09921))
* **deps:** update dependency typescript-eslint to ^8.39.0 ([#3781](https://github.com/homarr-labs/homarr/issues/3781)) ([1b5ccb5](https://github.com/homarr-labs/homarr/commit/1b5ccb529358836682a1cccd4a3da14f612884b9))
* **deps:** update dependency video.js to ^8.23.4 ([#3808](https://github.com/homarr-labs/homarr/issues/3808)) ([60accd6](https://github.com/homarr-labs/homarr/commit/60accd66f635ddb2f8c7a73faa865ee47aa40721))
* **deps:** update mantine monorepo to ^8.2.3 ([#3789](https://github.com/homarr-labs/homarr/issues/3789)) ([e6be8e1](https://github.com/homarr-labs/homarr/commit/e6be8e1db3d843e3d0e5b6d54c64999fd45f103d))
* **deps:** update mantine monorepo to ^8.2.4 ([#3803](https://github.com/homarr-labs/homarr/issues/3803)) ([ffa6290](https://github.com/homarr-labs/homarr/commit/ffa629064cdf202849d6f9c0ce645c6077c63e49))
* **deps:** update nextjs monorepo to v15.4.6 ([#3799](https://github.com/homarr-labs/homarr/issues/3799)) ([d327039](https://github.com/homarr-labs/homarr/commit/d32703910e0228a70a55a84ab712d6bd58fe7149))
* **deps:** update tanstack-query monorepo to ^5.84.2 ([#3810](https://github.com/homarr-labs/homarr/issues/3810)) ([d23196b](https://github.com/homarr-labs/homarr/commit/d23196b43034349d92cb9277633dd06b15ee0b7d))
* **deps:** update testcontainers-node monorepo to ^11.5.0 ([#3738](https://github.com/homarr-labs/homarr/issues/3738)) ([4bc053c](https://github.com/homarr-labs/homarr/commit/4bc053cc17d5a38db4255f9913e389fe86831676))
* **deps:** update testcontainers-node monorepo to ^11.5.1 ([#3796](https://github.com/homarr-labs/homarr/issues/3796)) ([ed950e9](https://github.com/homarr-labs/homarr/commit/ed950e9302450f33150d9800bc66e712bf8f6724))
* **deps:** update trpc monorepo to ^11.4.4 ([#3777](https://github.com/homarr-labs/homarr/issues/3777)) ([cc6d421](https://github.com/homarr-labs/homarr/commit/cc6d421e7db52b994e9e62fa1f828c72b5a20d72))
* **groups:** saving permissions with none activated causes error ([#3739](https://github.com/homarr-labs/homarr/issues/3739)) ([59c092f](https://github.com/homarr-labs/homarr/commit/59c092f8000f9a4e9fd70bde4176697dabb2e1c9))
* not hardcode hero banner scroll ([#3741](https://github.com/homarr-labs/homarr/issues/3741)) ([001f7a2](https://github.com/homarr-labs/homarr/commit/001f7a2a165fade9deaafb4cb319aa27926685e5))
* **opnsense:** switch secrets to api-key key and secret for easier understanding ([#3744](https://github.com/homarr-labs/homarr/issues/3744)) ([7f47468](https://github.com/homarr-labs/homarr/commit/7f474683a5e5b9ada978c055e57cd2b4114e17e0))
* some integrations handle parse errors wrong ([#3746](https://github.com/homarr-labs/homarr/issues/3746)) ([72e976e](https://github.com/homarr-labs/homarr/commit/72e976ee5ea28e549b7674c71b9a284e905e455b))
## [1.31.0](https://github.com/homarr-labs/homarr/compare/v1.30.1...v1.31.0) (2025-08-01)
### Features
* OPNsense integration and widget ([#3424](https://github.com/homarr-labs/homarr/issues/3424)) ([1dc1854](https://github.com/homarr-labs/homarr/commit/1dc1854cbfa39f4f22755cb84e1d6882e7e73721))
* **releases-widget:** add `Mark as read` action to mark releases as seen ([#3676](https://github.com/homarr-labs/homarr/issues/3676)) ([f63e646](https://github.com/homarr-labs/homarr/commit/f63e64627c79004b1e1bbb5a525fdde58f803da7))
* **releases-widget:** add `Select/Deselect All` to `import from docker` screen ([#3674](https://github.com/homarr-labs/homarr/issues/3674)) ([949c7a4](https://github.com/homarr-labs/homarr/commit/949c7a40d934b2d58a24bf9842f44703e73474ee))
* **releases-widget:** add new providers, Github Packages, linuxserver.io and Quay ([#3607](https://github.com/homarr-labs/homarr/issues/3607)) ([c92bbd2](https://github.com/homarr-labs/homarr/commit/c92bbd2da062221c3b5464838ad715c31a9d07b0))
* **releases-widget:** Allow to display only the icon ([#3695](https://github.com/homarr-labs/homarr/issues/3695)) ([03adf53](https://github.com/homarr-labs/homarr/commit/03adf538b03270404956dc16b191069f913e54d1))
### Bug Fixes
* characters are not considered Symbols when entering a new password ([#3689](https://github.com/homarr-labs/homarr/issues/3689)) ([6a819e3](https://github.com/homarr-labs/homarr/commit/6a819e38edca9f95b926a0162abdb6b873ddec08))
* **deps:** update dependency drizzle-orm to ^0.44.4 ([#3708](https://github.com/homarr-labs/homarr/issues/3708)) ([6126fac](https://github.com/homarr-labs/homarr/commit/6126fac915197c19ea6d448da8c4fc1b06b1383b))
* **deps:** update dependency ioredis to v5.7.0 ([#3718](https://github.com/homarr-labs/homarr/issues/3718)) ([4f9ecae](https://github.com/homarr-labs/homarr/commit/4f9ecae10406be4f6176947b517706843bd1c06d))
* **deps:** update dependency ldapts to v8.0.9 ([#3703](https://github.com/homarr-labs/homarr/issues/3703)) ([1aecf4f](https://github.com/homarr-labs/homarr/commit/1aecf4f90d4776bfa93b6f601fca16a7da8ffaba))
* **deps:** update dependency mysql2 to v3.14.3 ([#3711](https://github.com/homarr-labs/homarr/issues/3711)) ([952d4f7](https://github.com/homarr-labs/homarr/commit/952d4f71c9197cd88fa44ce5dbcf7d89f4c482d3))
* **deps:** update dependency swagger-ui-react to ^5.27.1 ([#3725](https://github.com/homarr-labs/homarr/issues/3725)) ([ab5e08a](https://github.com/homarr-labs/homarr/commit/ab5e08a4b09845e3aa4818214bc0f9ce830a2942))
* **deps:** update dependency undici to v7.13.0 ([#3717](https://github.com/homarr-labs/homarr/issues/3717)) ([b0bbd0d](https://github.com/homarr-labs/homarr/commit/b0bbd0d87ad6df7a9be83296060d8b0be6c091e0))
* **deps:** update mantine monorepo to ^8.2.2 ([#3715](https://github.com/homarr-labs/homarr/issues/3715)) ([1835641](https://github.com/homarr-labs/homarr/commit/183564176c9421faea70dff199d0b6f81325bf70))
* **deps:** update nextjs monorepo to v15.4.5 ([#3712](https://github.com/homarr-labs/homarr/issues/3712)) ([d0a112c](https://github.com/homarr-labs/homarr/commit/d0a112c7919cbcff9aaf2cc6b2dec8fd562246db))
* **deps:** update react monorepo to v19.1.1 ([#3706](https://github.com/homarr-labs/homarr/issues/3706)) ([0dc1f7c](https://github.com/homarr-labs/homarr/commit/0dc1f7cf14c0ca49d5eb8d37060773c3dab4834a))
* **deps:** update tanstack-query monorepo to ^5.83.1 ([#3719](https://github.com/homarr-labs/homarr/issues/3719)) ([f4e3713](https://github.com/homarr-labs/homarr/commit/f4e37134d806dac719012cb4dee19fb7e5daf7f2))
* **deps:** update tanstack-query monorepo to ^5.84.0 ([#3723](https://github.com/homarr-labs/homarr/issues/3723)) ([e7b2169](https://github.com/homarr-labs/homarr/commit/e7b21697efb0f90c3539152beaea94416cbb6fef))
* **deps:** update tanstack-query monorepo to ^5.84.1 ([#3729](https://github.com/homarr-labs/homarr/issues/3729)) ([511551a](https://github.com/homarr-labs/homarr/commit/511551aee7d5be9dadb70c596c5addbb646651cb))
### Reverts
* "feat(ping): ignore certificate error and show request durati… ([#3680](https://github.com/homarr-labs/homarr/issues/3680)) ([8eb44c9](https://github.com/homarr-labs/homarr/commit/8eb44c9f23584078b22605dac569387713dcd173))
## [1.30.1](https://github.com/homarr-labs/homarr/compare/v1.30.0...v1.30.1) (2025-07-25)
### Bug Fixes
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:22.17.0-alpine AS base
FROM node:22.18.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
+24 -24
View File
@@ -50,21 +50,21 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "^8.2.1",
"@mantine/core": "^8.2.1",
"@mantine/dropzone": "^8.2.1",
"@mantine/hooks": "^8.2.1",
"@mantine/modals": "^8.2.1",
"@mantine/tiptap": "^8.2.1",
"@mantine/colors-generator": "^8.2.4",
"@mantine/core": "^8.2.4",
"@mantine/dropzone": "^8.2.4",
"@mantine/hooks": "^8.2.4",
"@mantine/modals": "^8.2.4",
"@mantine/tiptap": "^8.2.4",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-query-next-experimental": "^5.83.0",
"@trpc/client": "^11.4.3",
"@trpc/next": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@tanstack/react-query": "^5.84.2",
"@tanstack/react-query-devtools": "^5.84.2",
"@tanstack/react-query-next-experimental": "^5.84.2",
"@trpc/client": "^11.4.4",
"@trpc/next": "^11.4.4",
"@trpc/react-query": "^11.4.4",
"@trpc/server": "^11.4.4",
"@xterm/addon-canvas": "^0.7.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
@@ -74,18 +74,18 @@
"dotenv": "^17.2.1",
"flag-icons": "^7.5.0",
"glob": "^11.0.3",
"jotai": "^2.12.5",
"jotai": "^2.13.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.4.4",
"next": "15.4.6",
"postcss-preset-mantine": "^1.18.0",
"prismjs": "^1.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.89.2",
"sass": "^1.90.0",
"superjson": "2.2.2",
"swagger-ui-react": "^5.27.0",
"swagger-ui-react": "^5.27.1",
"use-deep-compare-effect": "^1.8.1",
"zod": "^3.25.76"
},
@@ -94,15 +94,15 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.16.4",
"@types/node": "^22.17.1",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.8",
"@types/react-dom": "19.1.6",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.0",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
@@ -1,37 +1,16 @@
import { Box, Grid, GridCol, Group, Image, Stack, Title } from "@mantine/core";
import { splitToNChunks } from "@homarr/common";
import { integrationDefs } from "@homarr/definitions";
import classes from "./hero-banner.module.css";
const icons = [
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sabnzbd.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/deluge.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/radarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/sonarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/lidarr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/pihole.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/dashdot.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/overseerr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/plex.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyfin.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/homeassistant.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/freshrss.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/readarr.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/transmission.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/qbittorrent.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/nzbget.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/openmediavault.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/docker.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/jellyseerr.svg",
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/assets/adguardhome.svg",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/tdarr.png",
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/prowlarr.svg",
];
const icons = Object.values(integrationDefs)
.filter((int) => int.name !== "Mock")
.map((int) => int.iconUrl);
const countIconGroups = 3;
const animationDurationInSeconds = 12;
const animationDurationInSeconds = icons.length;
const arrayInChunks = splitToNChunks(icons, countIconGroups);
export const HeroBanner = () => {
@@ -61,12 +40,26 @@ export const HeroBanner = () => {
}}
>
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image key={`grid-column-${columnIndex}-scroll-1-${index}`} src={icon} radius="md" w={50} h={50} />
<Image
key={`grid-column-${columnIndex}-scroll-1-${index}`}
src={icon}
radius="md"
fit={"contain"}
w={50}
h={50}
/>
))}
{/* This is used for making the animation seem seamless */}
{arrayInChunks[columnIndex]?.map((icon, index) => (
<Image key={`grid-column-${columnIndex}-scroll-2-${index}`} src={icon} radius="md" w={50} h={50} />
<Image
key={`grid-column-${columnIndex}-scroll-2-${index}`}
src={icon}
radius="md"
fit={"contain"}
w={50}
h={50}
/>
))}
</Stack>
</GridCol>
@@ -19,4 +19,6 @@ export const integrationSecretIcons = {
tokenId: IconGrid3x3,
personalAccessToken: IconPasswordUser,
topic: IconMessage,
opnsenseApiKey: IconKey,
opnsenseApiSecret: IconPassword,
} satisfies Record<IntegrationSecretKind, TablerIcon>;
@@ -23,7 +23,7 @@ export async function GET(_req: NextRequest, props: { params: Promise<{ id: stri
headers.set("Content-Type", image.contentType);
headers.set("Content-Length", image.content.length.toString());
return new NextResponse(image.content, {
return new NextResponse(new Uint8Array(image.content), {
status: 200,
headers,
});
@@ -21,6 +21,12 @@ interface BoardItemContentProps {
item: SectionItem;
}
const getOverflowFromKind = (kind: SectionItem["kind"]) => {
if (kind === "iframe") return "hidden";
if (kind === "systemResources") return "visible";
return undefined;
};
export const BoardItemContent = ({ item }: BoardItemContentProps) => {
const { ref, width, height } = useElementSize<HTMLDivElement>();
const board = useRequiredBoard();
@@ -41,7 +47,7 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
root: {
"--opacity": board.opacity / 100,
containerType: "size",
overflow: item.kind === "iframe" ? "hidden" : undefined,
overflow: getOverflowFromKind(item.kind),
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
},
}}
+6 -5
View File
@@ -30,6 +30,7 @@
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
@@ -38,18 +39,18 @@
"dotenv": "^17.2.1",
"fastify": "^5.4.0",
"superjson": "2.2.2",
"undici": "7.12.0"
"undici": "7.13.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.16.4",
"dotenv-cli": "^8.0.0",
"@types/node": "^22.17.1",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.8",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"prettier": "^3.6.2",
"tsx": "4.20.3",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -35,8 +35,8 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.8",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
+266 -45
View File
@@ -1,5 +1,4 @@
<!-- Project Title -->
[![Banner](./banner.png)](https://homarr.dev/)
![](img/logo/2340450-2-title.png)
<!-- Badges -->
<p align="center">
@@ -20,10 +19,6 @@
<!-- Links -->
<p align="center">
<a href="https://demo.homarr.dev/">
<strong>Demo ✨</strong>
</a>
•
<a href="https://homarr.dev/docs/category/installation-1/">
<strong>Install 💻</strong>
</a> •
@@ -35,8 +30,7 @@
</a>
</p>
[![Features Section](./section-features.png)](https://homarr.dev/)
![](img/headers/features.png)
- 🖌️ Highly customizable with an extensive drag and drop grid system
- ✨ Integrates seamlessly with your favorite self-hosted applications
@@ -53,44 +47,268 @@
<br/>
<br/>
[![Widgets & Integrations Section](./section-widgets-and-integrations.png)](https://homarr.dev/docs/category/widgets)
![](img/headers/integrations.png)
Homarr has a [built-in collection of widgets and integrations](https://homarr.dev/docs/category/integrations), that connect to your applications and enable you to control them directly from the dashboard.
<!-- AUTO_GENERATE_INTEGRATION_LIST_START -->
- 📥 **Torrent clients**
- [Deluge](https://homarr.dev/docs/integrations/torrent#deluge)
- [Transmission](https://homarr.dev/docs/integrations/torrent#transmission)
- [qBittorent](https://homarr.dev/docs/integrations/torrent#qbittorrent-integration)
- 📥 **Usenet clients**
- [SABnzbd](https://homarr.dev/docs/integrations/usenet#sabnzbd)
- [NZBGet](https://homarr.dev/docs/integrations/usenet#nzbget)
- 📺 **Media servers**
- [Plex](https://homarr.dev/docs/integrations/media-server/#plex)
- [Jellyfin](https://homarr.dev/docs/integrations/media-server#jellyfin-and-emby)
- 📚 **Media collection managers**
- [Sonarr](https://homarr.dev/docs/integrations/servarr#sonarr)
- [Radarr](https://homarr.dev/docs/integrations/servarr#radarr)
- [Lidarr](https://homarr.dev/docs/integrations/servarr#lidarr)
- [Readarr](https://homarr.dev/docs/integrations/servarr#readarr)
- 🎞️ **Media request managers**
- [Overseerr](https://homarr.dev/docs/integrations/media-requester)
- [Jellyseerr](https://homarr.dev/docs/integrations/media-requester)
- 🚫 **DNS ad-blockers**
- [Pihole](https://homarr.dev/docs/integrations/dns#pihole)
- [AdGuard Home](https://homarr.dev/docs/integrations/dns#adguard-home)
- 🖥️ **Monitoring**
- [Dash.](https://homarr.dev/docs/integrations/hardware)
- [OpenMediaVault.](https://homarr.dev/docs/integrations/hardware/#openmediavault)
- [Proxmox.](https://homarr.dev/docs/integrations/hardware/#proxmox)
- 🐳 **Container management**:
- [Docker](https://homarr.dev/docs/integrations/containers)
<div align="center">
<table>
<tbody>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/adguard-home" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg" alt="AdGuard Home" width="90" height="90" />
<br/>
<p align="center">AdGuard<br/>Home</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/aria2" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg" alt="Aria2" width="90" height="90" />
<br/>
<p align="center">Aria2</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/codeberg" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg" alt="Codeberg" width="90" height="90" />
<br/>
<p align="center">Codeberg</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/dash-dot" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png" alt="Dash." width="90" height="90" />
<br/>
<p align="center">Dash.</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/deluge" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg" alt="Deluge" width="90" height="90" />
<br/>
<p align="center">Deluge</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/docker-hub" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg" alt="Docker Hub" width="90" height="90" />
<br/>
<p align="center">Docker<br/>Hub</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/emby" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg" alt="Emby" width="90" height="90" />
<br/>
<p align="center">Emby</p>
</a>
</td></tr>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/github" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg" alt="Github" width="90" height="90" />
<br/>
<p align="center">Github</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/github" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg" alt="GitHub Container Registry" width="90" height="90" />
<br/>
<p align="center">GitHub<br/>Container<br/>Registry</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/gitlab" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg" alt="Gitlab" width="90" height="90" />
<br/>
<p align="center">Gitlab</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/home-assistant" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg" alt="Home Assistant" width="90" height="90" />
<br/>
<p align="center">Home<br/>Assistant</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/jellyfin" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg" alt="Jellyfin" width="90" height="90" />
<br/>
<p align="center">Jellyfin</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/jellyseerr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg" alt="Jellyseerr" width="90" height="90" />
<br/>
<p align="center">Jellyseerr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/lidarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg" alt="Lidarr" width="90" height="90" />
<br/>
<p align="center">Lidarr</p>
</a>
</td></tr>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/linux-server-io" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg" alt="LinuxServer.io" width="90" height="90" />
<br/>
<p align="center">LinuxServer.io</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/nextcloud" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg" alt="Nextcloud" width="90" height="90" />
<br/>
<p align="center">Nextcloud</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/npm" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg" alt="NPM" width="90" height="90" />
<br/>
<p align="center">NPM</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/ntfy" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg" alt="ntfy" width="90" height="90" />
<br/>
<p align="center">ntfy</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/nzbget" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg" alt="NZBGet" width="90" height="90" />
<br/>
<p align="center">NZBGet</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/open-media-vault" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/openmediavault.svg" alt="OpenMediaVault" width="90" height="90" />
<br/>
<p align="center">OpenMediaVault</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/opnsense" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg" alt="OPNsense" width="90" height="90" />
<br/>
<p align="center">OPNsense</p>
</a>
</td></tr>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/overseerr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg" alt="Overseerr" width="90" height="90" />
<br/>
<p align="center">Overseerr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/pi-hole" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg" alt="Pi-hole" width="90" height="90" />
<br/>
<p align="center">Pi-hole</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/plex" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg" alt="Plex" width="90" height="90" />
<br/>
<p align="center">Plex</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/prowlarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg" alt="Prowlarr" width="90" height="90" />
<br/>
<p align="center">Prowlarr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/proxmox" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg" alt="Proxmox" width="90" height="90" />
<br/>
<p align="center">Proxmox</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/q-bittorent" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg" alt="qBittorrent" width="90" height="90" />
<br/>
<p align="center">qBittorrent</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/quay" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png" alt="Quay" width="90" height="90" />
<br/>
<p align="center">Quay</p>
</a>
</td></tr>
<tr><td align="center">
<a href="https://homarr.dev/docs/integrations/radarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg" alt="Radarr" width="90" height="90" />
<br/>
<p align="center">Radarr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/readarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg" alt="Readarr" width="90" height="90" />
<br/>
<p align="center">Readarr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/sabnzbd" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg" alt="SABnzbd" width="90" height="90" />
<br/>
<p align="center">SABnzbd</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/sonarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg" alt="Sonarr" width="90" height="90" />
<br/>
<p align="center">Sonarr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/tdarr" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png" alt="Tdarr" width="90" height="90" />
<br/>
<p align="center">Tdarr</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/transmission" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg" alt="Transmission" width="90" height="90" />
<br/>
<p align="center">Transmission</p>
</a>
</td>
<td align="center">
<a href="https://homarr.dev/docs/integrations/unifi-controller" target="_blank" rel="noreferrer noopener">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png" alt="Unifi Controller" width="90" height="90" />
<br/>
<p align="center">Unifi<br/>Controller</p>
</a>
</td></tr>
</tbody>
</table>
</div>
<!-- AUTO_GENERATE_INTEGRATION_LIST_END -->
<br/>
<br/>
[![Installation Section](./section-installation.png)](https://homarr.dev/docs/category/installation-1)
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
![](img/headers/installation.png)
<h2>
<a href="https://homarr.dev/docs/category/installation-1/">
@@ -101,12 +319,15 @@ Since we are updating Homarr very frequently, we recommend reading our official
<br/>
<br/>
[![Contribute Section](./section-contribute.png)](https://opencollective.com/homarr)
![](img/headers/contribute.png)
<br/>
Homarr is a free to use open source project that is maintained by volunteers and developers from all over the world. We publish under the ``Apache License 2.0`` license which allows commercial usage. We invest multiple hours daily in to providing support, developing Homarr, integrating to third party software and more. We also pay for licensing and server hosting fees.
Please consider to help us cover these costs to enable the future development of Homarr. Thank you!
Homarr is a free-to-use open source project maintained by volunteers and developers from all over the world.
We publish under the `Apache License 2.0` license which allows commercial usage.
We invest multiple hours daily in providing support, developing Homarr, integrating to third party software and more.
We also pay for licensing and server hosting fees.
Please consider helping us cover these costs to enable the future development of Homarr. Thank you!
<h2>
<a href="https://opencollective.com/homarr">
@@ -117,9 +338,9 @@ Please consider to help us cover these costs to enable the future development of
You can also support us by helping with [translating the entire project](https://homarr.dev/docs/community/translations) to as many languages as possible or contributing directly to the code or documentation. Please read our [Contribution Guidelines](/CONTRIBUTING.md). All contributions, regardless of their size or scope, are welcome and highly appreciated! Thank you ❤️
## Sponsors
Thanks to your generous sponsors we can continue to build Homarr. Check them out for high quality and easy to use development tools.
Thanks to your generous sponsors, we can continue to build Homarr. Check them out for high-quality and easy-to-use development tools.
Feel free to contact us at homarr-labs@proton.me if you wish to become a sponsor.
[![Covered by Argos Visual Testing](https://argos-ci.com/badge-large.svg)](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) \
[![Supported by PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=homarr-v1)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

+10 -9
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.30.1",
"version": "1.32.0",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -27,7 +27,8 @@
"test:e2e": "cross-env NODE_ENV=development CI=true vitest e2e",
"test:ui": "cross-env NODE_ENV=development CI=true vitest --exclude e2e --ui --coverage.enabled",
"typecheck": "turbo typecheck",
"with-env": "dotenv -e .env --"
"with-env": "dotenv -e .env --",
"scripts:update-readme-integrations": "tsx ./scripts/update-integration-list.mts"
},
"prettier": "@homarr/prettier-config",
"devDependencies": {
@@ -38,25 +39,25 @@
"@semantic-release/github": "^11.0.3",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.0.3",
"@testcontainers/redis": "^11.4.0",
"@testcontainers/redis": "^11.5.1",
"@turbo/gen": "^2.5.5",
"@vitejs/plugin-react": "^4.7.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^7.0.3",
"cross-env": "^10.0.0",
"jsdom": "^26.1.0",
"prettier": "^3.6.2",
"semantic-release": "^24.2.7",
"testcontainers": "^11.4.0",
"testcontainers": "^11.5.1",
"turbo": "^2.5.5",
"typescript": "^5.8.3",
"typescript": "^5.9.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.13.1",
"packageManager": "pnpm@10.14.0",
"engines": {
"node": ">=22.17.1"
"node": ">=22.18.0"
},
"pnpm": {
"onlyBuiltDependencies": [
@@ -71,7 +72,7 @@
"tree-sitter-json"
],
"overrides": {
"proxmox-api>undici": "7.12.0"
"proxmox-api>undici": "7.13.0"
},
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [
+2 -2
View File
@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+12 -11
View File
@@ -35,30 +35,31 @@
"@homarr/log": "workspace:^",
"@homarr/old-import": "workspace:^0.1.0",
"@homarr/old-schema": "workspace:^0.1.0",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.83.0",
"@trpc/client": "^11.4.3",
"@trpc/react-query": "^11.4.3",
"@trpc/server": "^11.4.3",
"@trpc/tanstack-react-query": "^11.4.3",
"@tanstack/react-query": "^5.84.2",
"@trpc/client": "^11.4.4",
"@trpc/react-query": "^11.4.4",
"@trpc/server": "^11.4.4",
"@trpc/tanstack-react-query": "^11.4.4",
"lodash.clonedeep": "^4.5.0",
"next": "15.4.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.4.6",
"react": "19.1.1",
"react-dom": "19.1.1",
"superjson": "2.2.2",
"trpc-to-openapi": "^2.3.2",
"trpc-to-openapi": "^2.4.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
+2
View File
@@ -8,6 +8,7 @@ import { groupRouter } from "./router/group";
import { homeRouter } from "./router/home";
import { iconsRouter } from "./router/icons";
import { importRouter } from "./router/import/import-router";
import { infoRouter } from "./router/info";
import { integrationRouter } from "./router/integration/integration-router";
import { inviteRouter } from "./router/invite";
import { kubernetesRouter } from "./router/kubernetes/router/kubernetes-router";
@@ -47,6 +48,7 @@ export const appRouter = createTRPCRouter({
media: mediaRouter,
updateChecker: updateCheckerRouter,
certificates: certificateRouter,
info: infoRouter,
});
// export type definition of API
+8 -6
View File
@@ -277,12 +277,14 @@ export const groupRouter = createTRPCRouter({
await ctx.db.delete(groupPermissions).where(eq(groupPermissions.groupId, input.groupId));
await ctx.db.insert(groupPermissions).values(
input.permissions.map((permission) => ({
groupId: input.groupId,
permission,
})),
);
if (input.permissions.length > 0) {
await ctx.db.insert(groupPermissions).values(
input.permissions.map((permission) => ({
groupId: input.groupId,
permission,
})),
);
}
}),
transferOwnership: permissionRequiredProcedure
.requiresPermission("admin")
+16
View File
@@ -0,0 +1,16 @@
import z from "zod";
import packageJson from "../../../../package.json";
import { createTRPCRouter, protectedProcedure } from "../trpc";
export const infoRouter = createTRPCRouter({
getInfo: protectedProcedure
.input(z.void())
.output(z.object({ version: z.string() }))
.meta({ openapi: { method: "GET", path: "/api/info", tags: ["info"] } })
.query(() => {
return {
version: packageJson.version,
};
}),
});
@@ -0,0 +1,53 @@
import { describe, expect, test, vi } from "vitest";
import type { Session } from "@homarr/auth";
import { createDb } from "@homarr/db/test";
import * as ping from "@homarr/ping";
import { appRouter } from "../../widgets/app";
// Mock the auth module to return an empty session
vi.mock("@homarr/auth", () => ({ auth: () => ({}) as Session }));
vi.mock("@homarr/ping", () => ({ sendPingRequestAsync: async () => await Promise.resolve(null) }));
describe("ping should call sendPingRequestAsync with url and return result", () => {
test("ping with error response should return error and url", async () => {
// Arrange
const spy = vi.spyOn(ping, "sendPingRequestAsync");
const url = "http://localhost";
const db = createDb();
const caller = appRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
spy.mockImplementation(() => Promise.resolve({ error: "error" }));
// Act
const result = await caller.ping({ url });
// Assert
expect(result.url).toBe(url);
expect("error" in result).toBe(true);
});
test("ping with success response should return statusCode and url", async () => {
// Arrange
const spy = vi.spyOn(ping, "sendPingRequestAsync");
const url = "http://localhost";
const db = createDb();
const caller = appRouter.createCaller({
db,
deviceType: undefined,
session: null,
});
spy.mockImplementation(() => Promise.resolve({ statusCode: 200, durationMs: 123 }));
// Act
const result = await caller.ping({ url });
// Assert
expect(result.url).toBe(url);
expect("statusCode" in result).toBe(true);
});
});
+17 -13
View File
@@ -1,12 +1,20 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod";
import { pingUrlChannel } from "@homarr/redis";
import { pingRequestHandler } from "@homarr/request-handler/ping";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const appRouter = createTRPCRouter({
ping: publicProcedure.input(z.object({ url: z.string() })).query(async ({ input }) => {
const pingResult = await sendPingRequestAsync(input.url);
return {
url: input.url,
...pingResult,
};
}),
updatedPing: publicProcedure
.input(
z.object({
@@ -15,20 +23,16 @@ export const appRouter = createTRPCRouter({
)
.subscription(async ({ input }) => {
await pingUrlChannel.addAsync(input.url);
const innerHandler = pingRequestHandler.handler({ url: input.url });
const pingResult = await sendPingRequestAsync(input.url);
return observable<{ url: string; statusCode: number; durationMs: number } | { url: string; error: string }>(
(emit) => {
// Run ping request in background
void innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then(({ data }) => {
emit.next({ url: input.url, ...data });
});
const unsubscribe = innerHandler.subscribe((pingResponse) => {
emit.next({
url: input.url,
...pingResponse,
});
emit.next({ url: input.url, ...pingResult });
const unsubscribe = pingChannel.subscribe((message) => {
// Only emit if same url
if (message.url !== input.url) return;
emit.next(message);
});
return () => {
+215
View File
@@ -0,0 +1,215 @@
import { observable } from "@trpc/server/observable";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type {
FirewallCpuSummary,
FirewallInterfacesSummary,
FirewallMemorySummary,
FirewallVersionSummary,
} from "@homarr/integrations";
import {
firewallCpuRequestHandler,
firewallInterfacesRequestHandler,
firewallMemoryRequestHandler,
firewallVersionRequestHandler,
} from "@homarr/request-handler/firewall";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const firewallRouter = createTRPCRouter({
getFirewallCpuStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = firewallCpuRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
summary: data,
};
}),
);
return results;
}),
subscribeFirewallCpuStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
summary: FirewallCpuSummary;
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = firewallCpuRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((summary) => {
emit.next({
integration,
summary,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
getFirewallInterfacesStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = firewallInterfacesRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
summary: data,
};
}),
);
return results;
}),
subscribeFirewallInterfacesStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
summary: FirewallInterfacesSummary[];
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = firewallInterfacesRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((summary) => {
emit.next({
integration,
summary,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
getFirewallVersionStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = firewallVersionRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
summary: data,
};
}),
);
return results;
}),
subscribeFirewallVersionStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
summary: FirewallVersionSummary;
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = firewallVersionRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((summary) => {
emit.next({
integration,
summary,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
getFirewallMemoryStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.query(async ({ ctx }) => {
const results = await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = firewallMemoryRequestHandler.handler(integration, {});
const { data, timestamp } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return {
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
updatedAt: timestamp,
},
summary: data,
};
}),
);
return results;
}),
subscribeFirewallMemoryStatus: publicProcedure
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("firewall")))
.subscription(({ ctx }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"firewall"> }>;
summary: FirewallMemorySummary;
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const innerHandler = firewallMemoryRequestHandler.handler(integrationWithSecrets, {});
const unsubscribe = innerHandler.subscribe((summary) => {
emit.next({
integration,
summary,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
});
+2
View File
@@ -3,6 +3,7 @@ import { appRouter } from "./app";
import { calendarRouter } from "./calendar";
import { dnsHoleRouter } from "./dns-hole";
import { downloadsRouter } from "./downloads";
import { firewallRouter } from "./firewall";
import { healthMonitoringRouter } from "./health-monitoring";
import { indexerManagerRouter } from "./indexer-manager";
import { mediaReleaseRouter } from "./media-release";
@@ -40,5 +41,6 @@ export const widgetRouter = createTRPCRouter({
options: optionsRouter,
releases: releasesRouter,
networkController: networkControllerRouter,
firewall: firewallRouter,
notifications: notificationsRouter,
});
+7 -7
View File
@@ -34,21 +34,21 @@
"@homarr/validation": "workspace:^0.1.0",
"bcrypt": "^6.0.0",
"cookies": "^0.9.1",
"ldapts": "8.0.8",
"next": "15.4.4",
"ldapts": "8.0.9",
"next": "15.4.6",
"next-auth": "5.0.0-beta.29",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.1",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"prettier": "^3.6.2",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
+4 -4
View File
@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/api": "workspace:^0.1.0",
"react": "19.1.0",
"react-dom": "19.1.0"
"react": "19.1.1",
"react-dom": "19.1.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -24,13 +24,13 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"undici": "7.12.0"
"undici": "7.13.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ import { Agent as HttpsAgent } from "node:https";
import path from "node:path";
import { checkServerIdentity, rootCertificates } from "node:tls";
import axios from "axios";
import type { RequestInfo, RequestInit, Response } from "undici";
import { fetch } from "undici";
import { env } from "@homarr/common/env";
@@ -131,8 +132,8 @@ export const createAxiosCertificateInstanceAsync = async (
});
};
export const fetchWithTrustedCertificatesAsync: typeof fetch = async (url, options) => {
const agent = await createCertificateAgentAsync();
export const fetchWithTrustedCertificatesAsync = async (url: RequestInfo, options?: RequestInit): Promise<Response> => {
const agent = await createCertificateAgentAsync(undefined);
return fetch(url, {
...options,
dispatcher: agent,
+2 -2
View File
@@ -35,7 +35,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.8",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+6 -6
View File
@@ -31,10 +31,10 @@
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"dayjs": "^1.11.13",
"next": "15.4.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"undici": "7.12.0",
"next": "15.4.6",
"react": "19.1.1",
"react-dom": "19.1.1",
"undici": "7.13.0",
"zod": "^3.25.76",
"zod-validation-error": "^3.5.3"
},
@@ -42,7 +42,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -25,14 +25,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.6.1",
"ioredis": "5.7.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+8 -8
View File
@@ -29,12 +29,12 @@
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.83.0",
"@trpc/client": "^11.4.3",
"@trpc/server": "^11.4.3",
"@trpc/tanstack-react-query": "^11.4.3",
"@tanstack/react-query": "^5.84.2",
"@trpc/client": "^11.4.4",
"@trpc/server": "^11.4.4",
"@trpc/tanstack-react-query": "^11.4.4",
"node-cron": "^4.2.1",
"react": "19.1.0",
"react": "19.1.1",
"zod": "^3.25.76"
},
"devDependencies": {
@@ -42,8 +42,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/react": "19.1.8",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"@types/react": "19.1.9",
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
@@ -1,6 +1,7 @@
import { checkCron } from "./validation";
export const EVERY_5_SECONDS = checkCron("*/5 * * * * *") satisfies string;
export const EVERY_30_SECONDS = checkCron("*/30 * * * * *") satisfies string;
export const EVERY_MINUTE = checkCron("* * * * *") satisfies string;
export const EVERY_5_MINUTES = checkCron("*/5 * * * *") satisfies string;
export const EVERY_10_MINUTES = checkCron("*/10 * * * *") satisfies string;
+3 -2
View File
@@ -32,6 +32,7 @@
"@homarr/icons": "workspace:^0.1.0",
"@homarr/integrations": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/ping": "workspace:^0.1.0",
"@homarr/redis": "workspace:^0.1.0",
"@homarr/request-handler": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
@@ -43,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+10
View File
@@ -3,6 +3,12 @@ import { dockerContainersJob } from "./jobs/docker";
import { iconsUpdaterJob } from "./jobs/icons-updater";
import { dnsHoleJob } from "./jobs/integrations/dns-hole";
import { downloadsJob } from "./jobs/integrations/downloads";
import {
firewallCpuJob,
firewallInterfacesJob,
firewallMemoryJob,
firewallVersionJob,
} from "./jobs/integrations/firewall";
import { healthMonitoringJob } from "./jobs/integrations/health-monitoring";
import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant";
import { indexerManagerJob } from "./jobs/integrations/indexer-manager";
@@ -39,6 +45,10 @@ export const jobGroup = createCronJobGroup({
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
firewallCpu: firewallCpuJob,
firewallMemory: firewallMemoryJob,
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
});
@@ -0,0 +1,46 @@
import { EVERY_5_SECONDS, EVERY_30_SECONDS, EVERY_HOUR, EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import {
firewallCpuRequestHandler,
firewallInterfacesRequestHandler,
firewallMemoryRequestHandler,
firewallVersionRequestHandler,
} from "@homarr/request-handler/firewall";
import { createRequestIntegrationJobHandler } from "@homarr/request-handler/lib/cached-request-integration-job-handler";
import { createCronJob } from "../../lib";
export const firewallCpuJob = createCronJob("firewallCpu", EVERY_5_SECONDS).withCallback(
createRequestIntegrationJobHandler(firewallCpuRequestHandler.handler, {
widgetKinds: ["firewall"],
getInput: {
firewall: () => ({}),
},
}),
);
export const firewallMemoryJob = createCronJob("firewallMemory", EVERY_MINUTE).withCallback(
createRequestIntegrationJobHandler(firewallMemoryRequestHandler.handler, {
widgetKinds: ["firewall"],
getInput: {
firewall: () => ({}),
},
}),
);
export const firewallInterfacesJob = createCronJob("firewallInterfaces", EVERY_30_SECONDS).withCallback(
createRequestIntegrationJobHandler(firewallInterfacesRequestHandler.handler, {
widgetKinds: ["firewall"],
getInput: {
firewall: () => ({}),
},
}),
);
export const firewallVersionJob = createCronJob("firewallVersion", EVERY_HOUR).withCallback(
createRequestIntegrationJobHandler(firewallVersionRequestHandler.handler, {
widgetKinds: ["firewall"],
getInput: {
firewall: () => ({}),
},
}),
);
@@ -15,9 +15,10 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
},
{
widgetKinds: ["healthMonitoring"],
widgetKinds: ["healthMonitoring", "systemResources"],
getInput: {
healthMonitoring: () => ({}),
systemResources: () => ({}),
},
},
),
+14 -4
View File
@@ -2,8 +2,8 @@ import { EVERY_MINUTE } from "@homarr/cron-jobs-core/expressions";
import { db } from "@homarr/db";
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
import { logger } from "@homarr/log";
import { pingUrlChannel } from "@homarr/redis";
import { pingRequestHandler } from "@homarr/request-handler/ping";
import { sendPingRequestAsync } from "@homarr/ping";
import { pingChannel, pingUrlChannel } from "@homarr/redis";
import { createCronJob } from "../lib";
@@ -28,6 +28,16 @@ export const pingJob = createCronJob("ping", EVERY_MINUTE, {
});
const pingAsync = async (url: string) => {
const handler = pingRequestHandler.handler({ url });
await handler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
const pingResult = await sendPingRequestAsync(url);
if ("statusCode" in pingResult) {
logger.debug(`executed ping for url ${url} with status code ${pingResult.statusCode}`);
} else {
logger.error(`Executing ping for url ${url} failed with error: ${pingResult.error}`);
}
await pingChannel.publishAsync({
url,
...pingResult,
});
};
@@ -0,0 +1,52 @@
import type { Database } from "../..";
import { and, eq } from "../..";
import { integrationSecrets } from "../../schema";
/**
* Previously the credentials for OPNsense were stored as username and password.
* However it should have been the api key and secret.
* For more information see:
* https://docs.opnsense.org/development/how-tos/api.html#creating-keys
*/
export async function migrateOpnsenseCredentialsAsync(db: Database) {
const existingIntegrations = await db.query.integrations.findMany({
where: (table, { eq }) => eq(table.kind, "opnsense"),
with: {
secrets: true,
},
});
await Promise.all(
existingIntegrations.map(async (integration) => {
const username = integration.secrets.find((secret) => secret.kind === "username");
if (!username) return;
await db
.update(integrationSecrets)
.set({
kind: "opnsenseApiKey",
})
.where(
and(eq(integrationSecrets.integrationId, username.integrationId), eq(integrationSecrets.kind, "username")),
);
}),
);
await Promise.all(
existingIntegrations.map(async (integration) => {
const password = integration.secrets.find((secret) => secret.kind === "password");
if (!password) return;
await db
.update(integrationSecrets)
.set({
kind: "opnsenseApiSecret",
})
.where(
and(eq(integrationSecrets.integrationId, password.integrationId), eq(integrationSecrets.kind, "password")),
);
}),
);
if (existingIntegrations.length > 0) {
console.log(`Migrated OPNsense credentials count="${existingIntegrations.length}"`);
}
}
+2
View File
@@ -1,6 +1,8 @@
import type { Database } from "../..";
import { migrateReleaseWidgetProviderToOptionsAsync } from "./0000_release_widget_provider_to_options";
import { migrateOpnsenseCredentialsAsync } from "./0001_opnsense_credentials";
export const applyCustomMigrationsAsync = async (db: Database) => {
await migrateReleaseWidgetProviderToOptionsAsync(db);
await migrateOpnsenseCredentialsAsync(db);
};
+7 -7
View File
@@ -44,15 +44,15 @@
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.2.1",
"@mantine/core": "^8.2.4",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.4.0",
"@testcontainers/mysql": "^11.5.1",
"better-sqlite3": "^12.2.0",
"dotenv": "^17.2.1",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.3",
"drizzle-orm": "^0.44.4",
"drizzle-zod": "^0.7.1",
"mysql2": "3.14.2",
"mysql2": "3.14.3",
"superjson": "2.2.2"
},
"devDependencies": {
@@ -60,11 +60,11 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/better-sqlite3": "7.6.13",
"dotenv-cli": "^8.0.0",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.8",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"prettier": "^3.6.2",
"tsx": "4.20.3",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -31,8 +31,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"eslint": "^9.32.0",
"tsx": "4.20.3",
"typescript": "^5.8.3"
"typescript": "^5.9.2"
}
}
@@ -9,8 +9,8 @@ export type HomarrDocumentationPath =
| "/blog/2023/12/22/updated-documentation"
| "/blog/2024/09/23/version-1.0"
| "/blog/2024/12/17/open-beta-1.0"
| "/blog/2024/12/31/migrate-secret-enryption-key"
| "/blog/2025/01/19/migration-guide-1.0"
| "/blog/2025/08/02/using-argus"
| "/blog/archive"
| "/blog/authors"
| "/blog/authors/ajnart"
@@ -39,22 +39,16 @@ export type HomarrDocumentationPath =
| "/search"
| "/docs/tags"
| "/docs/tags/active-directory"
| "/docs/tags/ad-guard"
| "/docs/tags/ad-guard-home"
| "/docs/tags/administration"
| "/docs/tags/advanced"
| "/docs/tags/analytics"
| "/docs/tags/api"
| "/docs/tags/apps"
| "/docs/tags/background"
| "/docs/tags/banner"
| "/docs/tags/blocking"
| "/docs/tags/boards"
| "/docs/tags/bookmark"
| "/docs/tags/bookmarks"
| "/docs/tags/caddy"
| "/docs/tags/certificates"
| "/docs/tags/checklist"
| "/docs/tags/code"
| "/docs/tags/community"
| "/docs/tags/configuration"
@@ -64,63 +58,37 @@ export type HomarrDocumentationPath =
| "/docs/tags/database"
| "/docs/tags/developer"
| "/docs/tags/development"
| "/docs/tags/dns"
| "/docs/tags/docker"
| "/docs/tags/donation"
| "/docs/tags/edit-mode"
| "/docs/tags/env"
| "/docs/tags/environment-variables"
| "/docs/tags/feeds"
| "/docs/tags/finance"
| "/docs/tags/getting-started"
| "/docs/tags/google"
| "/docs/tags/grafana"
| "/docs/tags/groups"
| "/docs/tags/hardware"
| "/docs/tags/health"
| "/docs/tags/help"
| "/docs/tags/icon-picker"
| "/docs/tags/icon-repositories"
| "/docs/tags/icons"
| "/docs/tags/iframe"
| "/docs/tags/images"
| "/docs/tags/installation"
| "/docs/tags/integrade"
| "/docs/tags/integration"
| "/docs/tags/integrations"
| "/docs/tags/interface"
| "/docs/tags/jellyserr"
| "/docs/tags/jobs"
| "/docs/tags/layout"
| "/docs/tags/ldap"
| "/docs/tags/links"
| "/docs/tags/lists"
| "/docs/tags/management"
| "/docs/tags/market"
| "/docs/tags/media"
| "/docs/tags/minecraft"
| "/docs/tags/monitoring"
| "/docs/tags/network"
| "/docs/tags/news"
| "/docs/tags/notebook"
| "/docs/tags/notes"
| "/docs/tags/oidc"
| "/docs/tags/open-collective"
| "/docs/tags/open-media-vault"
| "/docs/tags/overseerr"
| "/docs/tags/permissions"
| "/docs/tags/pgid"
| "/docs/tags/pi-hole"
| "/docs/tags/ping"
| "/docs/tags/programming"
| "/docs/tags/proxmox"
| "/docs/tags/proxy"
| "/docs/tags/puid"
| "/docs/tags/releases"
| "/docs/tags/repositories"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/rss"
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/security"
@@ -128,24 +96,15 @@ export type HomarrDocumentationPath =
| "/docs/tags/seo"
| "/docs/tags/server"
| "/docs/tags/settings"
| "/docs/tags/sinkhole"
| "/docs/tags/sso"
| "/docs/tags/stocks"
| "/docs/tags/system"
| "/docs/tags/table"
| "/docs/tags/tasks"
| "/docs/tags/technical-documentation"
| "/docs/tags/text"
| "/docs/tags/torrent"
| "/docs/tags/traefik"
| "/docs/tags/translations"
| "/docs/tags/unifi-controller"
| "/docs/tags/unraid"
| "/docs/tags/uploads"
| "/docs/tags/usenet"
| "/docs/tags/users"
| "/docs/tags/variables"
| "/docs/tags/widgets"
| "/docs/advanced/command-line"
| "/docs/advanced/command-line/fix-usernames"
| "/docs/advanced/command-line/password-recovery"
@@ -188,17 +147,43 @@ export type HomarrDocumentationPath =
| "/docs/getting-started/installation/source"
| "/docs/getting-started/installation/synology"
| "/docs/getting-started/installation/unraid"
| "/docs/integrations/cloud"
| "/docs/integrations/containers"
| "/docs/integrations/dns"
| "/docs/integrations/hardware"
| "/docs/integrations/adguard-home"
| "/docs/integrations/aria2"
| "/docs/integrations/codeberg"
| "/docs/integrations/dash-dot"
| "/docs/integrations/deluge"
| "/docs/integrations/docker-hub"
| "/docs/integrations/docker"
| "/docs/integrations/emby"
| "/docs/integrations/github-containerregistry"
| "/docs/integrations/github"
| "/docs/integrations/gitlab"
| "/docs/integrations/home-assistant"
| "/docs/integrations/jellyfin"
| "/docs/integrations/jellyseerr"
| "/docs/integrations/kubernetes"
| "/docs/integrations/media-requester"
| "/docs/integrations/media-server"
| "/docs/integrations/network"
| "/docs/integrations/servarr"
| "/docs/integrations/torrent"
| "/docs/integrations/usenet"
| "/docs/integrations/lidarr"
| "/docs/integrations/linux-server-io"
| "/docs/integrations/nextcloud"
| "/docs/integrations/npm"
| "/docs/integrations/ntfy"
| "/docs/integrations/nzbget"
| "/docs/integrations/open-media-vault"
| "/docs/integrations/opnsense"
| "/docs/integrations/overseerr"
| "/docs/integrations/pi-hole"
| "/docs/integrations/plex"
| "/docs/integrations/prowlarr"
| "/docs/integrations/proxmox"
| "/docs/integrations/q-bittorent"
| "/docs/integrations/quay"
| "/docs/integrations/radarr"
| "/docs/integrations/readarr"
| "/docs/integrations/sabnzbd"
| "/docs/integrations/sonarr"
| "/docs/integrations/tdarr"
| "/docs/integrations/transmission"
| "/docs/integrations/unifi-controller"
| "/docs/management/api"
| "/docs/management/apps"
| "/docs/management/boards"
@@ -209,23 +194,33 @@ export type HomarrDocumentationPath =
| "/docs/management/settings"
| "/docs/management/tasks"
| "/docs/management/users"
| "/docs/widgets/app"
| "/docs/widgets/bookmarks"
| "/docs/widgets/calendar"
| "/docs/widgets/clock"
| "/docs/widgets/dns-hole"
| "/docs/widgets/dns-hole-controls"
| "/docs/widgets/dns-hole-summary"
| "/docs/widgets/docker-containers"
| "/docs/widgets/downloads"
| "/docs/widgets/firewall"
| "/docs/widgets/health-monitoring"
| "/docs/widgets/home-assistant"
| "/docs/widgets/iframe"
| "/docs/widgets/indexer-manager"
| "/docs/widgets/media-requests"
| "/docs/widgets/media-releases"
| "/docs/widgets/media-request-list"
| "/docs/widgets/media-request-stats"
| "/docs/widgets/media-server"
| "/docs/widgets/media-transcoding"
| "/docs/widgets/minecraft-server-status"
| "/docs/widgets/network-controller"
| "/docs/widgets/network-controller-status"
| "/docs/widgets/network-controller-summary"
| "/docs/widgets/notebook"
| "/docs/widgets/notifications"
| "/docs/widgets/releases"
| "/docs/widgets/rss"
| "/docs/widgets/stocks"
| "/docs/widgets/rss-feed"
| "/docs/widgets/smart-home-entity-state"
| "/docs/widgets/smart-home-execute-automation"
| "/docs/widgets/stock-price"
| "/docs/widgets/video"
| "/docs/widgets/weather"
| ""
+69
View File
@@ -1,6 +1,8 @@
import { objectKeys } from "@homarr/common";
import type { AtLeastOneOf } from "@homarr/common/types";
import { createDocumentationLink } from "./docs";
export const integrationSecretKindObject = {
apiKey: { isPublic: false },
username: { isPublic: true },
@@ -9,6 +11,8 @@ export const integrationSecretKindObject = {
realm: { isPublic: true },
personalAccessToken: { isPublic: false },
topic: { isPublic: true },
opnsenseApiKey: { isPublic: false },
opnsenseApiSecret: { isPublic: false },
} satisfies Record<string, { isPublic: boolean }>;
export const integrationSecretKinds = objectKeys(integrationSecretKindObject);
@@ -18,6 +22,7 @@ interface integrationDefinition {
iconUrl: string;
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
category: AtLeastOneOf<IntegrationCategory>;
documentationUrl: string | null;
defaultUrl?: string; // optional default URL for the integration
}
@@ -27,150 +32,182 @@ export const integrationDefs = {
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/sabnzbd"),
},
nzbGet: {
name: "NZBGet",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
category: ["downloadClient", "usenet"],
documentationUrl: createDocumentationLink("/docs/integrations/nzbget"),
},
deluge: {
name: "Deluge",
secretKinds: [["password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/deluge"),
},
transmission: {
name: "Transmission",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/transmission"),
},
qBittorrent: {
name: "qBittorrent",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
category: ["downloadClient", "torrent"],
documentationUrl: createDocumentationLink("/docs/integrations/q-bittorent"),
},
aria2: {
name: "Aria2",
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons@latest/src/system_downloads_3.svg",
category: ["downloadClient", "torrent", "miscellaneous"],
documentationUrl: createDocumentationLink("/docs/integrations/aria2"),
},
sonarr: {
name: "Sonarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/sonarr"),
},
radarr: {
name: "Radarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/radarr"),
},
lidarr: {
name: "Lidarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/lidarr"),
},
readarr: {
name: "Readarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/readarr"),
},
prowlarr: {
name: "Prowlarr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
category: ["indexerManager"],
documentationUrl: createDocumentationLink("/docs/integrations/prowlarr"),
},
jellyfin: {
name: "Jellyfin",
secretKinds: [["username", "password"], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyfin"),
},
emby: {
name: "Emby",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/emby"),
},
plex: {
name: "Plex",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
category: ["mediaService", "mediaRelease"],
documentationUrl: createDocumentationLink("/docs/integrations/plex"),
},
jellyseerr: {
name: "Jellyseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/jellyseerr"),
},
overseerr: {
name: "Overseerr",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
category: ["mediaSearch", "mediaRequest", "search"],
documentationUrl: createDocumentationLink("/docs/integrations/overseerr"),
},
piHole: {
name: "Pi-hole",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/pi-hole"),
},
adGuardHome: {
name: "AdGuard Home",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
category: ["dnsHole"],
documentationUrl: createDocumentationLink("/docs/integrations/adguard-home"),
},
homeAssistant: {
name: "Home Assistant",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer"],
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
},
openmediavault: {
name: "OpenMediaVault",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/openmediavault.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/open-media-vault"),
},
dashDot: {
name: "Dash.",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/dash-dot"),
},
tdarr: {
name: "Tdarr",
secretKinds: [[], ["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
category: ["mediaTranscoding"],
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
},
proxmox: {
name: "Proxmox",
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/proxmox.svg",
category: ["healthMonitoring"],
documentationUrl: createDocumentationLink("/docs/integrations/proxmox"),
},
nextcloud: {
name: "Nextcloud",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg",
category: ["calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/nextcloud"),
},
unifiController: {
name: "Unifi Controller",
secretKinds: [["username", "password"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
category: ["networkController"],
documentationUrl: createDocumentationLink("/docs/integrations/unifi-controller"),
},
opnsense: {
name: "OPNsense",
secretKinds: [["opnsenseApiKey", "opnsenseApiSecret"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/opnsense.svg",
category: ["firewall"],
documentationUrl: createDocumentationLink("/docs/integrations/opnsense"),
},
github: {
name: "Github",
@@ -178,6 +215,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
documentationUrl: createDocumentationLink("/docs/integrations/github"),
},
dockerHub: {
name: "Docker Hub",
@@ -185,6 +223,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg",
category: ["releasesProvider"],
defaultUrl: "https://hub.docker.com",
documentationUrl: createDocumentationLink("/docs/integrations/docker-hub"),
},
gitlab: {
name: "Gitlab",
@@ -192,6 +231,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gitlab.svg",
category: ["releasesProvider"],
defaultUrl: "https://gitlab.com",
documentationUrl: createDocumentationLink("/docs/integrations/gitlab"),
},
npm: {
name: "NPM",
@@ -199,6 +239,7 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/npm.svg",
category: ["releasesProvider"],
defaultUrl: "https://registry.npmjs.org",
documentationUrl: createDocumentationLink("/docs/integrations/npm"),
},
codeberg: {
name: "Codeberg",
@@ -206,12 +247,38 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/codeberg.svg",
category: ["releasesProvider"],
defaultUrl: "https://codeberg.org",
documentationUrl: createDocumentationLink("/docs/integrations/codeberg"),
},
linuxServerIO: {
name: "LinuxServer.io",
secretKinds: [[]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.linuxserver.io",
documentationUrl: createDocumentationLink("/docs/integrations/linux-server-io"),
},
gitHubContainerRegistry: {
name: "GitHub Container Registry",
secretKinds: [[], ["personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/github.svg",
category: ["releasesProvider"],
defaultUrl: "https://api.github.com",
documentationUrl: createDocumentationLink("/docs/integrations/github"),
},
quay: {
name: "Quay",
secretKinds: [[], ["personalAccessToken"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png",
category: ["releasesProvider"],
defaultUrl: "https://quay.io",
documentationUrl: createDocumentationLink("/docs/integrations/quay"),
},
ntfy: {
name: "ntfy",
secretKinds: [["topic"], ["topic", "apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
category: ["notifications"],
documentationUrl: createDocumentationLink("/docs/integrations/ntfy"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
mock: {
@@ -232,6 +299,7 @@ export const integrationDefs = {
"notifications",
"smartHomeServer",
],
documentationUrl: null,
},
} as const satisfies Record<string, integrationDefinition>;
@@ -297,6 +365,7 @@ export const integrationCategories = [
"networkController",
"releasesProvider",
"notifications",
"firewall",
] as const;
export type IntegrationCategory = (typeof integrationCategories)[number];
+2
View File
@@ -26,6 +26,8 @@ export const widgetKinds = [
"releases",
"mediaReleases",
"dockerContainers",
"firewall",
"notifications",
"systemResources",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
+2 -2
View File
@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.42",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -26,14 +26,14 @@
"@homarr/common": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.2.1",
"@mantine/form": "^8.2.4",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+4 -4
View File
@@ -29,15 +29,15 @@
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.2.1",
"react": "19.1.0",
"@mantine/core": "^8.2.4",
"react": "19.1.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -32,8 +32,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "5.0.2",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"@types/bcrypt": "6.0.0",
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+4 -4
View File
@@ -28,7 +28,7 @@
"@ctrl/deluge": "^7.1.1",
"@ctrl/qbittorrent": "^9.6.0",
"@ctrl/transmission": "^7.2.0",
"@gitbeaker/rest": "^43.3.0",
"@gitbeaker/rest": "^43.4.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
@@ -45,7 +45,7 @@
"octokit": "^5.0.3",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.5",
"undici": "7.12.0",
"undici": "7.13.0",
"xml2js": "^0.6.2",
"zod": "^3.25.76"
},
@@ -55,7 +55,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
@@ -14,11 +14,13 @@ import { QBitTorrentIntegration } from "../download-client/qbittorrent/qbittorre
import { SabnzbdIntegration } from "../download-client/sabnzbd/sabnzbd-integration";
import { TransmissionIntegration } from "../download-client/transmission/transmission-integration";
import { EmbyIntegration } from "../emby/emby-integration";
import { GitHubContainerRegistryIntegration } from "../github-container-registry/github-container-registry-integration";
import { GithubIntegration } from "../github/github-integration";
import { GitlabIntegration } from "../gitlab/gitlab-integration";
import { HomeAssistantIntegration } from "../homeassistant/homeassistant-integration";
import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
import { LinuxServerIOIntegration } from "../linuxserverio/linuxserverio-integration";
import { LidarrIntegration } from "../media-organizer/lidarr/lidarr-integration";
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
@@ -29,11 +31,13 @@ import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
import { NPMIntegration } from "../npm/npm-integration";
import { NTFYIntegration } from "../ntfy/ntfy-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
import { PlexIntegration } from "../plex/plex-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
import { QuayIntegration } from "../quay/quay-integration";
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
import type { Integration, IntegrationInput } from "./integration";
@@ -99,11 +103,15 @@ export const integrationCreators = {
emby: EmbyIntegration,
nextcloud: NextcloudIntegration,
unifiController: UnifiControllerIntegration,
opnsense: OPNsenseIntegration,
github: GithubIntegration,
dockerHub: DockerHubIntegration,
gitlab: GitlabIntegration,
npm: NPMIntegration,
codeberg: CodebergIntegration,
linuxServerIO: LinuxServerIOIntegration,
gitHubContainerRegistry: GitHubContainerRegistryIntegration,
quay: QuayIntegration,
ntfy: NTFYIntegration,
mock: MockIntegration,
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
@@ -3,8 +3,10 @@ import { logger } from "@homarr/log";
import type { Integration } from "../integration";
import type { IIntegrationErrorHandler } from "./handler";
import { integrationFetchHttpErrorHandler } from "./http";
import { IntegrationError } from "./integration-error";
import { IntegrationUnknownError } from "./integration-unknown-error";
import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./parse";
const localLogger = logger.child({
module: "HandleIntegrationErrors",
@@ -13,7 +15,14 @@ const localLogger = logger.child({
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-explicit-any
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;
const defaultErrorHandlers: IIntegrationErrorHandler[] = [
integrationZodParseErrorHandler,
integrationJsonParseErrorHandler,
integrationFetchHttpErrorHandler,
];
export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[]) => {
const combinedErrorHandlers = [...defaultErrorHandlers, ...errorHandlers];
return <T extends AbstractConstructor<Integration>>(IntegrationBaseClass: T): T => {
abstract class ErrorHandledIntegration extends IntegrationBaseClass {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,7 +51,7 @@ export const HandleIntegrationErrors = (errorHandlers: IIntegrationErrorHandler[
throw error;
}
for (const handler of errorHandlers) {
for (const handler of combinedErrorHandlers) {
const handledError = handler.handleError(error, this.publicIntegration);
if (!handledError) continue;
@@ -8,8 +8,6 @@ import { removeTrailingSlash } from "@homarr/common";
import type { IntegrationSecretKind } from "@homarr/definitions";
import { HandleIntegrationErrors } from "./errors/decorator";
import { integrationFetchHttpErrorHandler } from "./errors/http";
import { integrationJsonParseErrorHandler, integrationZodParseErrorHandler } from "./errors/parse";
import { TestConnectionError } from "./test-connection/test-connection-error";
import type { TestingResult } from "./test-connection/test-connection-service";
import { TestConnectionService } from "./test-connection/test-connection-service";
@@ -32,11 +30,7 @@ export interface IntegrationTestingInput {
};
}
@HandleIntegrationErrors([
integrationZodParseErrorHandler,
integrationJsonParseErrorHandler,
integrationFetchHttpErrorHandler,
])
@HandleIntegrationErrors([])
export abstract class Integration {
constructor(protected integration: IntegrationInput) {}
@@ -20,7 +20,7 @@ const localLogger = logger.child({ module: "CodebergIntegration" });
export class CodebergIntegration extends Integration implements ReleasesProviderIntegration {
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
if (!this.hasSecretValue("personalAccessToken")) return await callback({});
if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined);
return await callback({
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
@@ -61,7 +61,7 @@ export class CodebergIntegration extends Integration implements ReleasesProvider
const details = await this.getDetailsAsync(owner, name);
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return fetchWithTrustedCertificatesAsync(
return await fetchWithTrustedCertificatesAsync(
this.url(`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`),
{ headers },
);
@@ -7,7 +7,7 @@ import { z } from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { createChannelEventHistory } from "../../../redis/src/lib/channel";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
@@ -32,14 +32,16 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
const cpuLoad = await this.getCurrentCpuLoadAsync();
const memoryLoad = await this.getCurrentMemoryLoadAsync();
const storageLoad = await this.getCurrentStorageLoadAsync();
const networkLoad = await this.getCurrentNetworkLoadAsync();
const channel = this.getChannel();
const history = await channel.getSliceUntilTimeAsync(dayjs().subtract(15, "minutes").toDate());
return {
cpuUtilization: cpuLoad.sumLoad,
memUsed: `${memoryLoad.loadInBytes}`,
memAvailable: `${info.maxAvailableMemoryBytes - memoryLoad.loadInBytes}`,
memUsedInBytes: memoryLoad.loadInBytes,
memAvailableInBytes: info.maxAvailableMemoryBytes - memoryLoad.loadInBytes,
network: networkLoad,
fileSystem: info.storage
.filter((_, index) => storageLoad[index] !== -1) // filter out undermoutned drives, they display as -1 in the load API
.map((storage, index) => ({
@@ -113,8 +115,13 @@ export class DashDotIntegration extends Integration implements ISystemHealthMoni
};
}
private async getCurrentNetworkLoadAsync() {
const response = await fetchWithTrustedCertificatesAsync(this.url("/load/network"));
return await networkLoadApi.parseAsync(await response.json());
}
private getChannel() {
return createChannelEventHistory<z.infer<typeof cpuLoadPerCoreApiList>>(
return createChannelEventHistoryOld<z.infer<typeof cpuLoadPerCoreApiList>>(
`integration:${this.integration.id}:history:cpu`,
100,
);
@@ -130,6 +137,11 @@ const memoryLoadApi = z.object({
load: z.number().min(0),
});
const networkLoadApi = z.object({
up: z.number().min(0),
down: z.number().min(0),
});
const internalServerInfoApi = z.object({
os: z.object({
distro: z.string(),
@@ -30,7 +30,8 @@ export class DockerHubIntegration extends Integration implements ReleasesProvide
}
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken")) return await callback({});
if (!this.hasSecretValue("username") || !this.hasSecretValue("personalAccessToken"))
return await callback(undefined);
const storedSession = await this.sessionStore.getAsync();
@@ -0,0 +1,145 @@
import { Octokit, RequestError } from "octokit";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
DetailsProviderResponse,
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
} from "../interfaces/releases-providers/releases-providers-types";
const localLogger = logger.child({ module: "GitHubContainerRegistryIntegration" });
export class GitHubContainerRegistryIntegration extends Integration implements ReleasesProviderIntegration {
private static readonly userAgent = "Homarr-Lab/Homarr:GitHubContainerRegistryIntegration";
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const headers: RequestInit["headers"] = {
"User-Agent": GitHubContainerRegistryIntegration.userAgent,
};
if (this.hasSecretValue("personalAccessToken"))
headers.Authorization = `Bearer ${this.getSecretValue("personalAccessToken")}`;
const response = await input.fetchAsync(this.url("/octocat"), {
headers,
});
if (!response.ok) {
return TestConnectionError.StatusResult(response);
}
return {
success: true,
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with GitHub Container Registry integration`,
{
identifier: repository.identifier,
},
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
}
const api = this.getApi();
const details = await this.getDetailsAsync(api, owner, name);
try {
const releasesResponse = await api.rest.packages.getAllPackageVersionsForPackageOwnedByUser({
username: owner,
package_type: "container",
package_name: name,
per_page: 100,
});
const releasesProviderResponse = releasesResponse.data.reduce<ReleaseProviderResponse[]>((acc, release) => {
if (!release.metadata?.container?.tags || !(release.metadata.container.tags.length > 0)) return acc;
release.metadata.container.tags.forEach((tag) => {
acc.push({
latestRelease: tag,
latestReleaseAt: new Date(release.updated_at),
releaseUrl: release.html_url,
releaseDescription: release.description ?? undefined,
});
});
return acc;
}, []);
return getLatestRelease(releasesProviderResponse, repository, details);
} catch (error) {
const errorMessage = error instanceof RequestError ? error.message : String(error);
localLogger.warn(`Failed to get releases for ${owner}\\${name} with GitHub Container Registry integration`, {
owner,
name,
error: errorMessage,
});
return {
id: repository.id,
error: { message: errorMessage },
};
}
}
protected async getDetailsAsync(
api: Octokit,
owner: string,
name: string,
): Promise<DetailsProviderResponse | undefined> {
try {
const response = await api.rest.packages.getPackageForUser({
username: owner,
package_type: "container",
package_name: name,
});
return {
projectUrl: response.data.repository?.html_url ?? response.data.html_url,
projectDescription: response.data.repository?.description ?? undefined,
isFork: response.data.repository?.fork,
isArchived: response.data.repository?.archived,
createdAt: new Date(response.data.created_at),
starsCount: response.data.repository?.stargazers_count,
openIssues: response.data.repository?.open_issues_count,
forksCount: response.data.repository?.forks_count,
};
} catch (error) {
localLogger.warn(`Failed to get details for ${owner}\\${name} with GitHub Container Registry integration`, {
owner,
name,
error: error instanceof RequestError ? error.message : String(error),
});
return undefined;
}
}
private getApi() {
return new Octokit({
baseUrl: this.url("/").origin,
request: {
fetch: fetchWithTrustedCertificatesAsync,
},
userAgent: GitHubContainerRegistryIntegration.userAgent,
throttle: { enabled: false }, // Disable throttling for this integration, Octokit will retry by default after a set time, thus delaying the repsonse to the user in case of errors. Errors will be shown to the user, no need to retry the request.
...(this.hasSecretValue("personalAccessToken") ? { auth: this.getSecretValue("personalAccessToken") } : {}),
});
}
}
@@ -58,7 +58,6 @@ export class GithubIntegration extends Integration implements ReleasesProviderIn
}
const api = this.getApi();
const details = await this.getDetailsAsync(api, owner, name);
try {
+8 -1
View File
@@ -21,13 +21,20 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
export { PlexIntegration } from "./plex/plex-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { OPNsenseIntegration } from "./opnsense/opnsense-integration";
// Types
export type { IntegrationInput } from "./base/integration";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type {
FirewallInterface,
FirewallCpuSummary,
FirewallInterfacesSummary,
FirewallVersionSummary,
FirewallMemorySummary,
} from "./interfaces/firewall-summary/firewall-summary-types";
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
@@ -0,0 +1,13 @@
import type {
FirewallCpuSummary,
FirewallInterfacesSummary,
FirewallMemorySummary,
FirewallVersionSummary,
} from "./firewall-summary-types";
export interface FirewallSummaryIntegration {
getFirewallCpuAsync(): Promise<FirewallCpuSummary>;
getFirewallMemoryAsync(): Promise<FirewallMemorySummary>;
getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]>;
getFirewallVersionAsync(): Promise<FirewallVersionSummary>;
}
@@ -0,0 +1,24 @@
export interface FirewallInterfacesSummary {
data: FirewallInterface[];
timestamp: Date;
}
export interface FirewallInterface {
name: string;
receive: number;
transmit: number;
}
export interface FirewallVersionSummary {
version: string;
}
export interface FirewallCpuSummary {
total: number;
}
export interface FirewallMemorySummary {
used: number;
total: number;
percent: number;
}
@@ -4,9 +4,13 @@ export interface SystemHealthMonitoring {
version: string;
cpuModelName: string;
cpuUtilization: number;
memUsed: string;
memAvailable: string;
memUsedInBytes: number;
memAvailableInBytes: number;
uptime: number;
network: {
up: number;
down: number;
} | null;
loadAverage: {
"1min": number;
"5min": number;
@@ -0,0 +1,88 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import type { ReleasesRepository, ReleasesResponse } from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./linuxserverio-schemas";
const localLogger = logger.child({ module: "LinuxServerIOsIntegration" });
export class LinuxServerIOIntegration extends Integration implements ReleasesProviderIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/health"));
if (!response.ok) {
return TestConnectionError.StatusResult(response);
}
return {
success: true,
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
{
identifier: repository.identifier,
},
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
}
const releasesResponse = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/images"));
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
error: {
message: error.message,
},
};
} else {
const release = data.data.repositories.linuxserver.find((repo) => repo.name === name);
if (!release) {
localLogger.warn(`Repository ${name} not found on provider, with LinuxServerIO integration`, {
owner,
name,
});
return {
id: repository.id,
error: { code: "noReleasesFound" },
};
}
return {
id: repository.id,
latestRelease: release.version,
latestReleaseAt: release.version_timestamp,
releaseDescription: release.changelog?.shift()?.desc,
projectUrl: release.github_url,
projectDescription: release.description,
isArchived: release.deprecated,
createdAt: release.initial_date ? new Date(release.initial_date) : undefined,
starsCount: release.stars,
};
}
}
}
@@ -0,0 +1,31 @@
import { z } from "zod";
export const releasesResponseSchema = z.object({
data: z.object({
repositories: z.object({
linuxserver: z.array(
z.object({
name: z.string(),
initial_date: z
.string()
.transform((value) => new Date(value))
.optional(),
github_url: z.string(),
description: z.string(),
version: z.string(),
version_timestamp: z.string().transform((value) => new Date(value)),
stars: z.number(),
deprecated: z.boolean(),
changelog: z
.array(
z.object({
date: z.string().transform((value) => new Date(value)),
desc: z.string(),
}),
)
.optional(),
}),
),
}),
}),
});
@@ -7,9 +7,13 @@ export class SystemHealthMonitoringMockService implements ISystemHealthMonitorin
version: "1.0.0",
cpuModelName: "Mock CPU",
cpuUtilization: Math.random(),
memUsed: (4 * 1024 * 1024 * 1024).toString(), // 4 GB in bytes
memAvailable: (8 * 1024 * 1024 * 1024).toString(), // 8 GB in bytes
memUsedInBytes: 4 * 1024 * 1024 * 1024, // 4 GB in bytes
memAvailableInBytes: 8 * 1024 * 1024 * 1024, // 8 GB in bytes
availablePkgUpdates: 0,
network: {
up: 1024 * 16,
down: 1024 * 16 * 6,
},
rebootRequired: false,
cpuTemp: Math.floor(Math.random() * 100), // Random temperature between 0 and 99
uptime: Math.floor(Math.random() * 1000000), // Random uptime in seconds
@@ -69,9 +69,11 @@ export class OpenMediaVaultIntegration extends Integration implements ISystemHea
version: systemResult.data.response.version,
cpuModelName: systemResult.data.response.cpuModelName ?? "Unknown CPU",
cpuUtilization: systemResult.data.response.cpuUtilization,
memUsed: systemResult.data.response.memUsed,
memAvailable: systemResult.data.response.memAvailable,
memUsedInBytes: Number(systemResult.data.response.memUsed),
memAvailableInBytes: Number(systemResult.data.response.memAvailable),
uptime: systemResult.data.response.uptime,
/* real-time traffic monitoring is not available over the RPC API from OMV */
network: null,
loadAverage: {
"1min": systemResult.data.response.loadAverage["1min"],
"5min": systemResult.data.response.loadAverage["5min"],
@@ -0,0 +1,187 @@
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ParseError, ResponseError } from "@homarr/common/server";
import { createChannelEventHistoryOld } from "../../../redis/src/lib/channel";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { FirewallSummaryIntegration } from "../interfaces/firewall-summary/firewall-summary-integration";
import type {
FirewallCpuSummary,
FirewallInterface,
FirewallInterfacesSummary,
FirewallMemorySummary,
FirewallVersionSummary,
} from "../interfaces/firewall-summary/firewall-summary-types";
import {
opnsenseCPUSchema,
opnsenseInterfacesSchema,
opnsenseMemorySchema,
opnsenseSystemSummarySchema,
} from "./opnsense-types";
export class OPNsenseIntegration extends Integration implements FirewallSummaryIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/diagnostics/system/system_information"), {
headers: {
Authorization: this.getAuthHeaders(),
},
});
if (!response.ok) return TestConnectionError.StatusResult(response);
const result = await response.json();
if (typeof result === "object" && result !== null) return { success: true };
return TestConnectionError.ParseResult(new ParseError("Expected object data"));
}
private getAuthHeaders() {
const key = super.getSecretValue("opnsenseApiKey");
const secret = super.getSecretValue("opnsenseApiSecret");
return `Basic ${btoa(`${key}:${secret}`)}`;
}
public async getFirewallVersionAsync(): Promise<FirewallVersionSummary> {
const responseVersion = await fetchWithTrustedCertificatesAsync(
this.url("/api/diagnostics/system/system_information"),
{
headers: {
Authorization: this.getAuthHeaders(),
},
},
);
if (!responseVersion.ok) {
throw new ResponseError(responseVersion);
}
const summary = opnsenseSystemSummarySchema.parse(await responseVersion.json());
return {
version: summary.versions.at(0) ?? "Unknown",
};
}
private getInterfacesChannel() {
return createChannelEventHistoryOld<FirewallInterface[]>(`integration:${this.integration.id}:interfaces`, 15);
}
public async getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]> {
const channel = this.getInterfacesChannel();
const responseInterfaces = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/traffic/interface"), {
headers: {
Authorization: this.getAuthHeaders(),
},
});
if (!responseInterfaces.ok) {
throw new ResponseError(responseInterfaces);
}
const interfaces = opnsenseInterfacesSchema.parse(await responseInterfaces.json());
const returnValue: FirewallInterface[] = [];
const interfaceKeys = Object.keys(interfaces.interfaces);
for (const key of interfaceKeys) {
const inter = interfaces.interfaces[key];
if (!inter) continue;
const bytesTransmitted = inter["bytes transmitted"];
const bytesReceived = inter["bytes received"];
const receiveValue = parseInt(bytesReceived, 10);
const transmitValue = parseInt(bytesTransmitted, 10);
returnValue.push({
name: inter.name,
receive: receiveValue,
transmit: transmitValue,
});
}
await channel.pushAsync(returnValue);
return await channel.getSliceAsync(0, 1);
}
public async getFirewallMemoryAsync(): Promise<FirewallMemorySummary> {
const responseMemory = await fetchWithTrustedCertificatesAsync(
this.url("/api/diagnostics/system/systemResources"),
{
headers: {
Authorization: this.getAuthHeaders(),
},
},
);
if (!responseMemory.ok) {
throw new ResponseError(responseMemory);
}
const memory = opnsenseMemorySchema.parse(await responseMemory.json());
// Using parseInt for memoryTotal is normal, the api sends the total memory as a string
const memoryTotal = parseInt(memory.memory.total);
const memoryUsed = memory.memory.used;
const memoryPercent = (100 * memoryUsed) / memoryTotal;
return {
total: memoryTotal,
used: memoryUsed,
percent: memoryPercent,
};
}
public async getFirewallCpuAsync(): Promise<FirewallCpuSummary> {
const responseCpu = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/cpu_usage/stream"), {
headers: {
Authorization: this.getAuthHeaders(),
},
});
if (!responseCpu.ok) {
throw new ResponseError(responseCpu);
}
if (!responseCpu.body) {
throw new Error("ReadableStream not supported in this environment.");
}
const reader = responseCpu.body.getReader();
const decoder = new TextDecoder();
let loopCounter = 0;
try {
while (loopCounter < 10) {
loopCounter++;
const result = await reader.read();
if (result.done) {
break;
}
if (!(result.value instanceof Uint8Array)) {
throw new Error("Received value is not an Uint8Array.");
}
const value: AllowSharedBufferSource = result.value;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (!line.startsWith("data:")) {
continue;
}
if (loopCounter < 2) {
continue;
}
const data = line.substring(5).trim();
const cpuValues = opnsenseCPUSchema.parse(JSON.parse(data));
return {
...cpuValues,
};
}
}
throw new Error("No valid CPU data found.");
} finally {
await reader.cancel();
}
}
}
@@ -0,0 +1,30 @@
import { z } from "zod";
// API documentation : https://docs.opnsense.org/development/api.html#core-api
export const opnsenseSystemSummarySchema = z.object({
name: z.string(),
versions: z.array(z.string()),
});
export const opnsenseMemorySchema = z.object({
memory: z.object({
total: z.string(),
used: z.number(),
}),
});
const interfaceSchema = z.object({
"bytes received": z.string(),
"bytes transmitted": z.string(),
name: z.string(),
});
export const opnsenseInterfacesSchema = z.object({
interfaces: z.record(interfaceSchema),
time: z.number(),
});
export const opnsenseCPUSchema = z.object({
total: z.number(),
});
@@ -0,0 +1,109 @@
import type { RequestInit, Response } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ReleasesProviderIntegration } from "../interfaces/releases-providers/releases-providers-integration";
import { getLatestRelease } from "../interfaces/releases-providers/releases-providers-integration";
import type {
ReleaseProviderResponse,
ReleasesRepository,
ReleasesResponse,
} from "../interfaces/releases-providers/releases-providers-types";
import { releasesResponseSchema } from "./quay-schemas";
const localLogger = logger.child({ module: "QuayIntegration" });
export class QuayIntegration extends Integration implements ReleasesProviderIntegration {
private async withHeadersAsync(callback: (headers: RequestInit["headers"]) => Promise<Response>): Promise<Response> {
if (!this.hasSecretValue("personalAccessToken")) return await callback(undefined);
return await callback({
Authorization: `token ${this.getSecretValue("personalAccessToken")}`,
});
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await this.withHeadersAsync(async (headers) => {
return await input.fetchAsync(this.url("/api/v1/discovery"), {
headers,
});
});
if (!response.ok) {
return TestConnectionError.StatusResult(response);
}
return {
success: true,
};
}
public async getLatestMatchingReleaseAsync(repository: ReleasesRepository): Promise<ReleasesResponse> {
const [owner, name] = repository.identifier.split("/");
if (!owner || !name) {
localLogger.warn(
`Invalid identifier format. Expected 'owner/name', for ${repository.identifier} with LinuxServerIO integration`,
{
identifier: repository.identifier,
},
);
return {
id: repository.id,
error: { code: "invalidIdentifier" },
};
}
const releasesResponse = await this.withHeadersAsync(async (headers) => {
return await fetchWithTrustedCertificatesAsync(
this.url(
`/api/v1/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}?includeTags=true&includeStats=true`,
),
{
headers,
},
);
});
if (!releasesResponse.ok) {
return {
id: repository.id,
error: { message: releasesResponse.statusText },
};
}
const releasesResponseJson: unknown = await releasesResponse.json();
const { data, success, error } = releasesResponseSchema.safeParse(releasesResponseJson);
if (!success) {
return {
id: repository.id,
error: {
message: error.message,
},
};
} else {
const details = {
projectDescription: data.description,
};
const releasesProviderResponse = Object.entries(data.tags).reduce<ReleaseProviderResponse[]>((acc, [_, tag]) => {
if (!tag.name || !tag.last_modified) return acc;
acc.push({
latestRelease: tag.name,
latestReleaseAt: new Date(tag.last_modified),
releaseUrl: `https://quay.io/repository/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/tag/${encodeURIComponent(tag.name)}`,
});
return acc;
}, []);
return getLatestRelease(releasesProviderResponse, repository, details);
}
}
}
@@ -0,0 +1,11 @@
import { z } from "zod";
export const releasesResponseSchema = z.object({
description: z.string().optional(),
tags: z.record(
z.object({
name: z.string(),
last_modified: z.string(),
}),
),
});
+2
View File
@@ -1,6 +1,7 @@
export * from "./interfaces/calendar/calendar-types";
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
export * from "./interfaces/firewall-summary/firewall-summary-types";
export * from "./interfaces/health-monitoring/health-monitoring-types";
export * from "./interfaces/indexer-manager/indexer-manager-types";
export * from "./interfaces/media-requests/media-request-types";
@@ -8,4 +9,5 @@ export * from "./base/searchable-integration";
export * from "./homeassistant/homeassistant-types";
export * from "./proxmox/proxmox-types";
export * from "./unifi-controller/unifi-controller-types";
export * from "./opnsense/opnsense-types";
export * from "./interfaces/media-releases";
+2 -2
View File
@@ -33,7 +33,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+6 -6
View File
@@ -33,19 +33,19 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.2.1",
"@mantine/core": "^8.2.4",
"@tabler/icons-react": "^3.34.1",
"dayjs": "^1.11.13",
"next": "15.4.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.4.6",
"react": "19.1.1",
"react-dom": "19.1.1",
"zod": "^3.25.76"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+5 -5
View File
@@ -24,15 +24,15 @@
"dependencies": {
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@mantine/core": "^8.2.1",
"@mantine/hooks": "^8.2.1",
"react": "19.1.0"
"@mantine/core": "^8.2.4",
"@mantine/hooks": "^8.2.4",
"react": "19.1.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+3 -3
View File
@@ -24,14 +24,14 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.2.1",
"@mantine/notifications": "^8.2.4",
"@tabler/icons-react": "^3.34.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+7 -7
View File
@@ -37,12 +37,12 @@
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.2.1",
"@mantine/hooks": "^8.2.1",
"@mantine/core": "^8.2.4",
"@mantine/hooks": "^8.2.4",
"adm-zip": "0.5.16",
"next": "15.4.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.4.6",
"react": "19.1.1",
"react-dom": "19.1.1",
"superjson": "2.2.2",
"zod": "^3.25.76",
"zod-form-data": "^2.0.7"
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+2 -2
View File
@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+4
View File
@@ -0,0 +1,4 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig];
+1
View File
@@ -0,0 +1 @@
export * from "./src";
+36
View File
@@ -0,0 +1,36 @@
{
"name": "@homarr/ping",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
+37
View File
@@ -0,0 +1,37 @@
import { fetch } from "undici";
import { extractErrorMessage } from "@homarr/common";
import { LoggingAgent } from "@homarr/common/server";
import { logger } from "@homarr/log";
export const sendPingRequestAsync = async (url: string) => {
try {
const controller = new AbortController();
// 10 seconds timeout:
const timeoutId = setTimeout(() => controller.abort(), 10000);
const start = performance.now();
return await fetch(url, {
dispatcher: new LoggingAgent({
connect: {
rejectUnauthorized: false, // Ping should always work, even with untrusted certificates
},
}),
signal: controller.signal,
})
.finally(() => {
clearTimeout(timeoutId);
})
.then((response) => {
const end = performance.now();
const durationMs = end - start;
return { statusCode: response.status, durationMs };
});
} catch (error) {
logger.error(new Error(`Failed to send ping request to "${url}"`, { cause: error }));
return {
error: extractErrorMessage(error),
};
}
};
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"types": ["node"],
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+3 -3
View File
@@ -27,14 +27,14 @@
"@homarr/db": "workspace:^",
"@homarr/definitions": "workspace:^",
"@homarr/log": "workspace:^",
"ioredis": "5.6.1",
"ioredis": "5.7.0",
"superjson": "2.2.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.31.0",
"typescript": "^5.8.3"
"eslint": "^9.32.0",
"typescript": "^5.9.2"
}
}
Loaded 100 of 178 files, more files were not shown because too many files have changed in this diff. Show more