diff --git a/.github/renovate.json b/.github/renovate.json5
similarity index 75%
rename from .github/renovate.json
rename to .github/renovate.json5
index 69fb4cdad..8978459cf 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json5
@@ -6,7 +6,8 @@
"packageRules": [
{
"matchPackagePatterns": [
- "^@homarr/"
+ "^@homarr/",
+ "tsx" // Disabled for now as version 0.14.4 did not work with the current version of homarr. It resulted in a ERR_MODULE_NOT_FOUND error
],
"enabled": false
},
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index b8fbfaa9d..000000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: Build apps and migration script
-
-on:
- pull_request:
- branches: ["*"]
- push:
- branches: ["main"]
- merge_group:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
- cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
-
-env:
- FORCE_COLOR: 3
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Setup
- uses: ./tooling/github/setup
-
- - name: Copy env
- shell: bash
- run: cp .env.example .env
-
- - name: Build
- run: pnpm build
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
index 228dd9989..a75c144b7 100644
--- a/.github/workflows/code-quality.yml
+++ b/.github/workflows/code-quality.yml
@@ -1,4 +1,4 @@
-name: Code quality analysis
+name: "[Quality] Code Analysis"
on:
pull_request:
@@ -8,7 +8,7 @@ on:
merge_group:
concurrency:
- group: ${{ github.workflow }}-${{ github.ref }}
+ group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
@@ -72,3 +72,15 @@ jobs:
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup
+ uses: ./tooling/github/setup
+ - name: Copy env
+ shell: bash
+ run: cp .env.example .env
+ - name: Build
+ run: pnpm build
diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventions-semantic-commits.yml
similarity index 77%
rename from .github/workflows/conventional-commits.yml
rename to .github/workflows/conventions-semantic-commits.yml
index 8f2216bd7..d26307ba0 100644
--- a/.github/workflows/conventional-commits.yml
+++ b/.github/workflows/conventions-semantic-commits.yml
@@ -1,7 +1,5 @@
-
# https://github.com/webiny/action-conventional-commits?tab=readme-ov-file
-
-name: Conventional Commits
+name: "[Conventions] Semantic Commits"
on:
pull_request:
@@ -12,5 +10,5 @@ jobs:
name: Conventional Commits
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- uses: webiny/action-conventional-commits@v1.3.0
\ No newline at end of file
diff --git a/.github/workflows/pr-conventional-commits.yml b/.github/workflows/conventions-semantic-pull-requests.yml
similarity index 81%
rename from .github/workflows/pr-conventional-commits.yml
rename to .github/workflows/conventions-semantic-pull-requests.yml
index e3dfb587b..cec459930 100644
--- a/.github/workflows/pr-conventional-commits.yml
+++ b/.github/workflows/conventions-semantic-pull-requests.yml
@@ -1,4 +1,4 @@
-name: "Lint PR"
+name: "[Conventions] Semantic PRs"
on:
pull_request_target:
@@ -11,8 +11,7 @@ permissions:
pull-requests: read
jobs:
- main:
- name: Validate PR title
+ validate-pull-request-title:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v5
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/deployment-docker-image.yml
similarity index 95%
rename from .github/workflows/docker-image.yml
rename to .github/workflows/deployment-docker-image.yml
index 046523358..40279c171 100644
--- a/.github/workflows/docker-image.yml
+++ b/.github/workflows/deployment-docker-image.yml
@@ -1,4 +1,4 @@
-name: Docker image
+name: "[Deployment] Release"
on:
pull_request:
@@ -70,13 +70,13 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
tags: |
type=raw,value=latest
type=raw,value=${{ steps.semver.outputs.next }}
- name: Build and push
id: buildPushAction
- uses: docker/build-push-action@v5
+ uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/riscv64,linux/arm/v7,linux/arm/v6
context: .
diff --git a/.github/workflows/deployment-weekly-release.yml b/.github/workflows/deployment-weekly-release.yml
new file mode 100644
index 000000000..db546dbd2
--- /dev/null
+++ b/.github/workflows/deployment-weekly-release.yml
@@ -0,0 +1,95 @@
+name: "[Deployment] Automatic Weekly Release"
+
+on:
+ schedule:
+ - cron: "0 19 * * 5" # https://crontab.guru/#0_19_*_*_5
+ workflow_dispatch:
+ inputs:
+ send-notifications:
+ type: boolean
+ required: false
+ default: true
+ description: Send notifications
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ create-and-merge-pr:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Discord notification
+ if: ${{ github.events.inputs.send-notifications }}
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Get Next Version
+ id: semver
+ uses: ietf-tools/semver-action@v1
+ with:
+ token: ${{ github.token }}
+ branch: dev
+ - name: Create pull request
+ run: "gh pr create --title \"chore(release): automatic release ${{ steps.semver.outputs.next }}\" --body \"**This is an automatic release**.
Manual action may be required for major bumps.
Detected change to be ``${{ steps.semver.outputs.bump }}``
Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``\" --base main --head dev --label automerge"
+ env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ - name: Discord notification
+ if: ${{ github.events.inputs.send-notifications }}
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
+ - name: Obtain token
+ id: obtainApprovalToken
+ uses: tibdex/github-app-token@v2
+ with:
+ private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
+ app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
+ - name: Approve PR
+ env:
+ GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
+ run: |
+ gh pr review --approve --body "Automatically approved by GitHub Action"
+ - id: automerge
+ if: ${{ steps.semver.outputs.bump != 'major' }}
+ name: automerge
+ uses: "pascalgn/automerge-action@v0.16.3"
+ env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ MERGE_METHOD: merge # we prefer merge commits for merging to master
+ MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
+ MERGE_DELETE_BRANCH: false # never set to true!
+ PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
+ MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
+ MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
+ MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
+
+ - name: Merged Discord notification
+ if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.events.inputs.send-notifications }}
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
+ - name: Major Bump Discord notification
+ if: ${{ steps.semver.outputs.bump == 'major' && github.events.inputs.send-notifications }}
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
+ - name: Discord Fail Notification
+ if: failure() && github.events.inputs.send-notifications
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
diff --git a/.github/workflows/renovate-automatic-approval b/.github/workflows/renovate-automatic-approval.yml
similarity index 56%
rename from .github/workflows/renovate-automatic-approval
rename to .github/workflows/renovate-automatic-approval.yml
index b4701ed96..1185fd800 100644
--- a/.github/workflows/renovate-automatic-approval
+++ b/.github/workflows/renovate-automatic-approval.yml
@@ -1,4 +1,4 @@
-name: Approve Renovate PRs
+name: "[Dependency Updates] Auto Approve"
on:
pull_request:
types: [opened, synchronize]
@@ -6,17 +6,20 @@ on:
jobs:
approve-renovate-prs:
runs-on: ubuntu-latest
-
steps:
- name: Checkout code
- uses: actions/checkout@v2
-
+ uses: actions/checkout@v4
+ - name: Obtain token
+ id: obtainToken
+ uses: tibdex/github-app-token@v2
+ with:
+ private_key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
+ app_id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
- name: Install GitHub CLI
run: sudo apt-get install -y gh
-
- name: Approve Renovate PRs
env:
- GITHUB_TOKEN: ${{ secrets.RENOVATE_APPROVE_TOKEN }}
+ GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
for pr in $(gh pr list --author homarr-renovate[bot] --json number --jq .[].number); do
gh pr review $pr --approve --body "Automatically approved by GitHub Action"
diff --git a/.gitignore b/.gitignore
index 8327c8b84..f7d87aa53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,8 +14,8 @@ coverage
out/
next-env.d.ts
-# nest.js
-apps/nestjs/dist
+# artifacts
+packages/db/migrations/*/migrate.cjs
# nitro
.nitro/
diff --git a/.nvmrc b/.nvmrc
index 48b14e6b2..b8e593f52 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20.14.0
+20.15.1
diff --git a/.run/db_migration_mysql_generate.run.xml b/.run/db_migration_mysql_generate.run.xml
new file mode 100644
index 000000000..1eaa49077
--- /dev/null
+++ b/.run/db_migration_mysql_generate.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/db_migration_sqlite_generate.run.xml b/.run/db_migration_sqlite_generate.run.xml
new file mode 100644
index 000000000..ea63d6b81
--- /dev/null
+++ b/.run/db_migration_sqlite_generate.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/db_push.run.xml b/.run/db_push.run.xml
new file mode 100644
index 000000000..e5d56c827
--- /dev/null
+++ b/.run/db_push.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/db_studio.run.xml b/.run/db_studio.run.xml
new file mode 100644
index 000000000..df61c66f7
--- /dev/null
+++ b/.run/db_studio.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/dev.run.xml b/.run/dev.run.xml
new file mode 100644
index 000000000..15ec68c7f
--- /dev/null
+++ b/.run/dev.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/docker_dev.run.xml b/.run/docker_dev.run.xml
new file mode 100644
index 000000000..0aa18c69a
--- /dev/null
+++ b/.run/docker_dev.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/format.run.xml b/.run/format.run.xml
new file mode 100644
index 000000000..801ecad89
--- /dev/null
+++ b/.run/format.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/format_fix.run.xml b/.run/format_fix.run.xml
new file mode 100644
index 000000000..391e76671
--- /dev/null
+++ b/.run/format_fix.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/test.run.xml b/.run/test.run.xml
new file mode 100644
index 000000000..d96c3ec1b
--- /dev/null
+++ b/.run/test.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/test_ui.run.xml b/.run/test_ui.run.xml
new file mode 100644
index 000000000..2f18e7aa9
--- /dev/null
+++ b/.run/test_ui.run.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b9a963aad..90148c6f2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,13 +4,23 @@
"mode": "auto"
}
],
+ "eslint.experimental.useFlatConfig": true,
"typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"prettier.configPath": "./tooling/prettier/index.mjs",
"cSpell.words": [
- "superjson",
+ "cqmin",
"homarr",
+ "jellyfin",
+ "superjson",
"trpc",
- "Umami"
- ]
-}
\ No newline at end of file
+ "Umami",
+ "Sonarr"
+ ],
+ "i18n-ally.dirStructure": "auto",
+ "i18n-ally.enabledFrameworks": ["next-international"],
+ "i18n-ally.localesPaths": ["./packages/translation/src/lang/"],
+ "i18n-ally.enabledParsers": ["ts"],
+ "i18n-ally.extract.keyMaxLength": 0,
+ "i18n-ally.keystyle": "flat"
+}
diff --git a/Dockerfile b/Dockerfile
index b9471f77e..03cd2719d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20.13.1-alpine AS base
+FROM node:20.15.1-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
diff --git a/apps/nextjs/eslint.config.js b/apps/nextjs/eslint.config.js
new file mode 100644
index 000000000..c131bab90
--- /dev/null
+++ b/apps/nextjs/eslint.config.js
@@ -0,0 +1,13 @@
+import baseConfig from "@homarr/eslint-config/base";
+import nextjsConfig from "@homarr/eslint-config/nextjs";
+import reactConfig from "@homarr/eslint-config/react";
+
+/** @type {import('typescript-eslint').Config} */
+export default [
+ {
+ ignores: [".next/**"],
+ },
+ ...baseConfig,
+ ...reactConfig,
+ ...nextjsConfig,
+];
diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs
index fcf1321a4..0cc05f786 100644
--- a/apps/nextjs/next.config.mjs
+++ b/apps/nextjs/next.config.mjs
@@ -1,6 +1,6 @@
// Importing env files here to validate on build
-import "./src/env.mjs";
import "@homarr/auth/env.mjs";
+import "./src/env.mjs";
/** @type {import("next").NextConfig} */
const config = {
@@ -9,6 +9,13 @@ const config = {
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
+ webpack: (config) => {
+ config.module.rules.push({
+ test: /\.node$/,
+ loader: "node-loader",
+ });
+ return config;
+ },
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
},
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index d82c1decd..a6f22572b 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -7,7 +7,7 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
- "lint": "dotenv -v SKIP_ENV_VALIDATION=1 next lint",
+ "lint": "eslint",
"format": "prettier --check . --ignore-path ../../.gitignore",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
@@ -18,6 +18,7 @@
"@homarr/api": "workspace:^0.1.0",
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
+ "@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
@@ -31,16 +32,18 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
- "@mantine/colors-generator": "^7.10.0",
- "@mantine/hooks": "^7.10.0",
- "@mantine/modals": "^7.10.0",
- "@mantine/tiptap": "^7.10.0",
+ "@mantine/colors-generator": "^7.11.1",
+ "@mantine/core": "^7.11.1",
+ "@mantine/hooks": "^7.11.1",
+ "@mantine/modals": "^7.11.1",
+ "@mantine/tiptap": "^7.11.1",
"@homarr/server-settings": "workspace:^0.1.0",
"@t3-oss/env-nextjs": "^0.10.1",
- "@tanstack/react-query": "^5.40.0",
- "@tanstack/react-query-devtools": "^5.40.0",
- "@tanstack/react-query-next-experimental": "5.40.0",
- "@trpc/client": "11.0.0-rc.377",
+ "@tanstack/react-query": "^5.51.1",
+ "@tanstack/react-query-devtools": "^5.51.1",
+ "@tanstack/react-query-next-experimental": "5.51.1",
+ "@tabler/icons-react": "^3.10.0",
+ "@trpc/client": "next",
"@trpc/next": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
@@ -48,17 +51,21 @@
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "^5.5.0",
"chroma-js": "^2.4.2",
+ "clsx": "^2.1.1",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
- "flag-icons": "^7.2.2",
- "glob": "^10.4.1",
- "jotai": "^2.8.2",
- "next": "^14.2.3",
+ "flag-icons": "^7.2.3",
+ "glob": "^11.0.0",
+ "jotai": "^2.9.0",
+ "mantine-react-table": "2.0.0-beta.5",
+ "next": "^14.2.5",
"postcss-preset-mantine": "^1.15.0",
- "react": "18.3.1",
- "react-dom": "18.3.1",
+ "prismjs": "^1.29.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
- "sass": "^1.77.2",
+ "react-simple-code-editor": "^0.14.1",
+ "sass": "^1.77.8",
"superjson": "2.2.1",
"use-deep-compare-effect": "^1.8.1"
},
@@ -67,22 +74,15 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "2.4.4",
- "@types/node": "^20.12.12",
+ "@types/node": "^20.14.10",
+ "@types/prismjs": "^1.26.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"concurrently": "^8.2.2",
- "eslint": "^8.57.0",
- "prettier": "^3.2.5",
- "tsx": "4.11.0",
- "typescript": "^5.4.5"
- },
- "eslintConfig": {
- "root": true,
- "extends": [
- "@homarr/eslint-config/base",
- "@homarr/eslint-config/nextjs",
- "@homarr/eslint-config/react"
- ]
+ "eslint": "^9.7.0",
+ "node-loader": "^2.0.0",
+ "prettier": "^3.3.2",
+ "typescript": "^5.5.3"
},
"prettier": "@homarr/prettier-config"
}
diff --git a/apps/nextjs/public/images/apps/imdb.png b/apps/nextjs/public/images/apps/imdb.png
new file mode 100644
index 000000000..9565159a4
Binary files /dev/null and b/apps/nextjs/public/images/apps/imdb.png differ
diff --git a/apps/nextjs/public/images/apps/lidarr.svg b/apps/nextjs/public/images/apps/lidarr.svg
new file mode 100644
index 000000000..41c5fb58a
--- /dev/null
+++ b/apps/nextjs/public/images/apps/lidarr.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/apps/nextjs/public/images/apps/radarr.svg b/apps/nextjs/public/images/apps/radarr.svg
new file mode 100644
index 000000000..93a4c9232
--- /dev/null
+++ b/apps/nextjs/public/images/apps/radarr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/nextjs/public/images/apps/readarr.svg b/apps/nextjs/public/images/apps/readarr.svg
new file mode 100644
index 000000000..faae05f79
--- /dev/null
+++ b/apps/nextjs/public/images/apps/readarr.svg
@@ -0,0 +1 @@
+
diff --git a/apps/nextjs/public/images/apps/sonarr.svg b/apps/nextjs/public/images/apps/sonarr.svg
new file mode 100644
index 000000000..86c9243db
--- /dev/null
+++ b/apps/nextjs/public/images/apps/sonarr.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/nextjs/public/images/apps/the-tvdb.svg b/apps/nextjs/public/images/apps/the-tvdb.svg
new file mode 100644
index 000000000..b23711d36
--- /dev/null
+++ b/apps/nextjs/public/images/apps/the-tvdb.svg
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/apps/nextjs/public/images/apps/tmdb.png b/apps/nextjs/public/images/apps/tmdb.png
new file mode 100644
index 000000000..9f983b883
Binary files /dev/null and b/apps/nextjs/public/images/apps/tmdb.png differ
diff --git a/apps/nextjs/public/images/apps/truenas.svg b/apps/nextjs/public/images/apps/truenas.svg
new file mode 100644
index 000000000..c3d96ff70
--- /dev/null
+++ b/apps/nextjs/public/images/apps/truenas.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/nextjs/public/images/apps/unraid-alt.svg b/apps/nextjs/public/images/apps/unraid-alt.svg
new file mode 100644
index 000000000..7d695dadc
--- /dev/null
+++ b/apps/nextjs/public/images/apps/unraid-alt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx
index d0345ec72..555862a51 100644
--- a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx
@@ -12,6 +12,8 @@ import { useScopedI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
+import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
+
export const LoginForm = () => {
const t = useScopedI18n("user");
const router = useRouter();
@@ -32,7 +34,7 @@ export const LoginForm = () => {
redirect: false,
callbackUrl: "/",
})
- .then((response) => {
+ .then(async (response) => {
if (!response?.ok || response.error) {
throw response?.error;
}
@@ -41,6 +43,7 @@ export const LoginForm = () => {
title: t("action.login.notification.success.title"),
message: t("action.login.notification.success.message"),
});
+ await revalidatePathActionAsync("/");
router.push("/");
})
.catch((error: Error | string) => {
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx
index 808759af1..346baff75 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_context.tsx
@@ -49,7 +49,6 @@ export const BoardProvider = ({
useEffect(() => {
setReadySections((previous) => previous.filter((id) => data.sections.some((section) => section.id === id)));
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx
index bf68bb902..e3c87aa05 100644
--- a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx
+++ b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx
@@ -33,6 +33,7 @@ export const generateColors = (hex: string) => {
return rgbaColors.map((color) => {
return (
"#" +
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
color
.split("(")[1]!
.replaceAll(" ", "")
diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access.tsx
deleted file mode 100644
index 5739e98b6..000000000
--- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Group, Stack, Tabs } from "@mantine/core";
-import { IconUser, IconUserDown, IconUsersGroup } from "@tabler/icons-react";
-
-import type { RouterOutputs } from "@homarr/api";
-import { clientApi } from "@homarr/api/client";
-import { useScopedI18n } from "@homarr/translation/client";
-import type { TablerIcon } from "@homarr/ui";
-import { CountBadge } from "@homarr/ui";
-
-import type { Board } from "../../_types";
-import { GroupsForm } from "./_access/group-access";
-import { InheritTable } from "./_access/inherit-access";
-import { UsersForm } from "./_access/user-access";
-
-interface Props {
- board: Board;
- initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
-}
-
-export const AccessSettingsContent = ({ board, initialPermissions }: Props) => {
- const { data: permissions } = clientApi.board.getBoardPermissions.useQuery(
- {
- id: board.id,
- },
- {
- initialData: initialPermissions,
- refetchOnMount: false,
- refetchOnWindowFocus: false,
- refetchOnReconnect: false,
- },
- );
-
- const [counts, setCounts] = useState({
- user: initialPermissions.userPermissions.length + (board.creator ? 1 : 0),
- group: initialPermissions.groupPermissions.length,
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
- setCounts(({ user, ...others }) => ({
- user: callback(user),
- ...others,
- }))
- }
- />
-
-
-
-
- setCounts(({ group, ...others }) => ({
- group: callback(group),
- ...others,
- }))
- }
- />
-
-
-
-
-
-
-
- );
-};
-
-interface TabItemProps {
- value: "user" | "group" | "inherited";
- count: number;
- icon: TablerIcon;
-}
-
-const TabItem = ({ value, icon: Icon, count }: TabItemProps) => {
- const t = useScopedI18n("board.setting.section.access.permission");
-
- return (
- }>
-
- {t(`tab.${value}`)}
-
-
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/form.ts b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/form.ts
deleted file mode 100644
index 08520b9d5..000000000
--- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/form.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { BoardPermission } from "@homarr/definitions";
-import { createFormContext } from "@homarr/form";
-
-export interface BoardAccessFormType {
- items: {
- itemId: string;
- permission: BoardPermission;
- }[];
-}
-
-export const [FormProvider, useFormContext, useForm] = createFormContext();
-
-export type OnCountChange = (callback: (prev: number) => number) => void;
diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/inherit-access.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/inherit-access.tsx
deleted file mode 100644
index d887ee696..000000000
--- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/inherit-access.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
-
-import type { RouterOutputs } from "@homarr/api";
-import { getPermissionsWithChildren } from "@homarr/definitions";
-import type { BoardPermission, GroupPermissionKey } from "@homarr/definitions";
-import { useScopedI18n } from "@homarr/translation/client";
-
-import { BoardAccessDisplayRow } from "./board-access-table-rows";
-import { GroupItemContent } from "./group-access";
-
-export interface InheritTableProps {
- initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
-}
-
-const mapPermissions = {
- "board-full-access": "board-full",
- "board-modify-all": "board-change",
- "board-view-all": "board-view",
-} satisfies Partial>;
-
-export const InheritTable = ({ initialPermissions }: InheritTableProps) => {
- const tPermissions = useScopedI18n("board.setting.section.access.permission");
- return (
-
-
-
-
- {tPermissions("field.user.label")}
- {tPermissions("field.permission.label")}
-
-
-
- {initialPermissions.inherited.map(({ group, permission }) => {
- const boardPermission =
- permission in mapPermissions
- ? mapPermissions[permission as keyof typeof mapPermissions]
- : getPermissionsWithChildren([permission]).includes("board-full-access")
- ? "board-full"
- : null;
-
- if (!boardPermission) {
- return null;
- }
-
- return (
- }
- permission={boardPermission}
- />
- );
- })}
-
-
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/user-access.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/user-access.tsx
deleted file mode 100644
index 8ed7e98ee..000000000
--- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_access/user-access.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { useCallback, useState } from "react";
-import Link from "next/link";
-import { Anchor, Box, Button, Group, Stack, Table, TableTbody, TableTh, TableThead, TableTr } from "@mantine/core";
-import { IconPlus } from "@tabler/icons-react";
-
-import type { RouterOutputs } from "@homarr/api";
-import { clientApi } from "@homarr/api/client";
-import { useModalAction } from "@homarr/modals";
-import { useI18n, useScopedI18n } from "@homarr/translation/client";
-import { UserAvatar } from "@homarr/ui";
-
-import type { Board } from "../../../_types";
-import { BoardAccessDisplayRow, BoardAccessSelectRow } from "./board-access-table-rows";
-import type { BoardAccessFormType, OnCountChange } from "./form";
-import { FormProvider, useForm } from "./form";
-import { UserSelectModal } from "./user-select-modal";
-
-export interface FormProps {
- board: Pick;
- initialPermissions: RouterOutputs["board"]["getBoardPermissions"];
- onCountChange: OnCountChange;
-}
-
-export const UsersForm = ({ board, initialPermissions, onCountChange }: FormProps) => {
- const { mutate, isPending } = clientApi.board.saveUserBoardPermissions.useMutation();
- const utils = clientApi.useUtils();
- const [users, setUsers] = useState