diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 000000000..edc4a8907
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,11 @@
+version = 1
+
+[[analyzers]]
+name = "javascript"
+
+ [analyzers.meta]
+ plugins = ["react"]
+ environment = ["nodejs"]
+
+[[transformers]]
+name = "prettier"
\ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..be4892035
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+Dockerfile
+.dockerignore
+node_modules
+npm-debug.log
+README.md
+.next
+.git
+dev
\ No newline at end of file
diff --git a/.env.example b/.env.example
index e27b97f5f..dc82ea23a 100644
--- a/.env.example
+++ b/.env.example
@@ -4,12 +4,30 @@
# This file will be committed to version control, so make sure not to have any secrets in it.
# If you are cloning this repo, create a copy of this file named `.env` and populate it with your secrets.
-# The database URL is used to connect to your PlanetScale database.
+# This is how you can use the sqlite driver:
+DB_DRIVER='better-sqlite3'
DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
+# Those are the two ways to use the mysql2 driver:
+# 1. Using the URL format:
+# DB_DRIVER='mysql2'
+# DB_URL='mysql://user:password@host:port/database'
+# 2. Using the connection options format:
+# DB_DRIVER='mysql2'
+# DB_HOST='localhost'
+# DB_PORT='3306'
+# DB_USER='username'
+# DB_PASSWORD='password'
+# DB_NAME='name-of-database'
+
# @see https://next-auth.js.org/configuration/options#nextauth_url
AUTH_URL='http://localhost:3000'
# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://next-auth.js.org/configuration/options#secret
AUTH_SECRET='supersecret'
+
+TURBO_TELEMETRY_DISABLED=1
+
+# Configure logging to use winston logger
+NODE_OPTIONS='-r @homarr/log/override'
\ No newline at end of file
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 043f0f9bc..2c9aab231 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,3 @@
# These are supported funding model platforms
-github: juliusmarminge
+open_collective: homarr
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index ae1ccf2da..c2be403e0 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -26,4 +26,3 @@ body:
attributes:
label: Additional information
description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here.
-
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..b551590ab
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,13 @@
+
+
+
+
Homarr
+
+
+**Thank you for your contribution. Please ensure that your pull request meets the following pull request:**
+
+- [ ] Builds without warnings or errors (``pnpm buid``, autofix with ``pnpm format:fix``)
+- [ ] Pull request targets ``dev`` branch
+- [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/)
+- [ ] No shorthand variable names are used (eg. ``x``, ``y``, ``i`` or any abbrevation)
+
diff --git a/.github/renovate.json b/.github/renovate.json
index 0942ac01d..1f5acc772 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -1,17 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "extends": [
- "config:base"
- ],
+ "extends": ["config:recommended"],
"packageRules": [
{
- "matchPackagePatterns": [
- "^@homarr/"
- ],
+ "matchPackagePatterns": ["^@homarr/"],
"enabled": false
}
],
"updateInternalDeps": true,
"rangeStrategy": "bump",
- "automerge": true
-}
\ No newline at end of file
+ "automerge": false,
+ "baseBranches": ["dev"],
+ "dependencyDashboard": false
+}
diff --git a/.github/workflows/automatic-release.yml b/.github/workflows/automatic-release.yml
deleted file mode 100644
index 8489bfce4..000000000
--- a/.github/workflows/automatic-release.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-name: Automatic Release
-
-on:
- schedule:
- - cron: 0 20 * * 5 # At 20:00 on Friday. - https://crontab.guru/#0_20_*_*_5
- workflow_dispatch:
-
-env:
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_AUTOMATIC_RELEASE }}
-
-jobs:
- merge:
- runs-on: ubuntu-latest
- steps:
- - name: Discord notification
- env:
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- uses: Ilshidur/action-discord@master
- with:
- args: 'Preparing the automatic release...'
- - uses: actions/checkout@v4
- - uses: peter-evans/create-pull-request@v5
- id: create-pull-request
- with:
- base: main
- branch: dev
- delete-branch: false
- title: "(chore): version update"
- reviewers: manuel-rw, meierschlumpf
- - name: Check outputs
- if: ${{ steps.create-pull-request.outputs.pull-request-number }}
- run: |
- echo "Pull Request Number - ${{ steps.create-pull-request.outputs.pull-request-number }}"
- echo "Pull Request URL - ${{ steps.create-pull-request.outputs.pull-request-url }}"
- - name: Discord notification
- if: ${{ steps.create-pull-request.outputs.pull-request-number }}
- env:
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
- uses: Ilshidur/action-discord@master
- with:
- args: 'Deployment pull request has been created at [${{ steps.create-pull-request.outputs.pull-request-number }}](${{ steps.create-pull-request.outputs.pull-request-url }})'
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..b8fbfaa9d
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,31 @@
+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/ci.yml b/.github/workflows/code-quality.yml
similarity index 84%
rename from .github/workflows/ci.yml
rename to .github/workflows/code-quality.yml
index cdd48bb9c..44846a959 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/code-quality.yml
@@ -1,4 +1,4 @@
-name: CI
+name: Code quality analysis
on:
pull_request:
@@ -55,3 +55,14 @@ jobs:
- name: Typecheck
run: turbo typecheck
+
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup
+ uses: ./tooling/github/setup
+
+ - name: Test
+ run: pnpm test
diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
new file mode 100644
index 000000000..94bfba67a
--- /dev/null
+++ b/.github/workflows/docker-image.yml
@@ -0,0 +1,89 @@
+name: Docker image
+
+on:
+ pull_request:
+ types:
+ - closed
+ branches:
+ - main
+ workflow_dispatch: {}
+
+permissions:
+ contents: write
+ packages: write
+
+env:
+ TURBO_TELEMETRY_DISABLED: 1
+
+concurrency: production
+
+jobs:
+ deploy:
+ name: Deploy docker image
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [20]
+ steps:
+ - name: Discord notification
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Deployment of an image has been triggered"
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v2
+ with:
+ version: 8
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: "pnpm"
+ - name: Install dependencies
+ run: pnpm install
+ - name: Build artifacts
+ run: pnpm build
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ - name: Bump version and push tag
+ id: githubTagAction
+ uses: anothrNick/github-tag-action@1.69.0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ WITH_V: false
+ DRY_RUN: true
+ - name: Discord notification
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Image has been tagged as ${{ steps.githubTagAction.outputs.new_tag }}"
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=raw,value=latest
+ type=raw,value=${{ steps.githubTagAction.outputs.new_tag }}
+ - name: Build and push
+ id: buildPushAction
+ uses: docker/build-push-action@v5
+ with:
+ platforms: linux/amd64,linux/arm64,linux/riscv64,linux/arm/v7,linux/arm/v6
+ context: .
+ push: false
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ network: host
+ - name: Discord notification
+ env:
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+ uses: Ilshidur/action-discord@master
+ with:
+ args: "Image built with ID ${{ steps.buildPushAction.outputs.imageid }}"
diff --git a/.gitignore b/.gitignore
index a9669defa..8327c8b84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
+.idea/
# testing
coverage
@@ -13,6 +14,9 @@ coverage
out/
next-env.d.ts
+# nest.js
+apps/nestjs/dist
+
# nitro
.nitro/
.output/
@@ -44,4 +48,10 @@ yarn-error.log*
.turbo
# database
-db.sqlite
\ No newline at end of file
+db.sqlite
+
+# logs
+*.log
+
+apps/tasks/tasks.cjs
+apps/websocket/wssServer.cjs
\ No newline at end of file
diff --git a/.nvmrc b/.nvmrc
index 87ec8842b..f203ab89b 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18.18.2
+20.13.1
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 5fcd84524..b24d8b507 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -2,12 +2,86 @@
"version": "0.2.0",
"configurations": [
{
- "name": "Next.js",
+ "name": "dev",
"type": "node-terminal",
"request": "launch",
"command": "pnpm dev",
- "cwd": "${workspaceFolder}/apps/nextjs/",
+ "cwd": "${workspaceFolder}",
"skipFiles": ["/**"]
+ },
+ {
+ "name": "docker dev",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm docker:dev",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": [
+ "/**"
+ ]
+ },
+ {
+ "name": "lint fix",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm lint:fix",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": [
+ "/**"
+ ]
+ },
+ {
+ "name": "format fix",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm format:fix",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": [
+ "/**"
+ ]
+ },
+ {
+ "name": "db push",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm db:push",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": ["/**"]
+ },
+ {
+ "name": "db studio",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm db:studio",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": ["/**"]
+ },
+ {
+ "name": "db migration run",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm db:migration:run",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": ["/**"]
+ },
+ {
+ "name": "test",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm test",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": [
+ "/**"
+ ]
+ },
+ {
+ "name": "test ui",
+ "type": "node-terminal",
+ "request": "launch",
+ "command": "pnpm test:ui",
+ "cwd": "${workspaceFolder}",
+ "skipFiles": [
+ "/**"
+ ]
}
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 44a73ec3a..53c760db0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,5 +3,13 @@
{
"mode": "auto"
}
+ ],
+ "typescript.tsdk": "node_modules\\typescript\\lib",
+ "js/ts.implicitProjectConfig.experimentalDecorators": true,
+ "prettier.configPath": "./tooling/prettier/index.mjs",
+ "cSpell.words": [
+ "superjson",
+ "homarr",
+ "trpc"
]
-}
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..b9471f77e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,87 @@
+FROM node:20.13.1-alpine AS base
+
+FROM base AS builder
+RUN apk add --no-cache libc6-compat
+RUN apk update
+# Set working directory
+WORKDIR /app
+COPY . .
+RUN npm i -g turbo
+RUN turbo prune @homarr/nextjs --docker --out-dir ./next-out
+RUN turbo prune @homarr/tasks --docker --out-dir ./tasks-out
+RUN turbo prune @homarr/websocket --docker --out-dir ./websocket-out
+RUN turbo prune @homarr/db --docker --out-dir ./migration-out
+
+# Add lockfile and package.json's of isolated subworkspace
+FROM base AS installer
+RUN apk add --no-cache libc6-compat curl bash
+RUN apk update
+WORKDIR /app
+
+# First install the dependencies (as they change less often)
+COPY .gitignore .gitignore
+
+COPY --from=builder /app/tasks-out/json/ .
+COPY --from=builder /app/tasks-out/pnpm-lock.yaml ./pnpm-lock.yaml
+RUN corepack enable pnpm && pnpm install
+
+COPY --from=builder /app/websocket-out/json/ .
+COPY --from=builder /app/websocket-out/pnpm-lock.yaml ./pnpm-lock.yaml
+RUN corepack enable pnpm && pnpm install
+
+COPY --from=builder /app/migration-out/json/ .
+COPY --from=builder /app/migration-out/pnpm-lock.yaml ./pnpm-lock.yaml
+RUN corepack enable pnpm && pnpm install
+
+COPY --from=builder /app/next-out/json/ .
+COPY --from=builder /app/next-out/pnpm-lock.yaml ./pnpm-lock.yaml
+
+RUN corepack enable pnpm && pnpm install sharp -w
+
+# Build the project
+COPY --from=builder /app/tasks-out/full/ .
+COPY --from=builder /app/websocket-out/full/ .
+COPY --from=builder /app/next-out/full/ .
+COPY --from=builder /app/migration-out/full/ .
+# Copy static data as it is not part of the build
+COPY static-data ./static-data
+ARG SKIP_ENV_VALIDATION=true
+RUN corepack enable pnpm && pnpm turbo run build
+
+FROM base AS runner
+WORKDIR /app
+
+RUN apk add --no-cache redis
+RUN mkdir /appdata
+RUN mkdir /appdata/db
+RUN mkdir /appdata/redis
+VOLUME /appdata
+
+# Don't run production as root
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+RUN chown -R nextjs:nodejs /appdata
+USER nextjs
+
+COPY --from=installer /app/apps/nextjs/next.config.mjs .
+COPY --from=installer /app/apps/nextjs/package.json .
+
+COPY --from=installer --chown=nextjs:nodejs /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
+COPY --from=installer --chown=nextjs:nodejs /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
+COPY --from=installer --chown=nextjs:nodejs /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
+
+COPY --from=installer --chown=nextjs:nodejs /app/packages/db/migrations ./db/migrations
+
+# Automatically leverage output traces to reduce image size
+# https://nextjs.org/docs/advanced-features/output-file-tracing
+COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/standalone ./
+COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/.next/static ./apps/nextjs/.next/static
+COPY --from=installer --chown=nextjs:nodejs /app/apps/nextjs/public ./apps/nextjs/public
+COPY --chown=nextjs:nodejs scripts/run.sh ./run.sh
+COPY --chown=nextjs:nodejs packages/redis/redis.conf /app/redis.conf
+
+ENV DB_URL='/appdata/db/db.sqlite'
+ENV DB_DIALECT='sqlite'
+ENV DB_DRIVER='better-sqlite3'
+
+CMD ["sh", "run.sh"]
diff --git a/README.md b/README.md
index a635a2b10..2eb9f8222 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,5 @@
-## Quick Start
+# THIS PROJECT IS STILL UNSTABLE AND WE DO NOT PROVIDE ANY SUPPORT FOR ISSUES THAT OCCURE.
+## PLEASE DO NOT OPEN ANY ISSUES OR DISCUSSIONS
+### EVERYTHING IS SUBJECT TO CHANGE
-To get it running, follow the steps below:
-
-### 1. Setup dependencies
-
-```bash
-# Install dependencies
-pnpm i
-
-# Configure environment variables
-# There is an `.env.example` in the root directory you can use for reference
-cp .env.example .env
-
-# Push the Drizzle schema to the database
-pnpm db:push
-```
-
-### 2. Start application
-
-Run `pnpm dev` at the project root folder to start the application.
-
-> **Note**
-> The authentication will currently fail with the message `TypeError: Failed to construct 'URL': Invalid base URL`. This issue will be resolved in the next next-auth beta release. You can track the issue [here](https://github.com/nextauthjs/next-auth/issues/9279).
-
-You can find the initial account creation page at [http://localhost:3000/init/user](http://localhost:3000/init/user).
-After that you can login at [http://localhost:3000/auth/login](http://localhost:3000/auth/login).
-
-### 3. When it's time to add a new package
-
-To add a new package, simply run `pnpm turbo gen init` in the monorepo root. This will prompt you for a package name as well as if you want to install any dependencies to the new package (of course you can also do this yourself later).
-
-The generator sets up the `package.json`, `tsconfig.json` and a `index.ts`, as well as configures all the necessary configurations for tooling around your package such as formatting, linting and typechecking. When the package is created, you're ready to go build out the package.
-
-## References
-
-The stack originates from [create-t3-app](https://github.com/t3-oss/create-t3-app).
-
-A [blog post](https://jumr.dev/blog/t3-turbo) where I wrote how to migrate a T3 app into this.
+Please use [this](https://github.com/ajnart/homarr) version of Homarr when you want to use it
diff --git a/apps/nextjs/next.config.mjs b/apps/nextjs/next.config.mjs
index d48c97617..fcf1321a4 100644
--- a/apps/nextjs/next.config.mjs
+++ b/apps/nextjs/next.config.mjs
@@ -4,31 +4,15 @@ import "@homarr/auth/env.mjs";
/** @type {import("next").NextConfig} */
const config = {
+ output: "standalone",
reactStrictMode: true,
- /** Enables hot reloading for local packages without a build step */
- transpilePackages: [
- "@homarr/api",
- "@homarr/auth",
- "@homarr/db",
- "@homarr/ui",
- "@homarr/validation",
- "@homarr/form",
- "@homarr/notifications",
- "@homarr/spotlight",
- ],
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
experimental: {
- optimizePackageImports: [
- "@mantine/core",
- "@mantine/hooks",
- "@mantine/dates",
- "@mantine/notifications",
- "@mantine/form",
- "@mantine/spotlight",
- ],
+ optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
},
+ transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
domains: ["cdn.jsdelivr.net"],
},
diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json
index d7f597354..5c78e7643 100644
--- a/apps/nextjs/package.json
+++ b/apps/nextjs/package.json
@@ -2,6 +2,7 @@
"name": "@homarr/nextjs",
"version": "0.1.0",
"private": true,
+ "type": "module",
"scripts": {
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
@@ -19,46 +20,58 @@
"@homarr/db": "workspace:^0.1.0",
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/form": "workspace:^0.1.0",
+ "@homarr/gridstack": "^1.0.0",
+ "@homarr/log": "workspace:^",
+ "@homarr/modals": "workspace:^0.1.0",
"@homarr/notifications": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
- "@mantine/hooks": "^7.4.0",
- "@mantine/modals": "^7.4.0",
- "@mantine/tiptap": "^7.4.0",
- "@t3-oss/env-nextjs": "^0.7.1",
- "@tanstack/react-query": "^5.17.1",
- "@tanstack/react-query-devtools": "^5.17.1",
- "@tanstack/react-query-next-experimental": "5.17.1",
- "@tiptap/extension-link": "^2.1.13",
- "@tiptap/react": "^2.1.13",
- "@tiptap/starter-kit": "^2.1.13",
- "@trpc/client": "next",
+ "@mantine/colors-generator": "^7.9.2",
+ "@mantine/hooks": "^7.9.2",
+ "@mantine/modals": "^7.9.2",
+ "@mantine/tiptap": "^7.9.2",
+ "@homarr/server-settings": "workspace:^0.1.0",
+ "@t3-oss/env-nextjs": "^0.10.1",
+ "@tanstack/react-query": "^5.37.1",
+ "@tanstack/react-query-devtools": "^5.37.1",
+ "@tanstack/react-query-next-experimental": "5.37.1",
+ "@trpc/client": "11.0.0-rc.374",
"@trpc/next": "next",
"@trpc/react-query": "next",
"@trpc/server": "next",
- "dayjs": "^1.11.10",
- "jotai": "^2.6.1",
- "mantine-modal-manager": "^7.4.0",
- "next": "^14.0.4",
- "postcss-preset-mantine": "^1.12.3",
- "react": "18.2.0",
- "react-dom": "18.2.0",
- "superjson": "2.2.1"
+ "@xterm/addon-canvas": "^0.7.0",
+ "@xterm/addon-fit": "0.10.0",
+ "@xterm/xterm": "^5.5.0",
+ "chroma-js": "^2.4.2",
+ "dayjs": "^1.11.11",
+ "dotenv": "^16.4.5",
+ "flag-icons": "^7.2.2",
+ "glob": "^10.3.15",
+ "jotai": "^2.8.1",
+ "next": "^14.2.3",
+ "postcss-preset-mantine": "^1.15.0",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "sass": "^1.77.2",
+ "superjson": "2.2.1",
+ "use-deep-compare-effect": "^1.8.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
- "@types/node": "^18.18.13",
- "@types/react": "^18.2.46",
- "@types/react-dom": "^18.2.18",
- "dotenv-cli": "^7.3.0",
- "eslint": "^8.56.0",
- "prettier": "^3.1.0",
- "typescript": "^5.3.3"
+ "@types/chroma-js": "2.4.4",
+ "@types/node": "^20.12.12",
+ "@types/react": "^18.3.2",
+ "@types/react-dom": "^18.3.0",
+ "concurrently": "^8.2.2",
+ "eslint": "^8.57.0",
+ "prettier": "^3.2.5",
+ "tsx": "4.10.5",
+ "typescript": "^5.4.5"
},
"eslintConfig": {
"root": true,
diff --git a/apps/nextjs/public/favicon.ico b/apps/nextjs/public/favicon.ico
index f0058b404..fe62a6143 100644
Binary files a/apps/nextjs/public/favicon.ico and b/apps/nextjs/public/favicon.ico differ
diff --git a/apps/nextjs/public/logo/homarr.png b/apps/nextjs/public/logo/homarr.png
deleted file mode 100644
index b28d4b139..000000000
Binary files a/apps/nextjs/public/logo/homarr.png and /dev/null differ
diff --git a/apps/nextjs/public/logo/logo.png b/apps/nextjs/public/logo/logo.png
new file mode 100644
index 000000000..25581ea5c
Binary files /dev/null and b/apps/nextjs/public/logo/logo.png differ
diff --git a/apps/nextjs/public/t3-icon.svg b/apps/nextjs/public/t3-icon.svg
deleted file mode 100644
index e377165f6..000000000
--- a/apps/nextjs/public/t3-icon.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-
diff --git a/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-accordion.tsx b/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-accordion.tsx
deleted file mode 100644
index 389bed980..000000000
--- a/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-accordion.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-"use client";
-
-import type { PropsWithChildren } from "react";
-import { useRouter } from "next/navigation";
-
-import type { IntegrationKind } from "@homarr/definitions";
-import { Accordion } from "@homarr/ui";
-
-type IntegrationGroupAccordionControlProps = PropsWithChildren<{
- activeTab: IntegrationKind | undefined;
-}>;
-
-export const IntegrationGroupAccordion = ({
- children,
- activeTab,
-}: IntegrationGroupAccordionControlProps) => {
- const router = useRouter();
-
- return (
-
- tab
- ? router.replace(`?tab=${tab}`, {})
- : router.replace("/integrations")
- }
- >
- {children}
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-secret-icons.ts b/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-secret-icons.ts
deleted file mode 100644
index f0d35442f..000000000
--- a/apps/nextjs/src/app/[locale]/(main)/integrations/_integration-secret-icons.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import type { IntegrationSecretKind } from "@homarr/definitions";
-import type { TablerIconsProps } from "@homarr/ui";
-import { IconKey, IconPassword, IconUser } from "@homarr/ui";
-
-export const integrationSecretIcons = {
- username: IconUser,
- apiKey: IconKey,
- password: IconPassword,
-} satisfies Record<
- IntegrationSecretKind,
- (props: TablerIconsProps) => JSX.Element
->;
diff --git a/apps/nextjs/src/app/[locale]/(main)/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/(main)/integrations/new/_integration-new-form.tsx
deleted file mode 100644
index 706cc3df1..000000000
--- a/apps/nextjs/src/app/[locale]/(main)/integrations/new/_integration-new-form.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-
-import type { IntegrationKind } from "@homarr/definitions";
-import { getSecretKinds } from "@homarr/definitions";
-import { useForm, zodResolver } from "@homarr/form";
-import {
- showErrorNotification,
- showSuccessNotification,
-} from "@homarr/notifications";
-import { useI18n } from "@homarr/translation/client";
-import { Button, Fieldset, Group, Stack, TextInput } from "@homarr/ui";
-import type { z } from "@homarr/validation";
-import { validation } from "@homarr/validation";
-
-import { api } from "~/trpc/react";
-import { IntegrationSecretInput } from "../_integration-secret-inputs";
-import {
- TestConnection,
- TestConnectionNoticeAlert,
- useTestConnectionDirty,
-} from "../_integration-test-connection";
-import { revalidatePathAction } from "../../../../revalidatePathAction";
-
-interface NewIntegrationFormProps {
- searchParams: Partial> & {
- kind: IntegrationKind;
- };
-}
-
-export const NewIntegrationForm = ({
- searchParams,
-}: NewIntegrationFormProps) => {
- const t = useI18n();
- const secretKinds = getSecretKinds(searchParams.kind);
- const initialFormValues = {
- name: searchParams.name ?? "",
- url: searchParams.url ?? "",
- secrets: secretKinds.map((kind) => ({
- kind,
- value: "",
- })),
- };
- const { isDirty, onValuesChange, removeDirty } = useTestConnectionDirty({
- defaultDirty: true,
- initialFormValue: initialFormValues,
- });
- const router = useRouter();
- const form = useForm({
- initialValues: initialFormValues,
- validate: zodResolver(validation.integration.create.omit({ kind: true })),
- onValuesChange,
- });
- const { mutateAsync, isPending } = api.integration.create.useMutation();
-
- const handleSubmit = async (values: FormType) => {
- if (isDirty) return;
- await mutateAsync(
- {
- kind: searchParams.kind,
- ...values,
- },
- {
- onSuccess: () => {
- showSuccessNotification({
- title: t("integration.page.create.notification.success.title"),
- message: t("integration.page.create.notification.success.message"),
- });
- void revalidatePathAction("/integrations").then(() =>
- router.push("/integrations"),
- );
- },
- onError: () => {
- showErrorNotification({
- title: t("integration.page.create.notification.error.title"),
- message: t("integration.page.create.notification.error.message"),
- });
- },
- },
- );
- };
-
- return (
-
- );
-};
-
-type FormType = Omit, "kind">;
diff --git a/apps/nextjs/src/app/[locale]/(main)/layout.tsx b/apps/nextjs/src/app/[locale]/(main)/layout.tsx
index 06e6f91fb..ba273b157 100644
--- a/apps/nextjs/src/app/[locale]/(main)/layout.tsx
+++ b/apps/nextjs/src/app/[locale]/(main)/layout.tsx
@@ -1,6 +1,5 @@
import type { PropsWithChildren } from "react";
-
-import { AppShellMain } from "@homarr/ui";
+import { AppShellMain } from "@mantine/core";
import { MainHeader } from "~/components/layout/header";
import { ClientShell } from "~/components/layout/shell";
diff --git a/apps/nextjs/src/app/[locale]/(main)/page.tsx b/apps/nextjs/src/app/[locale]/(main)/page.tsx
index 884e79fd8..26c718534 100644
--- a/apps/nextjs/src/app/[locale]/(main)/page.tsx
+++ b/apps/nextjs/src/app/[locale]/(main)/page.tsx
@@ -1,4 +1,4 @@
-import { Stack, Title } from "@homarr/ui";
+import { Stack, Title } from "@mantine/core";
export default function HomePage() {
return (
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx b/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx
new file mode 100644
index 000000000..908c05046
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/_client-providers/jotai.tsx
@@ -0,0 +1,8 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+import { Provider } from "jotai";
+
+export const JotaiProvider = ({ children }: PropsWithChildren) => {
+ return {children};
+};
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx b/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx
deleted file mode 100644
index c5f8ca826..000000000
--- a/apps/nextjs/src/app/[locale]/_client-providers/modals.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-"use client";
-
-import type { PropsWithChildren } from "react";
-
-import { useScopedI18n } from "@homarr/translation/client";
-
-import { ModalsManager } from "../modals";
-
-export const ModalsProvider = ({ children }: PropsWithChildren) => {
- const t = useScopedI18n("common.action");
- return (
-
- {children}
-
- );
-};
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx b/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx
index 626296d69..8297a73bb 100644
--- a/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx
+++ b/apps/nextjs/src/app/[locale]/_client-providers/next-international.tsx
@@ -3,10 +3,7 @@ import type { PropsWithChildren } from "react";
import { defaultLocale } from "@homarr/translation";
import { I18nProviderClient } from "@homarr/translation/client";
-export const NextInternationalProvider = ({
- children,
- locale,
-}: PropsWithChildren<{ locale: string }>) => {
+export const NextInternationalProvider = ({ children, locale }: PropsWithChildren<{ locale: string }>) => {
return (
{children}
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/session.tsx b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx
new file mode 100644
index 000000000..d4e0538db
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import type { PropsWithChildren } from "react";
+
+import type { Session } from "@homarr/auth";
+import { SessionProvider } from "@homarr/auth/client";
+
+interface AuthProviderProps {
+ session: Session | null;
+}
+
+export const AuthProvider = ({ children, session }: PropsWithChildren) => {
+ return {children};
+};
diff --git a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
index 731c027ae..db34ced95 100644
--- a/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
+++ b/apps/nextjs/src/app/[locale]/_client-providers/trpc.tsx
@@ -1,26 +1,21 @@
"use client";
+import type { PropsWithChildren } from "react";
import { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
-import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
+import { createWSClient, loggerLink, unstable_httpBatchStreamLink, wsLink } from "@trpc/client";
import superjson from "superjson";
-import { env } from "~/env.mjs";
-import { api } from "~/trpc/react";
+import type { AppRouter } from "@homarr/api";
+import { clientApi } from "@homarr/api/client";
-const getBaseUrl = () => {
- if (typeof window !== "undefined") return ""; // browser should use relative url
- if (env.VERCEL_URL) return env.VERCEL_URL; // SSR should use vercel url
+const wsClient = createWSClient({
+ url: "ws://localhost:3001",
+});
- return `http://localhost:${env.PORT}`; // dev SSR should use localhost
-};
-
-export function TRPCReactProvider(props: {
- children: React.ReactNode;
- headers?: Headers;
-}) {
+export function TRPCReactProvider(props: PropsWithChildren) {
const [queryClient] = useState(
() =>
new QueryClient({
@@ -32,35 +27,51 @@ export function TRPCReactProvider(props: {
}),
);
- const [trpcClient] = useState(() =>
- api.createClient({
- transformer: superjson,
+ const [trpcClient] = useState(() => {
+ return clientApi.createClient({
links: [
loggerLink({
enabled: (opts) =>
- process.env.NODE_ENV === "development" ||
- (opts.direction === "down" && opts.result instanceof Error),
- }),
- unstable_httpBatchStreamLink({
- url: `${getBaseUrl()}/api/trpc`,
- headers() {
- const headers = new Map(props.headers);
- headers.set("x-trpc-source", "nextjs-react");
- return Object.fromEntries(headers);
- },
+ process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error),
}),
+ (args) => {
+ return ({ op, next }) => {
+ console.log("op", op.type, op.input, op.path, op.id);
+ if (op.type === "subscription") {
+ const link = wsLink({
+ client: wsClient,
+ transformer: superjson,
+ });
+ return link(args)({ op, next });
+ }
+
+ return unstable_httpBatchStreamLink({
+ transformer: superjson,
+ url: `${getBaseUrl()}/api/trpc`,
+ headers() {
+ const headers = new Headers();
+ headers.set("x-trpc-source", "nextjs-react");
+ return headers;
+ },
+ })(args)({ op, next });
+ };
+ },
],
- }),
- );
+ });
+ });
return (
-
+
-
- {props.children}
-
+ {props.children}
-
+
);
}
+
+function getBaseUrl() {
+ if (typeof window !== "undefined") return window.location.origin;
+ if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
+ return `http://localhost:${process.env.PORT ?? 3000}`;
+}
diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx
new file mode 100644
index 000000000..b1986f12c
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
+
+import { clientApi } from "@homarr/api/client";
+import { useZodForm } from "@homarr/form";
+import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
+import { useScopedI18n } from "@homarr/translation/client";
+import type { z } from "@homarr/validation";
+import { validation } from "@homarr/validation";
+
+interface RegistrationFormProps {
+ invite: {
+ id: string;
+ token: string;
+ };
+}
+
+export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
+ const t = useScopedI18n("user");
+ const router = useRouter();
+ const { mutate, isPending } = clientApi.user.register.useMutation();
+ const form = useZodForm(validation.user.registration, {
+ initialValues: {
+ username: "",
+ password: "",
+ confirmPassword: "",
+ },
+ });
+
+ const handleSubmit = (values: z.infer) => {
+ mutate(
+ {
+ ...values,
+ inviteId: invite.id,
+ token: invite.token,
+ },
+ {
+ onSuccess() {
+ showSuccessNotification({
+ title: t("action.register.notification.success.title"),
+ message: t("action.register.notification.success.message"),
+ });
+ router.push("/auth/login");
+ },
+ onError(error) {
+ const message =
+ error.data?.code === "CONFLICT"
+ ? t("error.usernameTaken")
+ : t("action.register.notification.error.message");
+
+ showErrorNotification({
+ title: t("action.register.notification.error.title"),
+ message,
+ });
+ },
+ },
+ );
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx
new file mode 100644
index 000000000..989406228
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx
@@ -0,0 +1,66 @@
+import { notFound } from "next/navigation";
+import { Card, Center, Stack, Text, Title } from "@mantine/core";
+
+import { auth } from "@homarr/auth/next";
+import { and, db, eq } from "@homarr/db";
+import { invites } from "@homarr/db/schema/sqlite";
+import { getScopedI18n } from "@homarr/translation/server";
+
+import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
+import { RegistrationForm } from "./_registration-form";
+
+interface InviteUsagePageProps {
+ params: {
+ id: string;
+ };
+ searchParams: {
+ token: string;
+ };
+}
+
+export default async function InviteUsagePage({ params, searchParams }: InviteUsagePageProps) {
+ const session = await auth();
+ if (session) notFound();
+
+ const invite = await db.query.invites.findFirst({
+ where: and(eq(invites.id, params.id), eq(invites.token, searchParams.token)),
+ columns: {
+ id: true,
+ token: true,
+ expirationDate: true,
+ },
+ with: {
+ creator: {
+ columns: {
+ name: true,
+ },
+ },
+ },
+ });
+
+ if (!invite || invite.expirationDate < new Date()) notFound();
+
+ const t = await getScopedI18n("user.page.invite");
+
+ return (
+