Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf2525de5 | ||
|
|
3343468c55 | ||
|
|
27bd93fa8a | ||
|
|
e636e02d6a | ||
|
|
7f474683a5 | ||
|
|
4a07b3f866 | ||
|
|
d23196b430 | ||
|
|
ef40e75818 | ||
|
|
60accd66f6 | ||
|
|
4b6c69d48a | ||
|
|
c73ce1f704 | ||
|
|
ffa629064c | ||
|
|
dde41f6baf | ||
|
|
d32703910e | ||
|
|
a4914d09fd | ||
|
|
ed950e9302 | ||
|
|
bb681f7b5e | ||
|
|
72e976ee5e | ||
|
|
e6be8e1db3 | ||
|
|
5d494111df | ||
|
|
3ee408bf53 | ||
|
|
1b5ccb5293 | ||
|
|
cc6d421e7d | ||
|
|
c8d59d88e5 | ||
|
|
3dbf085e59 | ||
|
|
ff03220960 | ||
|
|
001f7a2a16 | ||
|
|
2780b1e794 | ||
|
|
59c092f800 | ||
|
|
4bc053cc17 | ||
|
|
baa652f314 | ||
|
|
2465676713 | ||
|
|
e45bef317a | ||
|
|
5d0966f466 | ||
|
|
9c377957ec | ||
|
|
ec3d560ad9 | ||
|
|
1dc1854cbf | ||
|
|
511551aee7 | ||
|
|
a9de2c39ff | ||
|
|
a14c3a2095 | ||
|
|
f63e64627c | ||
|
|
d66cccb0db | ||
|
|
0bfb8d845b | ||
|
|
3f3d31eb6a | ||
|
|
9a69644732 | ||
|
|
8eb44c9f23 | ||
|
|
c92bbd2da0 | ||
|
|
03adf538b0 | ||
|
|
949c7a40d9 | ||
|
|
6a819e38ed | ||
|
|
4b1699ab33 | ||
|
|
ab5e08a4b0 | ||
|
|
e7b21697ef | ||
|
|
f4e37134d8 | ||
|
|
58e686765c | ||
|
|
4f9ecae104 | ||
|
|
002063f6e1 | ||
|
|
183564176c | ||
|
|
b0bbd0d87a | ||
|
|
d0a112c791 | ||
|
|
952d4f71c9 | ||
|
|
6126fac915 | ||
|
|
a358377578 | ||
|
|
8a9bca39ea | ||
|
|
0dc1f7cf14 | ||
|
|
1aecf4f90d | ||
|
|
851a75aa54 | ||
|
|
91ae3ec84c | ||
|
|
6996f6d1a9 | ||
|
|
a16e9032f4 | ||
|
|
4b57d7b269 | ||
|
|
6d677828d4 |
No files matched your search
@@ -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,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,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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<!-- Project Title -->
|
||||
[](https://homarr.dev/)
|
||||

|
||||
|
||||
<!-- 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>
|
||||
|
||||
|
||||
[](https://homarr.dev/)
|
||||

|
||||
|
||||
- 🖌️ Highly customizable with an extensive drag and drop grid system
|
||||
- ✨ Integrates seamlessly with your favorite self-hosted applications
|
||||
@@ -53,44 +47,268 @@
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
[](https://homarr.dev/docs/category/widgets)
|
||||

|
||||
|
||||
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/>
|
||||
|
||||
[](https://homarr.dev/docs/category/installation-1)
|
||||
|
||||
Since we are updating Homarr very frequently, we recommend reading our official installation guides:
|
||||

|
||||
|
||||
<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/>
|
||||
|
||||
[](https://opencollective.com/homarr)
|
||||

|
||||
|
||||
<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.
|
||||
|
||||
[](https://argos-ci.com?utm_source=%5Bhomarr%5D&utm_campaign=oss) \
|
||||
[](https://www.pikapods.com/pods?run=homarr-v1)
|
||||
|
||||
|
Before Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@@ -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": [
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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: () => ({}),
|
||||
},
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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}"`);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
| ""
|
||||
|
||||
@@ -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];
|
||||
@@ -26,6 +26,8 @@ export const widgetKinds = [
|
||||
"releases",
|
||||
"mediaReleases",
|
||||
"dockerContainers",
|
||||
"firewall",
|
||||
"notifications",
|
||||
"systemResources",
|
||||
] as const;
|
||||
export type WidgetKind = (typeof widgetKinds)[number];
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [...baseConfig];
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"],
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||