Compare commits

...
Author SHA1 Message Date
Thomas Camlong 510c9b945d chore: validate CODEOWNERS in CI and act
Split workflow into validate (PR + act) and update (dev bot PR). Add
structure checks per GitHub docs, API error validation, and act config.
2026-06-04 15:37:31 +02:00
Thomas Camlong 2405e1b931 chore: add per-widget/integration CODEOWNERS generator
Derive owners from git history per feature folder and keep entries in sync
via a bot workflow on dev, following common monorepo automation patterns.
2026-06-04 15:24:07 +02:00
Thomas 41fe5928f1 fix: add bookmark widget option to toggle card borders (#5854) 2026-06-04 11:07:31 +02:00
homarr-renovate[bot] 6eb59d6f53 chore(deps): update tiptap monorepo to v3.24.0 (#5858)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-04 08:58:12 +00:00
Afonso Jorge Ramos 2c6b7a818e fix(immich): respect app color scheme in server-stats widget (#5859) 2026-06-04 09:50:02 +02:00
Thomas fd10788838 fix(auth): relax password complexity requirements to suggestions-only (#5764) 2026-06-04 07:56:34 +02:00
Thomas fe4598f722 fix(docker): restore container selection toolbar broken by Mantine v9 (#5855) 2026-06-03 10:03:37 +02:00
Thomas a755692d81 fix(ui): restore pre-v9 light variant colors (#5856) 2026-06-03 06:49:38 +00:00
homarr-renovate[bot] 3ff0d72ba1 chore(deps): update dependency next-intl to v4.13.0 (#5853)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-03 04:21:57 +00:00
homarr-renovate[bot] a63a191203 chore(deps): update turbo monorepo to ^2.9.16 (#5851)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 23:54:09 +00:00
homarr-renovate[bot] 1be906a0cd chore(deps): update dependency isomorphic-dompurify to ^3.15.0 (#5852)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 21:55:53 +00:00
homarr-renovate[bot] 63d8fa8c26 chore(deps): update dependency @ctrl/qbittorrent to ^9.12.2 (#5834)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 18:47:29 +00:00
homarr-renovate[bot] b5dd6ae8ef chore(deps): update babel monorepo to >=7.29.7 (#5830)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 14:11:26 +00:00
homarr-renovate[bot] d540ad374a chore(deps): update pnpm to v10.34.0 (#5846)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 09:17:24 +00:00
homarr-renovate[bot] 6a971c1986 chore(deps): update mantine monorepo to ^9.2.2 (#5844)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-02 04:13:28 +00:00
homarr-crowdin[bot] d86fe3ddf0 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-06-02 02:23:34 +00:00
homarr-renovate[bot] 06db8960a7 chore(deps): update turbo monorepo to ^2.9.15 (#5841)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 23:12:09 +00:00
homarr-renovate[bot] a5719acf34 chore(deps): update dependency tmp@<=0.2.3 to >=0.2.7 (#5848)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 21:06:21 +00:00
homarr-renovate[bot] d1d597c8e1 chore(deps): update testcontainers-node monorepo to ^12.0.1 (#5845)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-01 16:48:41 +00:00
homarr-renovate[bot] 657e42e4bb chore(deps): update dependency mysql2 to v3.22.4 (#5839)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 18:12:08 +00:00
homarr-renovate[bot] 256ebcb864 chore(deps): update dependency oxlint to ^1.67.0 (#5837)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 16:18:04 +00:00
homarr-renovate[bot] 72e86dee01 chore(deps): update dependency ioredis to v5.11.0 (#5838)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 14:53:18 +00:00
homarr-renovate[bot] b7910b16e0 chore(deps): update dependency oxfmt to ^0.52.0 (#5836)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 11:08:29 +00:00
homarr-renovate[bot] 5a83bb6e39 chore(deps): update dependency dayjs to ^1.11.21 (#5835)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-31 08:54:20 +00:00
homarr-renovate[bot] 433b66582d chore(deps): update dependency hono@<4.6.5 to >=4.12.23 (#5828)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 09:25:22 +00:00
homarr-renovate[bot] acbcb6818b chore(deps): update dependency react-error-boundary to ^6.1.2 (#5829)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-30 06:54:05 +00:00
homarr-crowdin[bot] 07d991a0a1 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-30 05:27:45 +00:00
homarr-renovate[bot] 92364e9574 chore(deps): update tanstack-query monorepo to ^5.100.14 (#5813)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 21:15:51 +00:00
ThomasandMeier Lukas 2991777b0a feat(users): rework general settings (#4915)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:22:55 +02:00
ThomasandMeier Lukas 6cc2ac56a9 fix: prevent GitHub releases check from blocking app on API failure (#5807)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-29 21:14:51 +02:00
homarr-update-contributors[bot] 92086e060f chore: update contributors (#5818) 2026-05-29 20:52:54 +02:00
homarr-renovate[bot] 2aa5ab95e6 chore(deps): update dependency trpc-to-openapi to ^3.3.0 (#5802)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-29 11:44:26 +00:00
Meier Lukas 650f761325 ci(format): fails to apply format due to conflict between --write and --check (#5815) 2026-05-29 11:07:48 +02:00
David be61602576 feat(auth): support custom token endpoint auth methods (#5806) 2026-05-29 10:51:28 +02:00
homarr-crowdin[bot] d01834d975 chore(lang): updated translations from crowdin (#5814) 2026-05-29 10:07:48 +02:00
Thomas Camlong 4a67bf0ef3 fix(ui): use imported theme instead of undefined createTheme
createTheme was called without being imported, causing a runtime
ReferenceError on every page load. Use the already-imported theme
object from @homarr/ui which contains the full Mantine configuration.
2026-05-28 15:54:37 +02:00
Thomas 4454e3fb7a fix: matine ui fixes for concistency (#5733) 2026-05-28 10:59:03 +02:00
Thomas c252b4f9ac feat(manage): unified management page layout (#5758) 2026-05-28 10:56:46 +02:00
homarr-renovate[bot] 4f2b3cf3ee chore(deps): update node.js to v24.16.0 (#5790)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-28 08:09:15 +00:00
Vivaan Dhawan b19549b601 fix: parse Speedtest Tracker created_at as local time (#5751) 2026-05-28 07:48:00 +00:00
homarr-renovate[bot] a0cc68798d chore(deps): update mantine monorepo to ^9.2.1 (#5801)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-28 03:50:55 +00:00
homarr-renovate[bot] 9a538941ee chore(deps): update dependency ws to ^8.21.0 (#5804)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 23:20:59 +00:00
homarr-renovate[bot] fc12500b3e chore(deps): update tanstack-query monorepo to ^5.100.13 (#5803)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 21:45:28 +00:00
homarr-renovate[bot] 64dccec362 chore(deps): update dependency oxfmt to ^0.51.0 (#5799)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 16:46:40 +00:00
homarr-renovate[bot]andThomas Camlong f7ea1b33a0 chore(deps): update mantine monorepo to v9 (#5411)
Co-authored-by: Thomas Camlong <thomas@ajnart.fr>
2026-05-27 17:29:52 +02:00
homarr-renovate[bot] fe3cebb902 chore(deps): update dependency hono@<4.6.5 to >=4.12.22 (#5797)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 13:06:33 +00:00
homarr-renovate[bot] ba1d213870 chore(deps): update dependency oxlint to ^1.66.0 (#5798)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 10:00:11 +00:00
Thomas 8f03a38de5 perf(boards): parallelize board page data fetching (#5736) 2026-05-27 07:44:37 +00:00
605af19cd8 refactor: replace ESLint + Prettier with OXC (oxlint + oxfmt) (#5741)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: Meierschlumpf <63781622+Meierschlumpf@users.noreply.github.com>
2026-05-27 09:37:44 +02:00
homarr-renovate[bot] b4921af60d chore(deps): update dependency sass to ^1.100.0 (#5795)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-27 05:24:00 +00:00
homarr-renovate[bot] 32385206ad chore(deps): update tiptap monorepo to v3.23.6 (#5793)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-26 21:03:11 +00:00
homarr-renovate[bot] 7e59ba3d5e chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.0.14 (#5791)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-26 07:57:31 +00:00
homarr-renovate[bot] 81caaa9d6c chore(deps): update vitest monorepo to ^4.1.7 (#5789)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-25 08:30:36 +00:00
homarr-renovate[bot] 09243148da chore(deps): update dependency ldapts to v8.1.8 (#5788)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-25 04:03:12 +00:00
homarr-update-contributors[bot] c7c57887cf chore: update contributors (#5771) 2026-05-24 22:44:36 +02:00
homarr-renovate[bot] 990a924631 chore(deps): update testcontainers-node monorepo to v12 (#5785) 2026-05-24 22:09:13 +02:00
homarr-renovate[bot] 83caf3ef4b chore(deps): update dependency @types/react to v19.2.15 (#5787)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 19:26:04 +00:00
homarr-renovate[bot] 991f07ae51 chore(deps): update tiptap monorepo to v3.23.5 (#5783)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 15:12:39 +00:00
homarr-renovate[bot] bc05128cd4 chore(deps): update dependency hono@<4.6.5 to >=4.12.21 (#5782)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 13:34:35 +00:00
homarr-renovate[bot] cf0fb8a879 chore(deps): update dependency hono@<4.6.5 to >=4.12.20 (#5780)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 11:25:35 +00:00
homarr-renovate[bot] 1f31735ee0 chore(deps): update dependency isomorphic-dompurify to ^3.14.0 (#5779)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-24 10:02:26 +00:00
homarr-crowdin[bot] ff974bf343 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-24 02:04:49 +00:00
homarr-renovate[bot] d42bbfce17 chore(deps): update tanstack-query monorepo to ^5.100.11 (#5777)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 20:13:50 +00:00
homarr-renovate[bot] 0130303e74 chore(deps): update dependency semver-parser to ^4.2.0 (#5766)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 15:08:34 +00:00
AustinandClaude Sonnet 4.6 66212b4e47 feat(board): persist category collapse state in localStorage for guests (#5714)
* feat(board): persist category collapse state in localStorage for guests

Authenticated users already had their collapse state saved server-side.
This extends the same behaviour to unauthenticated (guest) users by
reading/writing localStorage keyed by section ID.

On mount, if the user is a guest, the stored value is applied to the
disclosure. On toggle, guests write to localStorage instead of calling
the protected API mutation.

Closes #3355

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: set localStorage value as useDisclosure default instead of useEffect

Replaces the useEffect that read localStorage after mount with a direct
initial value computation passed to useDisclosure. Removes the effect,
the open/close destructuring, and the useEffect import.

Note: initialCollapsed is computed during render rather than post-mount,
which may cause a hydration mismatch on SSR if localStorage differs from
the server-provided section.collapsed. The typeof window guard handles
the SSR case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:25:10 +02:00
Thomas f10bb3ae23 feat: auto-open widget settings on add with all integrations pre-selected (#5767)
When adding a widget from the "Choose item to add" modal, the widget
edit modal now opens immediately after placement, allowing the user to
configure options and integrations before the widget renders.

All available integrations for the widget are selected by default.
2026-05-23 16:19:36 +02:00
homarr-renovate[bot] af1991a4a3 chore(deps): update dependency pg to ^8.21.0 (#5776)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-23 13:35:11 +00:00
homarr-releases[bot] 9a9551130c chore: update bug report template (#5775)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-05-22 21:01:54 +00:00
Meier Lukas a93f03eae4 Merge branch 'main' into dev 2026-05-22 22:46:15 +02:00
Thomas df4b57bd41 perf(boards): remove loading overlay and show board immediately (#5735) 2026-05-22 07:06:48 +00:00
ThomasandManuel dade477b32 feat(seed): add demo mode with pre-seeded user (#5739)
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
2026-05-21 16:08:46 +02:00
Thomas 79f88dc3b8 ci: add --affected filtering and fix Turbo cache invalidation (#5740)
* ci: add --affected filtering and fix Turbo cache invalidation

- Remove globalDependencies **/.env which invalidated all caches on any .env change
- Move 40+ env vars from globalEnv to build-specific env (lint/format/typecheck don't need DB_URL etc.)
- Add per-task inputs with test file exclusions so test changes don't trigger build/typecheck
- Enable affectedUsingTaskInputs for task-level --affected detection
- Add --affected to all turbo commands in CI (lint, format, typecheck, build)
- Add fetch-depth: 0 for proper git history comparison
- Add Docker Buildx + GHA layer caching for E2E builds

* ci: add Cloudflare remote cache for Turborepo

Configure turborepo-remote-cache-cloudflare as the remote cache backend.
This commit can be reverted independently to disable remote caching.

* fix(ci): set TURBO_SCM_BASE for --affected to work in PRs

Turbo defaults to comparing against `main`, but PRs target `dev`.
Without explicit base/head refs, --affected falls back to running
all tasks. Use the PR base SHA for pull_requests, the before SHA
for pushes, and HEAD~1 as a last resort.

* perf(typecheck): exclude bundled .cjs/.css from tsc

websocket (835K lines) and tasks (755K lines) had bundled output
files being typechecked due to `include: ["."]` in their tsconfig.
Full typecheck drops from ~70s to ~26s.

* perf(turbo): scope topo task inputs to package.json only

The topo virtual task had no explicit inputs, defaulting to all files.
This caused any source change to cascade cache invalidation through
all dependent packages via ^topo. Since topo is only for dependency
ordering, its hash should only change when dependencies change.

* fix(turbo): disable remote cache signature verification

The cache lives on a private CF worker, signature verification
just adds noise without security benefit.

* fix(turbo): remove runtime env vars from build cache key

These env vars (DB_*, AUTH_*, DOCKER_*, etc.) are all runtime
configuration provided by users when running the Docker container.
They don't influence the build output since Next.js standalone mode
reads process.env at runtime and SKIP_ENV_VALIDATION=true skips
validation during build. Listing them in build.env caused unnecessary
cache busting in local dev.

Also restores coverage report comments per review feedback.

* fix(turbo): include test files in typecheck cache inputs

Test files must be typechecked — excluding them from typecheck inputs
caused changed tests to skip type validation.
2026-05-21 14:11:27 +02:00
homarr-renovate[bot] 4a12f8be61 chore(deps): update dependency hono@<4.6.5 to >=4.12.19 (#5762)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-21 11:33:05 +00:00
homarr-renovate[bot] 044ad9edf9 fix(deps): update dependency better-sqlite3 to ^12.10.0 (#5727) 2026-05-21 07:16:58 +02:00
homarr-renovate[bot] 57d2c6a7e1 chore(deps): update dependency eslint to ^10.4.0 (#5761)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-20 16:59:38 +00:00
homarr-renovate[bot]andMeier Lukas 46f6301fdd chore(deps): update turbo monorepo to ^2.9.14 (#5754)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-20 17:05:07 +02:00
homarr-renovate[bot]andMeier Lukas c3f300d87d chore(deps): update dependency turbo to v2.9.14 [security] (#5760)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-20 16:54:18 +02:00
homarr-renovate[bot] 1de558dd51 chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.0.13 (#5749)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-20 13:46:33 +00:00
homarr-renovate[bot] 2711f3db13 chore(deps): update dependency turbo to v2.9.14 [security] (#5759)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-20 10:57:42 +00:00
homarr-renovate[bot] ff3933b335 chore(deps): update dependency rollup@>=4.0.0 <4.22.4 to >=4.60.4 (#5752)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-20 07:50:23 +00:00
Thomas fa737638a8 fix: exclude .env files from Docker build context (#5738) 2026-05-20 09:05:02 +02:00
homarr-renovate[bot] 97b7c417ab chore(deps): update dependency isomorphic-dompurify to ^3.13.0 (#5750)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-19 12:46:32 +00:00
homarr-renovate[bot] 58d8005d38 chore(deps): update dependency tsdav to ^2.2.2 (#5748)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-19 05:08:44 +00:00
homarr-renovate[bot] 507ac5181f chore(deps): update dependency @playwright/test to ^1.60.0 (#5746)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-19 00:14:25 +00:00
homarr-renovate[bot] b0cdb81533 chore(deps): update tiptap monorepo to v3.23.4 (#5743)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-18 22:10:37 +00:00
homarr-renovate[bot] fc0367a32f chore(deps): update dependency hono@<4.6.5 to >=4.12.18 (#5745)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-18 20:31:45 +00:00
Releases Homarr 48ef8eb6c0 chore(release): 1.61.1 [skip ci] 2026-05-18 19:45:44 +00:00
Meier Lukas 7cd30b640c fix(docker): improve icon matching (#5693)
* fix(docker): improve icon matching

* fix: unused import
2026-05-18 21:45:33 +02:00
Austin e9142c0dbc fix(downloads-widget): increase name column default width ratio 2026-05-18 21:44:47 +02:00
Meier LukasandThomas a9855fe8d3 chore(deps): remove deprecated million lint (#5671)
* chore(deps): remove deprecated million lint

* fix: install now missing playwright dependency

* fix: broken lockfile

* fix: broken lockfile

---------

Co-authored-by: Thomas <thomas@ajnart.fr>
2026-05-18 21:42:02 +02:00
StrandedTurtleandMeier Lukas 7a1272146c fix(tracearr): support absolute image urls (#5654)
* Update base URL handling for tracearr integration

* fix: remove unnecessary prefix from bcrypt hashing

* refactor: add is-absolute-url check

* test: add unit tests

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-18 21:39:01 +02:00
homarr-renovate[bot] db61b6f390 chore(deps): update dependency next-intl to v4.12.0 (#5734)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-18 19:02:14 +00:00
homarr-renovate[bot] 887c294191 chore(deps): update dependency axios@>=1.0.0 <1.8.2 to >=1.16.1 (#5742)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-18 16:31:32 +00:00
homarr-renovate[bot] 2f2a6291a2 chore(deps): update dependency linkifyjs@<4.3.2 to >=4.3.3 (#5732)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-18 04:30:21 +00:00
homarr-renovate[bot] 1b3353118d chore(deps): update tiptap monorepo to v3.23.2 (#5731)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-17 18:06:09 +00:00
homarr-renovate[bot] e03f56ac31 chore(deps): update dependency ws to ^8.20.1 (#5729)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-17 16:10:10 +00:00
homarr-renovate[bot] d571a7f75e chore(deps): update dependency swagger-ui-react to ^5.32.6 (#5726)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-17 11:13:32 +00:00
homarr-renovate[bot] e3b765b9bd chore(deps): update dependency fast-xml-parser to ^5.8.0 (#5725)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-17 06:51:41 +00:00
homarr-renovate[bot] 4da313826d chore(deps): update dependency @types/node to ^24.12.4 (#5724)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 22:53:08 +00:00
homarr-renovate[bot] 948ed7a7fa chore(deps): update vitest monorepo to ^4.1.6 (#5723)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 16:08:51 +00:00
homarr-renovate[bot] f7fc64ebc4 chore(deps): update tanstack-query monorepo to ^5.100.10 (#5722)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 15:04:21 +00:00
homarr-renovate[bot] b7f51d6f5c chore(deps): update dependency next-intl to v4.11.2 (#5721)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 11:58:47 +00:00
homarr-renovate[bot] e185eeb6d7 chore(deps): update dependency yaml to v2.9.0 (#5720)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 10:25:24 +00:00
homarr-renovate[bot] 90d703b870 chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.0.12 (#5719)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-16 08:54:57 +00:00
75640f21e0 chore: merge current beta into dev (#5718)
* ci: support releases on next branch (#5599)

Co-authored-by: Copilot <copilot@github.com>

* perf(memory): combine 3 node processes into one (#5600)

Co-authored-by: Copilot <copilot@github.com>

* ci: add beta release workflow (#5601)

Co-authored-by: Copilot <copilot@github.com>

* chore(release): 1.60.1-beta.1 [skip ci]

* chore: retrigger release v1.60.1-beta.1

* chore(release): 1.60.1-beta.1 [skip ci]

* ci: disable package-manager cache

* chore(release): 1.60.1-beta.1 [skip ci]

* perf: (next branch) reduce idle memory on startup (#5637)

* chore(release): 1.60.1-beta.2 [skip ci]

* chore(release): 1.61.1-beta.1 [skip ci]

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: homarr-releases[bot] <175486441+homarr-releases[bot]@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
2026-05-15 19:35:55 +02:00
Meier Lukas 4aa43ae27a Merge branch 'dev' into beta [skip-ci] 2026-05-15 19:25:38 +02:00
homarr-update-contributors[bot] 6b67671e29 chore: update contributors (#5717)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-05-15 13:59:07 +00:00
homarr-renovate[bot] 220f361b69 chore(deps): update turbo monorepo to ^2.9.12 (#5712)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-14 10:24:19 +00:00
homarr-renovate[bot] a1d6457ced chore(deps): update semantic-release monorepo (#5709)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-14 07:50:20 +00:00
homarr-renovate[bot] f7ade3c676 chore(deps): update dependency @tabler/icons-react to ^3.44.0 (#5711)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-14 04:15:07 +00:00
homarr-renovate[bot] f656d9ca2d chore(deps): update turbo monorepo to ^2.9.11 (#5710)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-14 00:07:48 +00:00
homarr-renovate[bot] cefd90385d chore(deps): update dependency next-intl to v4.11.1 (#5707)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-13 15:51:14 +00:00
homarr-renovate[bot] e5021b1d78 chore(deps): update tiptap monorepo to v3.23.1 (#5708)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-13 12:16:00 +00:00
homarr-renovate[bot] f0bd5132a3 chore(deps): update dependency brace-expansion@>=2.0.0 <=2.0.1 to >=5.0.6 (#5706)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-13 09:52:26 +00:00
homarr-renovate[bot] d356fa41a0 chore(deps): update dependency brace-expansion@>=1.0.0 <=1.1.11 to >=5.0.6 (#5705)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-13 06:55:25 +00:00
homarr-renovate[bot] a9e9ac1cfa chore(deps): update dependency @types/node to ^24.12.3 (#5704)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-12 23:09:48 +00:00
homarr-renovate[bot] 1b3e720701 chore(deps): update dependency vite@>=5.0.0 <=5.4.18 to >=8.0.11 (#5701)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-12 21:28:14 +00:00
homarr-renovate[bot] 2461363d4f chore(deps): update dependency @next/eslint-plugin-next to v16.2.6 (#5703)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-12 20:02:16 +00:00
homarr-renovate[bot] f29e74b6f4 chore(deps): update turbo monorepo to ^2.9.10 (#5702)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-12 17:19:38 +00:00
homarr-renovate[bot] 89dfe82fe9 chore(deps): update dependency @tabler/icons-react to ^3.43.0 (#5700)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 23:02:13 +00:00
homarr-renovate[bot] 48a0f75c31 chore(deps): update react monorepo to v19.2.6 (#5698)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 21:25:44 +00:00
homarr-renovate[bot] 37103cf414 chore(deps): update dependency next to v16.2.6 [security] (#5699)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 19:56:51 +00:00
homarr-renovate[bot] dec34916b2 chore(deps): update nextjs monorepo to v16.2.5 (#5697)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 21:03:28 +02:00
homarr-renovate[bot] 64a4c196c0 chore(deps): update dependency tsdav to ^2.2.1 (#5696)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 17:33:08 +00:00
homarr-renovate[bot] 41d596a1c9 chore(deps): update pnpm to v10.33.4 (#5695)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 15:01:42 +00:00
homarr-renovate[bot] bac0bc7b84 chore(deps): update dependency jotai to ^2.20.0 (#5694)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-11 14:00:57 +00:00
homarr-renovate[bot] 1ac6774821 chore(deps): update pnpm to v10.33.3 (#5689)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-10 15:00:34 +00:00
homarr-renovate[bot] 7adb21ca59 chore(deps): update dependency @tabler/icons-react to ^3.42.0 (#5691)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-10 13:27:39 +00:00
homarr-renovate[bot] 4daf2729e5 chore(deps): update dependency rollup@>=4.0.0 <4.22.4 to >=4.60.3 (#5688)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-10 11:59:24 +00:00
homarr-renovate[bot] 1942c32128 chore(deps): update turbo monorepo to ^2.9.9 (#5690)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-10 10:24:31 +00:00
homarr-renovate[bot] b061a59433 chore(deps): update dependency fast-xml-parser to ^5.7.3 (#5692)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-10 09:01:25 +00:00
Meier Lukas ab867a9e50 chore: remove temporary ts-expect-error 2026-05-10 10:40:29 +02:00
homarr-crowdin[bot] 968338aa97 chore(lang): updated translations from crowdin (#5685)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-05-10 10:38:59 +02:00
Releases Homarr b34102a973 chore(release): 1.61.1-beta.1 [skip ci] 2026-05-08 21:55:01 +00:00
homarr-releases[bot] 378d1e1f1f chore(release): release v1.61.1-beta 2026-05-08 21:53:57 +00:00
Meier Lukas 3a8abe4415 chore(docs): update sitemap 2026-05-08 23:48:31 +02:00
Meier Lukas 6feb37dfd4 Merge branch 'main' into next 2026-05-08 23:46:21 +02:00
Meier Lukas 8042a18dc1 Merge branch 'main' into dev 2026-05-08 22:20:35 +02:00
homarr-releases[bot] c78bf05fc3 chore: update bug report template (#5682)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-05-08 20:17:34 +00:00
Releases Homarr 4635c51fe1 chore(release): 1.61.0 [skip ci] 2026-05-08 20:06:52 +00:00
homarr-releases[bot] 5cc0f97aa2 chore(release): automatic release v1.61.0 2026-05-08 20:05:49 +00:00
homarr-renovate[bot] b581f9b11a chore(deps): update dependency @extractus/feed-extractor to v7.2.1 (#5670)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-08 19:26:12 +00:00
Meier Lukas 756e8d1724 feat(qbittorrent): add api-key support (#5677) 2026-05-08 20:08:37 +02:00
homarr-renovate[bot] 7c64de5cbc chore(deps): update turbo monorepo to ^2.9.8 (#5669)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-08 17:25:39 +00:00
Ashton BattyandMeier Lukas 56af6bcaf0 feat(docker): Podman compatibility, multi-socket support, per-host error isolation (#5425)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-08 17:31:56 +02:00
homarr-renovate[bot] 84b269a80d chore(deps): update tanstack-query monorepo to ^5.100.9 (#5678)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-08 15:30:26 +00:00
Borislaw SotnikowandMeier Lukas 4d30a2e8c4 feat(installations): support secrets file for all environment variables (#5125)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-08 17:11:08 +02:00
Meier Lukas 2d15b3d407 chore(deps): update dependency zod to v4.4.3 (#5675) 2026-05-08 17:10:22 +02:00
Manuel 44a2973b1a revert(deps): downgrade pnpm to v10 (#5673) 2026-05-08 16:03:12 +02:00
Manuel 00a055283e fix(qbittorrent): support version 5.2 (#5672) 2026-05-08 15:44:04 +02:00
Manuel a474dcd961 chore: exclude renovate managed files from codeowners (#5676) 2026-05-08 15:37:41 +02:00
homarr-update-contributors[bot] 69fe180968 chore: update contributors (#5674)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-05-08 13:25:53 +00:00
036f6aa185 chore(deps): update dependency recharts to v3 (#4302)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-08 15:02:52 +02:00
homarr-crowdin[bot]andMeier Lukas 12a1bd7e2e chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-08 13:54:36 +02:00
homarr-renovate[bot] c4ec45a9d0 chore(deps): update pnpm to v11.0.4 (#5668)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-08 03:04:44 +00:00
homarr-renovate[bot]andMeier Lukas b8e0123893 chore(deps): update pnpm to v11 (#5660)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-07 22:46:55 +02:00
homarr-renovate[bot] df953771a7 chore(deps): update dependency axios@&gt;&#x3d;1.0.0 &lt;1.8.2 to >=1.16.0 (#5665)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-07 20:22:00 +00:00
44bd16d677 fix(calendar): keep working integrations when one fails (#5629)
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-07 22:02:21 +02:00
sharpsalatandsharpsalat 277a509f49 feat: add pwa support (#5530)
* feat: add pwa support

* fix: deepsource warning

* fix: format warning

* fix: lint error

---------

Co-authored-by: sharpsalat <deniska.xrenov23@yandex.ru>
2026-05-07 21:53:17 +02:00
homarr-crowdin[bot]andMeier Lukas a9ad991892 chore(lang): updated translations from crowdin (#5623)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-07 21:04:10 +02:00
Meier Lukas 3183914a15 fix(apps): creation on the fly broken (#5605) 2026-05-07 20:59:12 +02:00
Meier Lukas 62be81ea60 fix(lang): portuguese is linked to brazilian portuguese on crowdin (#5622) 2026-05-07 20:54:11 +02:00
Meier Lukas ffc292b22a ci: increase timeout of asset extraction due job cancellation
https://github.com/homarr-labs/homarr/actions/runs/25509168979/attempts/1
2026-05-07 18:48:13 +02:00
Releases Homarr c83fdb2a6e chore(release): 1.60.1-beta.2 [skip ci] 2026-05-07 16:39:41 +00:00
ThomasandMeier Lukas b5c316b825 feat(spotlight): rework !bang search UX (#4813)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-07 18:38:40 +02:00
homarr-releases[bot] 81fdd90e7a chore(release): release v1.60.1-beta 2026-05-07 16:38:37 +00:00
KyleandThomas 831b6221fd feat: add auto/system appearance mode + fix dark mode system resources charts (#5492)
Co-authored-by: Thomas <thomas@ajnart.fr>
2026-05-07 16:28:37 +00:00
homarr-update-contributors[bot] 37edfb2de2 chore: update integrations list readme (#5662)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-05-07 16:11:16 +00:00
43cc68bdd4 feat: add Umami Analytics integration and widget (#5362)
Co-authored-by: Josh G <febrile42@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
2026-05-07 17:50:58 +02:00
Meier Lukas 0937555d49 feat(entity-state): add data attributes for custom-css styling (#5655) 2026-05-07 16:54:57 +02:00
homarr-renovate[bot] 663fe980bf chore(deps): update dependency yaml to v2.8.4 (#5659)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-07 09:39:58 +00:00
homarr-renovate[bot] 9e2c487389 chore(deps): update dependency isomorphic-dompurify to ^3.12.0 (#5658)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-07 06:19:31 +00:00
homarr-renovate[bot] 698e551790 chore(deps): update tanstack-query monorepo to ^5.100.8 (#5657)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-07 06:18:42 +00:00
homarr-renovate[bot] 71e4526ad5 chore(deps): update dependency zod to ^4.4.2 (#5656)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-06 22:04:29 +00:00
Thomas 5ee84f8cf4 perf: (next branch) reduce idle memory on startup (#5637) 2026-05-06 21:39:26 +00:00
Meier Lukas b07a4a0f3e fix(update-checker): hide prereleases on non-prerelease versions (#5645) 2026-05-06 21:23:14 +02:00
homarr-renovate[bot] 62a476a949 chore(deps): update dependency eslint to ^10.3.0 (#5652)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-06 16:35:25 +00:00
homarr-renovate[bot] 6671ac48f9 chore(deps): update tanstack-query monorepo to ^5.100.7 (#5651)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-06 09:30:45 +00:00
homarr-renovate[bot] 3c7a7615ba chore(deps): update dependency nanoid@&gt;&#x3d;4.0.0 &lt;5.0.9 to >=5.1.11 (#5648)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-06 06:13:36 +00:00
homarr-renovate[bot] 2f301bb56a chore(deps): update turbo monorepo to ^2.9.7 (#5649)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-06 03:01:59 +00:00
homarr-renovate[bot] e06abbc176 chore(deps): update dependency nanoid@&gt;&#x3d;4.0.0 &lt;5.0.9 to >=5.1.10 (#5646)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-05 21:10:38 +00:00
homarr-renovate[bot] 04daeba089 chore(deps): update dependency jsdom to ^29.1.1 (#5643)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-05 10:55:08 +00:00
homarr-renovate[bot] 7c83d2cb6e chore(deps): update dependency hono@&lt;4.6.5 to >=4.12.16 (#5642)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-05 10:54:29 +00:00
homarr-renovate[bot] 2829d145af chore(deps): update dependency zod to ^4.4.1 (#5641)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-05 02:56:52 +00:00
homarr-renovate[bot] 44bf36fff9 chore(deps): update dependency timezones-ical-library to ^2.2.0 (#5640)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-05 00:01:35 +00:00
homarr-renovate[bot] acd11c3fc1 chore(deps): update dependency isomorphic-dompurify to ^3.11.0 (#5638)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-04 16:53:37 +00:00
homarr-renovate[bot] b9c701a71b chore(deps): update dependency next-intl to v4.11.0 (#5635)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 19:08:04 +00:00
homarr-renovate[bot] e52ecd64ee chore(deps): update dependency next-intl to v4.10.1 (#5632)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 16:59:56 +00:00
homarr-renovate[bot] af77f729ec chore(deps): update tanstack-query monorepo to ^5.100.6 (#5633)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 16:59:30 +00:00
homarr-renovate[bot] 7c39e4de13 chore(deps): update dependency next-intl to v4.10.0 (#5631)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 15:56:23 +00:00
homarr-renovate[bot] 3e918e5d3d chore(deps): update dependency tsdav to ^2.2.0 (#5627)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 14:57:50 +00:00
homarr-renovate[bot] 9cd45f7832 chore(deps): update trpc monorepo to ^11.17.0 (#5630)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 13:19:32 +00:00
homarr-renovate[bot] 90ca3770a1 chore(deps): update tiptap monorepo to v3.22.5 (#5628)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-03 10:12:22 +00:00
homarr-renovate[bot]andMeier Lukas a183339063 chore(deps): update dependency eslint to v10 (#5038)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-02 21:41:16 +02:00
homarr-renovate[bot] 0a4e4c3b70 chore(deps): update dependency next-intl to v4.9.2 (#5625)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-02 19:07:29 +00:00
homarr-renovate[bot] b52e664d0f chore(deps): update dependency swagger-ui-react to ^5.32.5 (#5621)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-02 13:21:15 +00:00
homarr-renovate[bot] 9f4634a95d chore(deps): update dependency mysql2 to v3.22.3 (#5619)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-02 11:25:52 +00:00
homarr-renovate[bot] a6f6c2e945 chore(deps): update dependency jsdom to ^29.1.0 (#5620)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-05-02 02:53:35 +00:00
homarr-crowdin[bot]andMeier Lukas b6d23e7546 chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-05-02 01:32:16 +00:00
Meier Lukas b0f2c94674 chore: update bug report template 2026-05-01 22:51:17 +02:00
Releases Homarr 80d18bb10d chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:44:34 +00:00
homarr-releases[bot] 2bb0f61da7 chore(release): release v1.60.1-beta 2026-05-01 20:43:32 +00:00
Meier Lukas 24386656fd Merge branch 'next' of https://github.com/homarr-labs/homarr into next 2026-05-01 22:42:07 +02:00
Meier Lukas 453360fe61 ci: disable package-manager cache 2026-05-01 22:42:02 +02:00
Releases Homarr beac1ed780 chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:41:02 +00:00
homarr-releases[bot] aa1b3810f1 chore(release): release v1.60.1-beta 2026-05-01 20:40:05 +00:00
Meier Lukas ce3c1b6cc1 chore: retrigger release v1.60.1-beta.1 2026-05-01 22:38:50 +02:00
Releases Homarr 3f6b830f9b chore(release): 1.60.1-beta.1 [skip ci] 2026-05-01 20:36:08 +00:00
homarr-releases[bot] f0cb79799a chore(release): release v1.60.1-beta 2026-05-01 20:35:08 +00:00
Meier Lukas 7561462594 Merge branch 'main' into next 2026-05-01 22:30:33 +02:00
homarr-releases[bot] 9cf0070442 chore: update bug report template (#5612)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-05-01 20:23:03 +00:00
Meier LukasandCopilot 647afbe2cb ci: add beta release workflow (#5601)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 22:20:58 +02:00
Meier LukasandCopilot bfa8566284 ci: add beta release workflow (#5601)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 22:18:45 +02:00
Releases Homarr e31555776e chore(release): 1.60.0 [skip ci] 2026-05-01 20:12:31 +00:00
homarr-releases[bot] 3c88ae9023 chore(release): automatic release v1.60.0 2026-05-01 20:11:31 +00:00
Meier Lukas 5200c17baf chore: retrigger release v1.60.0 2026-05-01 22:10:44 +02:00
Releases Homarr 97311b338a chore(release): 1.60.0 [skip ci] 2026-05-01 20:04:34 +00:00
homarr-releases[bot] a6b7e9df93 chore(release): automatic release v1.60.0 2026-05-01 20:03:37 +00:00
Meier LukasandCopilot a1aa5505a3 perf(memory): combine 3 node processes into one (#5600)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 16:35:11 +02:00
homarr-update-contributors[bot] e9289bd174 chore: update contributors (#5609)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-05-01 14:25:59 +00:00
Meier Lukas e8edc203b6 fix: migrate from node-cron to croner (#5407) 2026-05-01 16:21:38 +02:00
Meier Lukas ed6680ba6a perf(icons): updater takes 10 seconds to run with high cpu usage (#5608) 2026-05-01 16:21:22 +02:00
homarr-renovate[bot] 16c6929396 chore(deps): update tanstack-query monorepo to ^5.100.5 (#5606)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-30 19:30:50 +00:00
homarr-renovate[bot] dba9efc726 chore(deps): update tanstack-query monorepo to ^5.100.4 (#5603)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-30 17:53:27 +00:00
Meier Lukas ef012981db chore: replace app-id with client-id for actions/create-github-app-token due to deprecation (#5602) 2026-04-30 18:28:55 +02:00
Meier LukasandCopilot e77b9bb3d2 ci: support releases on next branch (#5599)
Co-authored-by: Copilot <copilot@github.com>
2026-04-30 18:28:32 +02:00
Meier Lukas c24f4954ea ci: fix minor issues (#5604) 2026-04-30 18:27:57 +02:00
Meier Lukas a268fd5a26 chore(issue-template): fix issues for integration and widget template 2026-04-30 17:49:24 +02:00
homarr-renovate[bot] 8094458b10 chore(deps): update dependency fast-xml-parser to ^5.7.2 (#5591)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-30 12:21:17 +00:00
homarr-crowdin[bot]andMeier Lukas b88a1a771e chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2026-04-30 12:20:44 +00:00
homarr-renovate[bot] b94d2f15bc chore(deps): update tanstack-query monorepo to ^5.100.2 (#5594)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-30 12:16:25 +00:00
Meier LukasandCopilot ce8e330c82 fix(openapi): openapi document empty (#5579)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 23:29:42 +02:00
homarr-renovate[bot] 0e7b9fa2d4 chore(deps): update dependency hono@&lt;4.6.5 to >=4.12.15 (#5589)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-29 19:33:19 +00:00
Meier Lukas 42b108964b ci: remove turbo cache (#5590) 2026-04-29 20:54:09 +02:00
homarr-renovate[bot] ff630ea647 chore(deps): update dependency dockerode to v5 (#5588)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-29 15:43:59 +02:00
homarr-renovate[bot] 45e5827872 chore(deps): update tanstack-query monorepo to ^5.100.1 (#5586)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-28 20:22:56 +00:00
homarr-renovate[bot] b20e8a1cd1 chore(deps): update pnpm to v10.33.2 (#5585)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-28 17:59:14 +00:00
homarr-renovate[bot] 712da837b1 chore(deps): update dependency vite@&gt;&#x3d;5.0.0 &lt;&#x3d;5.4.18 to >=8.0.10 (#5583)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-28 06:35:34 +00:00
homarr-crowdin[bot] 424999e13a chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-04-28 01:49:29 +00:00
homarr-renovate[bot] f8ce537dce chore(deps): update dependency fastify to ^5.8.5 (#5580)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-27 21:10:03 +00:00
Meier Lukas 3d202d409f feat(about): add opencollective contributors (#5481) 2026-04-27 22:35:42 +02:00
homarr-renovate[bot] 1c8fc989da chore(deps): update dependency axios@&gt;&#x3d;1.0.0 &lt;1.8.2 to >=1.15.2 (#5575)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-27 19:53:12 +00:00
homarr-renovate[bot] a410ee7caf chore(deps): update tanstack-query monorepo to ^5.99.2 (#5563)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-27 19:52:57 +00:00
homarr-renovate[bot] 03f80e1ee2 chore(deps): update dependency isomorphic-dompurify to ^3.10.0 (#5578)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-27 19:52:11 +00:00
homarr-crowdin[bot] c04baab4e9 chore(lang): updated translations from crowdin (#5572)
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-04-27 21:50:03 +02:00
homarr-renovate[bot] feb84c9a62 chore(deps): update pnpm to v10.33.1 (#5576)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-27 11:12:07 +00:00
homarr-renovate[bot] e097aa3ee9 chore(deps): update vitest monorepo to ^4.1.5 (#5574)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-26 17:45:38 +00:00
homarr-update-contributors[bot] a8bf9219a2 chore: update contributors (#5564)
Co-authored-by: Homarr Update Contributors <210161987+homarr-update-contributors[bot]@users.noreply.github.com>
2026-04-26 19:22:10 +02:00
homarr-renovate[bot] 9199a2b221 chore(deps): update dependency vite@&gt;&#x3d;5.0.0 &lt;&#x3d;5.4.18 to >=8.0.9 (#5569)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-26 05:46:37 +00:00
homarr-renovate[bot] fda64b36ae chore(deps): update dependency mysql2 to v3.22.2 (#5573)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-26 02:56:09 +00:00
homarr-releases[bot] ad19e4c954 chore: update bug report template (#5568)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-04-25 16:58:24 +00:00
Releases Homarr d91ca61ff0 chore(release): 1.59.3 [skip ci] 2026-04-24 21:11:59 +00:00
homarr-releases[bot] 05c260667a chore(release): automatic release v1.59.3 2026-04-24 21:11:01 +00:00
Meier Lukas fb5b882e28 chore: retrigger release v1.59.3 2026-04-24 23:09:56 +02:00
Releases Homarr c5e00c2543 chore(release): 1.59.3 [skip ci] 2026-04-24 19:52:38 +00:00
homarr-releases[bot] 33964fbfe8 chore(release): automatic release v1.59.3 2026-04-24 19:51:32 +00:00
homarr-renovate[bot] abd42de067 chore(deps): update dependency axios@&gt;&#x3d;1.0.0 &lt;1.8.2 to >=1.15.1 (#5565)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-24 18:00:22 +00:00
homarr-renovate[bot] 7852f1d987 fix(deps): update dependency better-sqlite3 to ^12.9.0 (#5531)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-24 15:38:24 +02:00
homarr-renovate[bot] e4dab4d1b4 chore(deps): update tanstack-query monorepo to ^5.99.1 (#5559)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-24 05:43:00 +00:00
homarr-renovate[bot] 346f57f3fa chore(deps): update dependency fast-xml-parser to ^5.7.1 (#5556)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-24 05:41:52 +00:00
homarr-renovate[bot] a4c8d67e92 chore(deps): update dependency rollup@&gt;&#x3d;4.0.0 &lt;4.22.4 to >=4.60.2 (#5558)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-24 02:49:24 +00:00
homarr-crowdin[bot] e53cf65054 chore(lang): updated translations from crowdin
* chore(lang): update translations from crowdin

* chore(lang): update translations from crowdin

---------

Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-04-24 01:26:52 +00:00
homarr-renovate[bot] 320e88992a chore(deps): update tiptap monorepo to v3.22.4 (#5557)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-23 13:52:25 +00:00
homarr-renovate[bot] 678e151681 chore(deps): update dependency fast-xml-parser to ^5.7.0 (#5555)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-22 15:21:42 +00:00
homarr-renovate[bot] 5d1517bdf2 chore(deps): update nextjs monorepo to v16.2.4 (#5552)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-22 05:33:24 +00:00
homarr-renovate[bot] dc7b1b005e chore(deps): update dependency mysql2 to v3.22.1 (#5554)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-22 02:46:06 +00:00
homarr-crowdin[bot] 9bbc8b4db9 chore(lang): updated translations from crowdin
Co-authored-by: Crowdin Homarr <190541745+homarr-crowdin[bot]@users.noreply.github.com>
2026-04-22 01:22:33 +00:00
homarr-renovate[bot] 0a32fe0967 chore(deps): update node.js (#5546)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-21 19:21:25 +00:00
homarr-renovate[bot] 0d0dc6aaa0 chore(deps): update dependency isomorphic-dompurify to ^3.9.0 (#5550)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 16:18:07 +00:00
homarr-renovate[bot] bd1a15af22 chore(deps): update dependency nanoid@&gt;&#x3d;4.0.0 &lt;5.0.9 to >=5.1.9 (#5551)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 14:59:22 +00:00
homarr-renovate[bot] 841fdbdd78 chore(deps): update dependency swagger-ui-react to ^5.32.4 (#5549)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 10:56:48 +00:00
homarr-renovate[bot] de595b0623 chore(deps): update dependency prettier to ^3.8.3 (#5545)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 08:16:36 +00:00
homarr-renovate[bot] 9c52a33f3a chore(deps): update dependency fast-xml-parser to ^5.6.0 (#5548)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 05:50:18 +00:00
homarr-renovate[bot] 3b14cb0783 chore(deps): update dependency hono@&lt;4.6.5 to >=4.12.14 [security] (#5524)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-20 02:49:29 +00:00
homarr-renovate[bot] 4a2b221c25 chore(deps): update dependency next-auth to v5.0.0-beta.31 (#5543)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-19 22:44:37 +00:00
homarr-renovate[bot] 3bfa9d669e chore(deps): update dependency typescript-eslint to ^8.58.2 (#5537)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-19 20:49:49 +00:00
homarr-renovate[bot] 8fdfde4f2f chore(deps): update dependency swagger-ui-react to ^5.32.3 (#5539)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-19 20:47:33 +00:00
homarr-renovate[bot] 7873c175b2 chore(deps): update dependency @auth/drizzle-adapter to ^1.11.2 (#5542)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-19 20:47:06 +00:00
homarr-renovate[bot] e583328e87 chore(deps): update dependency @auth/core to ^0.41.2 (#5541)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-19 19:49:34 +00:00
homarr-renovate[bot] 4d024a2a02 chore(deps): update dependency fast-xml-parser to ^5.5.12 (#5534)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-18 16:49:40 +00:00
homarr-renovate[bot] 051e0c3dc0 chore(deps): update dependency @immich/sdk to ^2.7.5 (#5535)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-18 15:45:46 +00:00
homarr-renovate[bot] 5114bba2b1 chore(deps): update dependency dotenv to ^17.4.2 (#5529)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-04-17 20:52:17 +00:00
homarr-releases[bot] ec986147a2 chore: update bug report template (#5533)
Co-authored-by: Releases Homarr <175486441+homarr-releases[bot]@users.noreply.github.com>
2026-04-17 20:09:25 +00:00
434 changed files with 31822 additions and 15652 deletions

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
-P ubuntu-latest=catthehacker/ubuntu:act-latest
--container-architecture linux/amd64
+191
View File
@@ -0,0 +1,191 @@
---
description: Comprehensive Homarr codebase architecture and conventions reference
alwaysApply: true
---
# Homarr Codebase Context
Homarr is an open-source, self-hosted dashboard for managing homelab services. It integrates with 50+ self-hosted apps (media servers, download clients, DNS, NAS, etc.) and provides a drag-and-drop widget-based UI.
## Tech Stack
- **Monorepo**: pnpm workspaces + Turborepo
- **Framework**: Next.js (App Router, `output: "standalone"`) — T3 Stack
- **Language**: TypeScript throughout
- **ORM**: Drizzle (supports SQLite via better-sqlite3, MySQL, PostgreSQL)
- **API**: tRPC (HTTP + WebSocket subscriptions) with superjson + OpenAPI bridge via `trpc-to-openapi`
- **Auth**: NextAuth v5 (database sessions, Credentials/LDAP/OIDC providers)
- **UI**: Mantine v7 (not Tailwind) + Tabler icons
- **State**: Jotai (atoms), TanStack Query (server state via tRPC)
- **Realtime**: WebSocket server (`ws`) for tRPC subscriptions, backed by Redis pub/sub
- **Cron**: `node-cron` in a standalone Fastify service (`apps/tasks`)
- **i18n**: next-intl with `[locale]` dynamic segment (prefix mode: `"never"`, locale from cookie)
- **Testing**: Vitest + jsdom, Playwright for E2E
- **Package manager**: pnpm 10.33.2, Node >= 24.15.0
## Repository Structure
```
homarr/
├── apps/
│ ├── nextjs/ # Main Next.js application (port 3000)
│ ├── tasks/ # Cron job runner + Fastify tRPC API (port 3002)
│ └── websocket/ # Standalone tRPC WebSocket server (port 3001)
├── packages/
│ ├── api/ # tRPC appRouter, procedures, OpenAPI
│ ├── auth/ # NextAuth config, providers, session, API keys
│ ├── db/ # Drizzle schema (3 DB drivers), migrations, queries
│ ├── core/ # Env validation, DB/Redis driver factories, logging
│ ├── definitions/ # Domain enums: WidgetKind, IntegrationKind, permissions
│ ├── widgets/ # All 39 dashboard widgets (definitions + components)
│ ├── integrations/ # Integration classes (HTTP clients to external apps)
│ ├── redis/ # Redis pub/sub channels, caching abstractions
│ ├── translation/ # next-intl setup, locale configs, lang JSON files
│ ├── ui/ # Shared Mantine components, theme, hooks
│ ├── validation/ # Shared zod schemas for API/forms
│ ├── common/ # Shared utilities, IDs, errors
│ ├── cron-jobs/ # Cron job implementations (25+ jobs)
│ ├── cron-jobs-core/ # Cron scheduling primitives
│ ├── cron-job-api/ # tRPC router for job management (start/stop/trigger)
│ ├── cron-job-status/ # Cron status via Redis
│ ├── boards/ # Board context, edit mode, cache updater
│ ├── modals/ # Modal primitives on Mantine
│ ├── modals-collection/ # Feature modals (apps, boards, docker, etc.)
│ ├── form/ # useZodForm (Mantine + zod resolver)
│ ├── forms-collection/# Reusable form UIs (new app, icon picker, upload)
│ ├── spotlight/ # Command palette / search with multiple modes
│ ├── request-handler/ # Server request handlers (feeds, integrations)
│ ├── notifications/ # Mantine notifications wrapper
│ ├── docker/ # Dockerode-based Docker access
│ ├── icons/ # Icon DB/repo integration
│ ├── image-proxy/ # Image proxy + caching
│ ├── ping/ # Reachability / ping utilities
│ ├── analytics/ # Server-side analytics (Umami)
│ ├── server-settings/ # Server setting keys/types
│ ├── settings/ # User-facing settings UI context
│ ├── old-import/ # Legacy Homarr import
│ ├── old-schema/ # Legacy Homarr zod schemas
│ └── cli/ # Node CLI for ops (brocli)
├── tooling/
│ ├── eslint/ # Shared ESLint configs
│ ├── prettier/ # Shared Prettier config
│ ├── typescript/ # Base tsconfig
│ └── github/ # CI setup action
├── development/ # Dev docker-compose (Redis, MySQL, PostgreSQL)
├── e2e/ # E2E test specs
└── Dockerfile # Multi-stage production build
```
## Three Runtime Services
| Service | Port | Role |
|---------|------|------|
| Next.js | 3000 | Main web app (SSR + API routes) |
| WebSocket | 3001 | tRPC subscriptions via `ws` |
| Tasks | 3002 | Cron job runner + management API via Fastify |
In Docker, nginx listens on **7575** and proxies to all three. The tasks service authenticates via `CRON_JOB_API_KEY` header.
## Package Dependency Layers
1. **Foundation**: `core` (env, DB drivers, Redis client, logging) → no `@homarr` deps
2. **Domain**: `definitions` → `common` → `core`
3. **Data**: `db` → `core`, `definitions`, `common`
4. **Auth**: `auth` → `db`, `definitions`, `core`, `validation`
5. **Cross-cutting**: `translation`, `validation`, `redis`, `server-settings`
6. **Feature backends**: `integrations`, `request-handler`, `cron-jobs`, `docker`, `ping`
7. **API surface**: `api` → pulls in auth, db, redis, integrations, etc.
8. **UI stack**: `ui`, `modals`, `form`, `notifications`, `boards`, `spotlight`
9. **Composed features**: `widgets`, `modals-collection`, `forms-collection`
## Key Patterns
### tRPC
- **Server caller** (RSC): `import { api } from "@homarr/api/server"` → cached caller with auth context
- **Client hooks**: `import { clientApi } from "@homarr/api/client"` → React Query hooks
- **Router structure**: `packages/api/src/root.ts` defines top-level namespaces (`user`, `board`, `widget`, `integration`, `docker`, `kubernetes`, etc.)
- **Procedures**: `publicProcedure`, `protectedProcedure`, `permissionRequiredProcedure`, `onboardingProcedure`
- **WebSocket subscriptions**: Client uses `wsLink` when `type === "subscription"`, connects to port 3001
### Multi-Database Support
Drizzle schemas exist in three parallel implementations:
- `packages/db/schema/sqlite.ts`
- `packages/db/schema/mysql.ts`
- `packages/db/schema/postgresql.ts`
Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-postgres`). Migrations are per-engine in `packages/db/migrations/{sqlite,mysql,postgresql}/`.
### Widget System
Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record<WidgetKind, ...>`). Each widget:
- Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })`
- Uses `optionsBuilder.from(factory => ({ ... }))` for typed options
- Loads dynamically via `next/dynamic`
- Fetches data via tRPC client hooks (backed by cron job caches)
### Integration System
Integrations are defined in `packages/definitions/src/integration.ts` (`integrationDefs`) and instantiated via `createIntegrationAsync` in `packages/integrations/src/base/creator.ts`. Each extends abstract `Integration` class with `testConnectionAsync` and typed methods.
### Cron Job System
Jobs defined in `packages/cron-jobs/src/jobs/`. Each job:
- Uses `createCronJob` with a cron expression
- Publishes results to Redis channels
- Widget subscriptions consume those channels via tRPC subscriptions
- Job management (start/stop/trigger) via `cronJobApi` HTTP client to port 3002
### Modals
Created with `createModal(component).withOptions({ defaultTitle })` from `@homarr/modals`. Opened via `useModalAction(SomeModal)`. Feature modals live in `packages/modals-collection/`.
### Forms
`useZodForm` from `@homarr/form` wraps Mantine `useForm` with zod validation. Shared form UIs in `packages/forms-collection/`.
### Routing (App Router)
- `apps/nextjs/src/app/[locale]/` — all UI routes under locale segment
- Route groups: `(home)`, `(content)`, `(board)` — don't affect URLs
- Key routes: `boards/[name]`, `manage/...` (admin), `auth/login`, `init` (onboarding), `widgets/[kind]`
- Locale driven by cookie (no URL prefix)
### Auth Providers
Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `oidc`. Database sessions with httpOnly cookies. API key auth for programmatic access.
## Environment Variables
Key env vars (from `.env.example`):
- `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres`
- `DB_URL`: Database connection string
- `AUTH_SECRET`: NextAuth secret
- `SECRET_ENCRYPTION_KEY`: AES-256-CBC for integration secrets
- `AUTH_PROVIDERS`: Comma-separated (`credentials`, `ldap`, `oidc`)
- `CRON_JOB_API_KEY`: Shared key between Next.js and tasks service
- `REDIS_*`: Redis connection (host, port, password)
- `ENABLE_KUBERNETES`: Optional Kubernetes integration
## Development Commands
- `pnpm dev` — runs all three services in parallel via Turborepo
- `pnpm docker:dev` — starts Redis + MySQL + PostgreSQL containers
- `pnpm db:push` — push schema to SQLite (dev)
- `pnpm db:studio` — Drizzle Studio
- `pnpm test` — Vitest unit tests
- `pnpm test:e2e` — Playwright E2E
- `pnpm lint` / `pnpm format` — ESLint + Prettier
- `pnpm typecheck` — tsc --noEmit across all packages
## Conventions
- Workspace packages use `@homarr/` scope
- All versions managed via pnpm `catalog:` in workspace
- Path alias `~/*` → `./src/*` in Next.js app
- Mantine for all UI (no Tailwind) — primary color is red, `autoContrast: true`
- Icons from `@tabler/icons-react`
- Drag-and-drop via `@dnd-kit/*`
- `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`)
- `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom`
+3 -6
View File
@@ -3,9 +3,6 @@ version = 1
[[analyzers]]
name = "javascript"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
[[transformers]]
name = "prettier"
[analyzers.meta]
plugins = ["react"]
environment = ["nodejs"]
+5 -1
View File
@@ -9,4 +9,8 @@ README.md
.git
dev
.build
e2e
e2e
apps/nextjs/.next
.env
.env.*
!.env.example
+14 -4
View File
@@ -12,10 +12,6 @@ AUTH_SECRET="supersecret"
# or starting the project without any (which will show a randomly generated one).
SECRET_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
# Cron job API key is generated every time the container starts as it is required for communication between nextjs-api and tasks-api
# export CRON_JOB_API_KEY=$(openssl rand -base64 32)
# CRON_JOB_API_KEY="your-generated-api-key"
LOG_LEVEL='info'
# This is how you can use the sqlite driver:
@@ -45,6 +41,20 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
TURBO_TELEMETRY_DISABLED=1
# Turborepo remote cache (optional, for faster CI/local builds)
# Uncomment and fill in to enable remote caching locally.
# CI uses GitHub secrets for these values automatically.
# TURBO_API=https://your-cache-worker.workers.dev
# TURBO_TEAM=team_your_team
# TURBO_TOKEN=your-turbo-token
# TURBO_REMOTE_CACHE_SIGNATURE_KEY=your-signature-key
# Comma-separated paths to Docker-compatible Unix sockets.
# Useful for connecting to multiple runtimes (Docker + Podman) simultaneously.
# When unset and no TCP hosts configured, defaults to /var/run/docker.sock.
# Example: DOCKER_SOCKET_PATHS=/var/run/docker.sock,/run/user/1000/podman/podman.sock
# DOCKER_SOCKET_PATHS=
# Enable kubernetes tool
# ENABLE_KUBERNETES=true
+6
View File
@@ -33,6 +33,12 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.62.0
- 1.61.0
- 1.60.1-beta.1
- 1.60.0
- 1.59.3
- 1.59.2
- 1.59.1
- 1.59.0
- 1.58.1
+9 -6
View File
@@ -19,22 +19,25 @@ body:
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the integration.
description: |
Add any other information related to the integration.
After your integration is merged to `dev`, a bot PR updates [CODEOWNERS](https://github.com/homarr-labs/homarr/blob/dev/CODEOWNERS) so you are requested on future PRs touching your folders. Run `pnpm scripts:generate-codeowners` locally for faster updates. Add email mappings in `scripts/codeowners-overrides.json` if the generator cannot resolve your GitHub user.
- type: dropdown
attributes:
label: Public API available?
description: Is there a public API available, that we can consume in Homarr?
options:
- Yes, available on a website
- Yes, available in the application itself
- No
- "Yes, available on a website"
- "Yes, available in the application itself"
- "No"
validations:
required: true
- type: dropdown
attributes:
label: Are you willing to contribute this yourself?
options:
- Yes
- No
- "Yes"
- "No"
validations:
required: true
+6 -3
View File
@@ -19,12 +19,15 @@ body:
- type: textarea
attributes:
label: Additional information
description: Add any other information related to the widget.
description: |
Add any other information related to the widget.
After your widget is merged to `dev`, a bot PR updates [CODEOWNERS](https://github.com/homarr-labs/homarr/blob/dev/CODEOWNERS) so you are requested on future PRs touching your folders. Run `pnpm scripts:generate-codeowners` locally for faster updates. Add email mappings in `scripts/codeowners-overrides.json` if the generator cannot resolve your GitHub user.
- type: dropdown
attributes:
label: Are you willing to contribute this yourself?
options:
- Yes
- No
- "Yes"
- "No"
validations:
required: true
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write # required to approve pull request
- name: Install GitHub CLI
+56 -9
View File
@@ -11,21 +11,58 @@ on:
concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
# You can leverage Vercel Remote Caching with Turbo to speed up your builds
# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds
env:
FORCE_COLOR: 3
SKIP_ENV_VALIDATION: true
TURBO_API: ${{ secrets.TURBO_API }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD~1' }}
TURBO_SCM_HEAD: ${{ github.sha }}
jobs:
lint-format-autofix:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.head_ref }}
- name: Setup
uses: ./tooling/github/setup
- name: Get changed files
id: changed
run: |
git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files
echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT
- name: Auto-fix lint + format
if: steps.changed.outputs.count != '0'
run: |
xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true
xargs -r -a /tmp/changed-files pnpm exec oxfmt --write
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "style: auto-fix lint and format"
lint:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
uses: ./tooling/github/setup
@@ -35,31 +72,35 @@ jobs:
run: cp .env.example .env
- name: Lint
run: pnpm lint && pnpm lint:ws
run: pnpm turbo lint --affected && pnpm lint:ws
format:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
uses: ./tooling/github/setup
- name: Format
run: pnpm format
run: pnpm turbo format --affected --continue
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
uses: ./tooling/github/setup
- name: Typecheck
run: turbo typecheck
run: pnpm turbo typecheck --affected
test:
runs-on: ubuntu-latest
@@ -82,11 +123,12 @@ jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Setup
uses: ./tooling/github/setup
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build docker image
id: build-docker-image
uses: docker/build-push-action@v7
@@ -94,8 +136,11 @@ jobs:
platforms: linux/amd64
context: .
push: false
load: true
tags: homarr-e2e
network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env:
SKIP_ENV_VALIDATION: true
- name: Install playwright browsers
@@ -109,10 +154,12 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
uses: ./tooling/github/setup
- name: Copy env
shell: bash
run: cp .env.example .env
- name: Build
run: pnpm build
run: pnpm turbo build --affected
@@ -22,7 +22,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.CROWDIN_APP_PRIVATE_KEY }}
app-id: ${{ secrets.CROWDIN_APP_ID }}
client-id: ${{ secrets.CROWDIN_APP_ID }}
permission-contents: write # required to commit to crowdin branch
permission-pull-requests: write # required to create pull request
@@ -0,0 +1,104 @@
name: "[Deployment] Beta Release"
on:
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
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Beta release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Get Next Version
id: semver
uses: ietf-tools/semver-action@v1
with:
token: ${{ github.token }}
branch: next
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): release ${{ steps.semver.outputs.next }}-beta" --body "Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}-beta``" --base beta --head next --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }}-beta (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
id: obtainApprovalToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
GITHUB_TOKEN: ${{ steps.obtainApprovalToken.outputs.token }}
run: |
gh pr review --approve --body "Automatically approved by GitHub Action"
- name: Obtain token
id: obtainMergeToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to beta branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
if: ${{ steps.semver.outputs.bump != 'major' }}
name: automerge
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to beta
MERGE_COMMIT_MESSAGE: "chore(release): release ${{ steps.semver.outputs.next }}-beta"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
MERGE_RETRIES: 20 # 20 retries * MERGE_RETRY_SLEEP until step fails
MERGE_RETRY_SLEEP: 10000 # 10 seconds * MERGE_RETRIES until step fails
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}-beta"
- name: Discord Fail Notification
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@0.4.0
with:
args: "The beta release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+16 -14
View File
@@ -5,6 +5,7 @@ on:
branches:
- main
- beta
- next
- dev
workflow_dispatch:
inputs:
@@ -34,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' }}
SKIP_RELEASE: ${{ github.event_name == 'workflow_dispatch' || github.ref_name == 'dev' || github.ref_name == 'next' }}
outputs:
version: ${{ steps.read-semver.outputs.version || steps.version-fallback.outputs.version }}
git_ref: ${{ steps.read-git-ref.outputs.ref || github.ref }}
@@ -53,8 +54,8 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev / next and publish the release
- uses: actions/checkout@v6
if: env.SKIP_RELEASE == 'false'
@@ -65,8 +66,8 @@ jobs:
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.14.1
cache: "pnpm"
node-version: 24.16.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
- name: Install dependencies
@@ -95,20 +96,21 @@ jobs:
id: read-git-ref
run: |
echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Update dev branch
- name: Update dev / next branch
if: env.SKIP_RELEASE == 'false'
continue-on-error: true # Prevent pipeline from failing when merge fails
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
UPSTREAM_BRANCH: ${{ github.ref_name == 'main' && 'dev' || 'next' }}
run: |
git config user.name "Releases Homarr"
git config user.email "175486441+homarr-releases[bot]@users.noreply.github.com"
git remote set-url origin https://x-access-token:${{ steps.obtainToken.outputs.token }}@github.com/${{ github.repository }}.git
git fetch origin dev
git checkout dev
git pull origin dev
git fetch origin ${{ env.UPSTREAM_BRANCH }}
git checkout ${{ env.UPSTREAM_BRANCH }}
git pull origin ${{ env.UPSTREAM_BRANCH }}
git rebase ${{ github.ref_name }}
git push origin dev
git push origin ${{ env.UPSTREAM_BRANCH }}
build-amd64:
name: Build docker image for amd64
needs: release
@@ -191,7 +193,7 @@ jobs:
name: Extract amd64 asset from docker image
needs: [release, build-amd64]
runs-on: ubuntu-latest
timeout-minutes: 2
timeout-minutes: 5
steps:
- name: Extract amd64
if: needs.release.outputs.skipped == 'false'
@@ -207,7 +209,7 @@ jobs:
name: Extract arm64 asset from docker image
needs: [release, build-arm64]
runs-on: ubuntu-24.04-arm
timeout-minutes: 2
timeout-minutes: 5
steps:
- name: Extract arm64
if: needs.release.outputs.skipped == 'false'
@@ -234,7 +236,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to publish the release
- name: Log in to the Container registry
@@ -274,6 +276,6 @@ jobs:
- name: Discord notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "Successfully deployed images for branch **${{ github.ref_name }}**. Tagged as **${{env.NEXT_VERSION}}**."
+14 -13
View File
@@ -21,10 +21,10 @@ jobs:
timeout-minutes: 2
steps:
- name: Discord notification
if: ${{ github.events.inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "Automatic release has been triggered: [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})"
- name: Checkout repository
@@ -38,14 +38,15 @@ jobs:
token: ${{ github.token }}
branch: dev
- name: Create pull request
id: create-pull-request
run: 'gh pr create --title "chore(release): automatic release ${{ steps.semver.outputs.next }}" --body "**This is an automatic release**.<br/>Manual action may be required for major bumps.<br/>Detected change to be ``${{ steps.semver.outputs.bump }}``<br/>Bump version from ``${{ steps.semver.outputs.current }}`` to ``${{ steps.semver.outputs.next }}``" --base main --head dev --label automerge'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: Discord notification
if: ${{ github.events.inputs.send-notifications }}
if: ${{ github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "Created a release PR ${{ steps.create-pull-request.outputs.url }} for version ${{ steps.semver.outputs.next }} (new behaviour: ${{ steps.semver.outputs.bump }})"
- name: Obtain token
@@ -53,7 +54,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_APPROVE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
client-id: ${{ secrets.RENOVATE_APPROVE_APP_ID }}
permission-pull-requests: write
- name: Approve PR
env:
@@ -65,7 +66,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # write to main branch (due to merge)
permission-pull-requests: write # merge pull request
- id: automerge
@@ -74,7 +75,7 @@ jobs:
uses: "pascalgn/automerge-action@v0.16.4"
env:
GITHUB_TOKEN: ${{ steps.obtainMergeToken.outputs.token }}
MERGE_METHOD: merge # we prefer merge commits for merging to master
MERGE_METHOD: merge # we prefer merge commits for merging to main
MERGE_COMMIT_MESSAGE: "chore(release): automatic release ${{ steps.semver.outputs.next }}"
MERGE_DELETE_BRANCH: false # never set to true!
PULL_REQUEST: "${{ steps.create-pull-request.outputs.pr_number }}"
@@ -83,23 +84,23 @@ jobs:
MERGE_REQUIRED_APPROVALS: 0 # do not require approvals
- name: Merged Discord notification
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.events.inputs.send-notifications }}
if: ${{ steps.automerge.outputs.mergeResult == 'merged' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "Merged PR ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Major Bump Discord notification
if: ${{ steps.semver.outputs.bump == 'major' && github.events.inputs.send-notifications }}
if: ${{ steps.semver.outputs.bump == 'major' && github.event.inputs.send-notifications }}
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "The release PR must be manually merged because the next version is a major version: ${{ steps.create-pull-request.outputs.url }} for release ${{ steps.semver.outputs.next }}"
- name: Discord Fail Notification
if: failure() && github.events.inputs.send-notifications
if: failure() && github.event.inputs.send-notifications
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
uses: Ilshidur/action-discord@master
uses: Ilshidur/action-discord@0.4.0
with:
args: "The automatic release workflow [run ${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) has failed"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_DOCS_RELEASE_APP_PRIVATE_KEY }}
app-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
client-id: ${{ vars.HOMARR_DOCS_RELEASE_APP_ID }}
owner: homarr-labs
repositories: |
documentation
@@ -48,7 +48,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
app-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
+131
View File
@@ -0,0 +1,131 @@
name: Update CODEOWNERS
on:
workflow_dispatch: {}
pull_request:
paths:
- CODEOWNERS
- packages/widgets/src/**
- packages/integrations/src/**
- packages/api/src/router/widgets/**
- packages/cron-jobs/src/jobs/**
- scripts/generate-codeowners.mts
- scripts/validate-codeowners.mts
- scripts/codeowners-overrides.json
push:
paths:
- packages/widgets/src/**
- packages/integrations/src/**
- packages/api/src/router/widgets/**
- packages/cron-jobs/src/jobs/**
- scripts/generate-codeowners.mts
- scripts/validate-codeowners.mts
- scripts/codeowners-overrides.json
branches:
- dev
permissions:
contents: read
pull-requests: write
jobs:
validate:
name: Validate CODEOWNERS
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- uses: pnpm/action-setup@v6
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check CODEOWNERS is up to date
run: pnpm exec tsx ./scripts/generate-codeowners.mts --check
- name: Validate CODEOWNERS
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: pnpm exec tsx ./scripts/validate-codeowners.mts
update:
name: Open CODEOWNERS update PR
needs: validate
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: update-codeowners
cancel-in-progress: false
steps:
- name: Obtain token
id: obtainToken
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write
permission-pull-requests: write
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- uses: pnpm/action-setup@v6
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate CODEOWNERS
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: pnpm scripts:generate-codeowners
- name: Validate CODEOWNERS
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: pnpm exec tsx ./scripts/validate-codeowners.mts
- name: Create Pull Request
id: create-pull-request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.obtainToken.outputs.token }}
branch: update-codeowners
base: dev
title: "chore: update widget/integration CODEOWNERS"
delete-branch: true
commit-message: "chore: update widget/integration CODEOWNERS"
body: |
This PR updates per-widget and per-integration CODEOWNERS entries from git history.
Unmapped authors can be added in `scripts/codeowners-overrides.json`.
- name: Install GitHub CLI
run: sudo apt-get install -y gh
- name: Enable auto-merge
if: steps.create-pull-request.outputs.pull-request-number != ''
env:
GITHUB_TOKEN: ${{ steps.obtainToken.outputs.token }}
run: |
gh pr merge ${{ steps.create-pull-request.outputs.pull-request-number }} --auto --squash
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
@@ -24,7 +24,7 @@ jobs:
uses: actions/create-github-app-token@v3
with:
private-key: ${{ secrets.HOMARR_UPDATE_CONTRIBUTORS_PRIVATE_KEY }}
app-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
client-id: ${{ vars.HOMARR_UPDATE_CONTRIBUTORS_APP_ID }}
permission-contents: write # required to commit to branch
permission-pull-requests: write # required to create pr & enable automerge
- name: Checkout code
+1 -1
View File
@@ -71,4 +71,4 @@ en.d.json.ts
# personal agent config
AGENTS.md
.opencode
.opencode
+1 -1
View File
@@ -1 +1 @@
24.14.1
24.16.0
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"endOfLine": "lf",
"sortPackageJson": true,
"ignorePatterns": ["**/*.cjs", "**/lang/**", "static-data/**", "CHANGELOG.md"]
}
+37
View File
@@ -0,0 +1,37 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import", "jsx-a11y"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"typescript/consistent-type-imports": ["warn", { "prefer": "type-imports", "fixStyle": "separate-type-imports" }],
"typescript/no-non-null-assertion": "error",
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
"unicorn/no-new-array": "warn",
"unicorn/no-useless-spread": "warn",
"unicorn/no-useless-length-check": "warn",
"unicorn/no-useless-fallback-in-spread": "warn",
"jsx-a11y/prefer-tag-over-role": "warn",
"oxc/const-comparisons": "warn",
"react-hooks/exhaustive-deps": "warn",
"react/no-children-prop": "warn",
"jsx-a11y/no-autofocus": "warn",
"jsx-a11y/img-redundant-alt": "warn",
"react/react-in-jsx-scope": "off"
},
"options": {
"reportUnusedDisableDirectives": "off"
},
"ignorePatterns": ["**/*.config.js", "**/*.config.mjs", "**/*.cjs", "**/lang/*.d.json.ts", ".next/**"],
"settings": {
"react": { "version": "19" }
}
}
+1 -2
View File
@@ -31,6 +31,5 @@ scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n|
# refactorTemplates:
# - i18n.get("$1")
# If set to true, only enables this custom framework (will disable all built-in frameworks)
monopoly: true
monopoly: true
+5 -15
View File
@@ -15,9 +15,7 @@
"request": "launch",
"command": "pnpm docker:dev",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "lint fix",
@@ -25,9 +23,7 @@
"request": "launch",
"command": "pnpm lint:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "format fix",
@@ -35,9 +31,7 @@
"request": "launch",
"command": "pnpm format:fix",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "db push",
@@ -69,9 +63,7 @@
"request": "launch",
"command": "pnpm test",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
},
{
"name": "test ui",
@@ -79,9 +71,7 @@
"request": "launch",
"command": "pnpm test:ui",
"cwd": "${workspaceFolder}",
"skipFiles": [
"<node_internals>/**"
]
"skipFiles": ["<node_internals>/**"]
}
]
}
+3 -14
View File
@@ -1,13 +1,6 @@
{
"eslint.workingDirectories": [
{
"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": [
"ajnart",
"cqmin",
@@ -34,11 +27,7 @@
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.displayLanguage": "en",
"i18n-ally.enabledFrameworks": [
"custom"
],
"i18n-ally.localesPaths": [
"packages/translation/src/lang",
],
"i18n-ally.keystyle": "nested",
"i18n-ally.enabledFrameworks": ["custom"],
"i18n-ally.localesPaths": ["packages/translation/src/lang"],
"i18n-ally.keystyle": "nested"
}
+3906 -3814
View File
File diff suppressed because it is too large. Load diff
+87
View File
@@ -3,7 +3,94 @@
# Exempt Renovate‑managed files (no owners)
package.json
**/package.json
package-lock.json
**/package-lock.json
pnpm-lock.yaml
**/pnpm-lock.yaml
pnpm-workspace.yaml
Dockerfile
docker-compose.yml
# BEGIN GENERATED widget/integration owners
/packages/api/src/router/widgets/anchor-notes.ts /packages/integrations/src/anchor/ /packages/widgets/src/anchor-note/ @omariqbal
/packages/api/src/router/widgets/app.ts /packages/widgets/src/app/ @manuel-rw
/packages/api/src/router/widgets/calendar.ts /packages/widgets/src/calendar/ @manuel-rw
/packages/api/src/router/widgets/coolify.ts /packages/integrations/src/coolify/ /packages/widgets/src/coolify/ @Rhiz3K
/packages/api/src/router/widgets/dns-hole.ts /packages/cron-jobs/src/jobs/integrations/dns-hole.ts /packages/widgets/src/dns-hole/controls/ @manuel-rw
/packages/api/src/router/widgets/dns-hole.ts /packages/cron-jobs/src/jobs/integrations/dns-hole.ts /packages/widgets/src/dns-hole/summary/ @manuel-rw
/packages/api/src/router/widgets/downloads.ts /packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/widgets/src/downloads/ @manuel-rw
/packages/api/src/router/widgets/firewall.ts /packages/cron-jobs/src/jobs/integrations/firewall.ts /packages/widgets/src/firewall/ @Oupsman
/packages/api/src/router/widgets/health-monitoring.ts /packages/cron-jobs/src/jobs/integrations/health-monitoring.ts /packages/widgets/src/health-monitoring/ @dgshue
/packages/api/src/router/widgets/immich.ts /packages/integrations/src/immich/ /packages/widgets/src/immich/album-carousel/ @manuel-rw
/packages/api/src/router/widgets/immich.ts /packages/integrations/src/immich/ /packages/widgets/src/immich/server-stats/ @manuel-rw
/packages/api/src/router/widgets/indexer-manager.ts /packages/cron-jobs/src/jobs/integrations/indexer-manager.ts /packages/widgets/src/indexer-manager/ @manuel-rw
/packages/api/src/router/widgets/media-release.ts /packages/widgets/src/media-releases/ @manuel-rw
/packages/api/src/router/widgets/media-requests.ts /packages/cron-jobs/src/jobs/integrations/media-requests.ts /packages/widgets/src/media-requests/list/ @manuel-rw
/packages/api/src/router/widgets/media-requests.ts /packages/cron-jobs/src/jobs/integrations/media-requests.ts /packages/widgets/src/media-requests/stats/ @manuel-rw
/packages/api/src/router/widgets/media-server.ts /packages/cron-jobs/src/jobs/integrations/media-server.ts /packages/widgets/src/media-server/ @manuel-rw
/packages/api/src/router/widgets/media-transcoding.ts /packages/cron-jobs/src/jobs/integrations/media-transcoding.ts /packages/integrations/src/media-transcoding/ /packages/widgets/src/media-transcoding/ @manuel-rw
/packages/api/src/router/widgets/minecraft.ts /packages/widgets/src/minecraft/server-status/ @manuel-rw
/packages/api/src/router/widgets/network-controller.ts /packages/cron-jobs/src/jobs/integrations/network-controller.ts /packages/widgets/src/network-controller/network-status/ @pitschi
/packages/api/src/router/widgets/network-controller.ts /packages/cron-jobs/src/jobs/integrations/network-controller.ts /packages/widgets/src/network-controller/summary/ @pitschi
/packages/api/src/router/widgets/notebook.ts /packages/widgets/src/notebook/ @manuel-rw
/packages/api/src/router/widgets/notifications.ts /packages/cron-jobs/src/jobs/integrations/notifications.ts /packages/widgets/src/notifications/ @itzTheMeow
/packages/api/src/router/widgets/releases.ts /packages/widgets/src/releases/ @Aandree5
/packages/api/src/router/widgets/rssFeed.ts /packages/widgets/src/rssFeed/ @manuel-rw
/packages/api/src/router/widgets/smart-home.ts /packages/cron-jobs/src/jobs/integrations/home-assistant.ts /packages/widgets/src/smart-home/entity-state/ @manuel-rw
/packages/api/src/router/widgets/smart-home.ts /packages/cron-jobs/src/jobs/integrations/home-assistant.ts /packages/widgets/src/smart-home/execute-automation/ @manuel-rw
/packages/api/src/router/widgets/speedtest-tracker.ts /packages/integrations/src/speedtest-tracker/ /packages/widgets/src/speedtest-tracker/ @kylegrieder
/packages/api/src/router/widgets/stocks.ts /packages/widgets/src/stocks/ @jackwellerreal
/packages/api/src/router/widgets/timetable.ts /packages/cron-jobs/src/jobs/integrations/timetable.ts /packages/widgets/src/timetable/ @manuel-rw
/packages/api/src/router/widgets/tracearr.ts /packages/cron-jobs/src/jobs/integrations/tracearr.ts /packages/integrations/src/tracearr/ /packages/widgets/src/tracearr/ @StrandedTurtle
/packages/api/src/router/widgets/umami.ts /packages/cron-jobs/src/jobs/integrations/umami.ts /packages/integrations/src/umami/ /packages/widgets/src/umami/ @febrile42
/packages/api/src/router/widgets/weather.ts /packages/cron-jobs/src/jobs/weather.ts /packages/widgets/src/weather/ @manuel-rw
/packages/cron-jobs/src/jobs/docker.ts /packages/widgets/src/docker/ @ashtonbatty
/packages/cron-jobs/src/jobs/integrations/dns-hole.ts /packages/integrations/src/pi-hole/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/aria2/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/deluge/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/nzbget/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/qbittorrent/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/sabnzbd/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/slskd/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/downloads.ts /packages/integrations/src/download-client/transmission/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/home-assistant.ts /packages/integrations/src/homeassistant/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/media-organizer.ts /packages/integrations/src/media-organizer/lidarr/ @NicolasNewman
/packages/cron-jobs/src/jobs/integrations/media-organizer.ts /packages/integrations/src/media-organizer/radarr/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/media-organizer.ts /packages/integrations/src/media-organizer/readarr/ @manuel-rw
/packages/cron-jobs/src/jobs/integrations/media-organizer.ts /packages/integrations/src/media-organizer/sonarr/ @manuel-rw
/packages/integrations/src/adguard-home/ @manuel-rw
/packages/integrations/src/codeberg/ @Aandree5
/packages/integrations/src/dashdot/ @dgshue
/packages/integrations/src/docker-hub/ @Aandree5
/packages/integrations/src/emby/ @manuel-rw
/packages/integrations/src/github-container-registry/ @manuel-rw
/packages/integrations/src/github/ @manuel-rw
/packages/integrations/src/gitlab/ @Aandree5
/packages/integrations/src/glances/ @dgshue
/packages/integrations/src/ical/ @NicolasNewman
/packages/integrations/src/jellyfin/ @manuel-rw
/packages/integrations/src/jellyseerr/ @manuel-rw
/packages/integrations/src/linuxserverio/ @Aandree5
/packages/integrations/src/nextcloud/ @manuel-rw
/packages/integrations/src/npm/ @Aandree5
/packages/integrations/src/ntfy/ @manuel-rw
/packages/integrations/src/openmediavault/ @dgshue
/packages/integrations/src/opnsense/ @manuel-rw
/packages/integrations/src/overseerr/ @manuel-rw
/packages/integrations/src/plex/ @manuel-rw
/packages/integrations/src/prowlarr/ @manuel-rw
/packages/integrations/src/proxmox/ @manuel-rw
/packages/integrations/src/quay/ @Aandree5
/packages/integrations/src/search-ch/ @manuel-rw
/packages/integrations/src/seerr/ @manuel-rw
/packages/integrations/src/truenas/ @dgshue
/packages/integrations/src/unifi-controller/ @manuel-rw
/packages/integrations/src/unraid/ @dgshue
/packages/widgets/src/bookmarks/ @manuel-rw
/packages/widgets/src/clock/ @manuel-rw
/packages/widgets/src/iframe/ @manuel-rw
/packages/widgets/src/system-disks/ @manuel-rw
/packages/widgets/src/system-resources/ @kylegrieder
/packages/widgets/src/video/ @manuel-rw
# END GENERATED widget/integration owners
+1 -3
View File
@@ -1,4 +1,4 @@
FROM node:24.14.1-alpine AS base
FROM node:24.16.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -43,8 +43,6 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder /app/apps/tasks/tasks.cjs ./apps/tasks/tasks.cjs
COPY --from=builder /app/apps/websocket/wssServer.cjs ./apps/websocket/wssServer.cjs
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
COPY --from=builder /app/packages/db/migrations ./db/migrations
-13
View File
@@ -1,13 +0,0 @@
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,
];
+11 -5
View File
@@ -6,7 +6,6 @@ import "@homarr/core/infrastructure/logs/env";
import "@homarr/docker/env";
import type { NextConfig } from "next";
import MillionLint from "@million/lint";
import createNextIntlPlugin from "next-intl/plugin";
// Package path does not work... so we need to use relative path
@@ -32,6 +31,8 @@ const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: ["@mantine/core", "@mantine/hooks", "@tabler/icons-react"],
turbopackFileSystemCacheForDev: true,
preloadEntriesOnStart: false,
webpackMemoryOptimizations: true,
},
transpilePackages: ["@homarr/ui", "@homarr/notifications", "@homarr/modals", "@homarr/spotlight", "@homarr/widgets"],
images: {
@@ -42,9 +43,18 @@ const nextConfig: NextConfig = {
},
],
},
// skipcq: JS-0116
// eslint-disable-next-line @typescript-eslint/require-await,no-restricted-syntax
async headers() {
return [
{
source: "/sw.js",
headers: [
{ key: "Content-Type", value: "application/javascript; charset=utf-8" },
{ key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
{ key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" },
],
},
{
source: "/(.*)", // Apply CSP to all routes
headers: [
@@ -74,8 +84,4 @@ const nextConfig: NextConfig = {
},
};
// Skip transform is used because of webpack loader, without it for example 'Tooltip.Floating' will not work and show an error
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const withMillionLint = MillionLint.next({ rsc: true, skipTransform: true, telemetry: false });
export default withNextIntl(nextConfig);
+4 -8
View File
@@ -7,13 +7,12 @@
"build": "pnpm with-env next build",
"clean": "git clean -xdf .next .turbo node_modules",
"dev": "pnpm with-env next dev",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt .",
"lint": "oxlint",
"start": "pnpm with-env next start",
"typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@dnd-kit/core": "catalog:",
"@dnd-kit/modifiers": "catalog:",
@@ -44,9 +43,11 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/settings": "workspace:^0.1.0",
"@homarr/spotlight": "workspace:^0.1.0",
"@homarr/tasks": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@homarr/websocket": "workspace:^0.1.0",
"@homarr/widgets": "workspace:^0.1.0",
"@mantine/colors-generator": "catalog:",
"@mantine/core": "catalog:",
@@ -54,7 +55,6 @@
"@mantine/hooks": "catalog:",
"@mantine/modals": "catalog:",
"@mantine/tiptap": "catalog:",
"@million/lint": "catalog:",
"@tabler/icons-react": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
@@ -91,8 +91,6 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "catalog:",
"@types/node": "catalog:",
@@ -101,9 +99,7 @@
"@types/react-dom": "catalog:",
"@types/swagger-ui-react": "catalog:",
"concurrently": "catalog:",
"eslint": "catalog:",
"node-loader": "catalog:",
"prettier": "catalog:",
"typescript": "catalog:"
}
}
+7
View File
@@ -0,0 +1,7 @@
self.addEventListener("install", () => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
@@ -2,29 +2,27 @@
import type { PropsWithChildren } from "react";
import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core";
import { createTheme, DirectionProvider, MantineProvider } from "@mantine/core";
import { DirectionProvider, MantineProvider, v8CssVariablesResolver } from "@mantine/core";
import dayjs from "dayjs";
import { clientApi } from "@homarr/api/client";
import { useSession } from "@homarr/auth/client";
import { parseCookies, setClientCookie } from "@homarr/common";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemeCookieKey } from "@homarr/definitions";
import { theme } from "@homarr/ui";
export const CustomMantineProvider = ({
children,
defaultColorScheme,
}: PropsWithChildren<{ defaultColorScheme: ColorScheme }>) => {
}: PropsWithChildren<{ defaultColorScheme: MantineColorScheme }>) => {
const manager = useColorSchemeManager();
return (
<DirectionProvider>
<MantineProvider
defaultColorScheme={defaultColorScheme}
colorSchemeManager={manager}
theme={createTheme({
primaryColor: "red",
autoContrast: true,
})}
theme={theme}
cssVariablesResolver={v8CssVariablesResolver}
>
{children}
</MantineProvider>
@@ -35,7 +33,7 @@ export const CustomMantineProvider = ({
export function useColorSchemeManager(): MantineColorSchemeManager {
const { data: session } = useSession();
const updateCookieValue = (value: Exclude<MantineColorScheme, "auto">) => {
const updateCookieValue = (value: MantineColorScheme) => {
setClientCookie(colorSchemeCookieKey, value, { expires: dayjs().add(1, "year").toDate(), path: "/" });
};
@@ -60,7 +58,6 @@ export function useColorSchemeManager(): MantineColorSchemeManager {
},
set: (value) => {
if (value === "auto") return;
try {
if (session) {
mutateColorScheme({ colorScheme: value });
@@ -59,7 +59,7 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) {
{t("subtitle")}
</Text>
</Stack>
<Card withBorder w={64 * 6} maw="90vw">
<Card w={64 * 6} maw="90vw">
<RegistrationForm invite={invite} />
</Card>
<Text size="xs" c="gray.5" ta="center">
@@ -9,7 +9,7 @@ import { z } from "zod/v4";
import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import type { useForm } from "@homarr/form";
import type { UseFormReturnType } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
@@ -162,7 +162,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
interface SubmitButtonProps {
isPending: boolean;
form: ReturnType<typeof useForm<FormType, (values: FormType) => FormType>>;
form: UseFormReturnType<FormType>;
provider: "credentials" | "ldap";
}
@@ -198,7 +198,7 @@ const PasswordForgottenCollapse = ({ username }: PasswordForgottenCollapseProps)
{tForgotPassword("label")}
</Anchor>
<Collapse in={visible}>
<Collapse expanded={visible}>
<Card>
<Stack gap="xs">
<Text size="sm">{tForgotPassword("description")}</Text>
@@ -37,7 +37,7 @@ export default async function Login(props: LoginProps) {
{t("subtitle")}
</Text>
</Stack>
<Card withBorder w={64 * 6} maw="90vw">
<Card w={64 * 6} maw="90vw">
<LoginForm
providers={env.AUTH_PROVIDERS}
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
@@ -1,70 +1,27 @@
"use client";
import { useCallback, useRef } from "react";
import { Box, LoadingOverlay, Stack } from "@mantine/core";
import { Box, Stack } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context";
import { BoardCategorySection } from "~/components/board/sections/category-section";
import { BoardEmptySection } from "~/components/board/sections/empty-section";
import { BoardBackgroundVideo } from "~/components/layout/background";
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { useIsBoardReady } from "./_ready-context";
let boardName: string | null = null;
export const updateBoardName = (name: string | null) => {
boardName = name;
};
type UpdateCallback = (prev: RouterOutputs["board"]["getHomeBoard"]) => RouterOutputs["board"]["getHomeBoard"];
export const useUpdateBoard = () => {
const utils = clientApi.useUtils();
const updateBoard = useCallback(
(updaterWithoutUndefined: UpdateCallback) => {
if (!boardName) {
throw new Error("Board name is not set");
}
utils.board.getBoardByName.setData({ name: boardName }, (previous) =>
previous ? updaterWithoutUndefined(previous) : previous,
);
},
[utils],
);
return {
updateBoard,
};
};
export const ClientBoard = () => {
const board = useRequiredBoard();
const currentLayoutId = useCurrentLayout();
const isReady = useIsBoardReady();
const fullWidthSortedSections = board.sections
.filter((section) => section.kind === "empty" || section.kind === "category")
.sort((sectionA, sectionB) => sectionA.yOffset - sectionB.yOffset);
const ref = useRef<HTMLDivElement>(null);
return (
<Box h="100%" pos="relative">
<BoardBackgroundVideo />
<LoadingOverlay
visible={!isReady}
transitionProps={{ duration: 500 }}
loaderProps={{ size: "lg" }}
h={fullHeightWithoutHeaderAndFooter}
/>
<Stack ref={ref} h="100%" style={{ visibility: isReady ? "visible" : "hidden" }}>
<Stack h="100%">
{fullWidthSortedSections.map((section) =>
section.kind === "empty" ? (
// Unique keys per layout to always reinitialize the gridstack
<BoardEmptySection key={`${currentLayoutId}-${section.id}`} section={section} />
) : (
<BoardCategorySection key={`${currentLayoutId}-${section.id}`} section={section} />
@@ -41,13 +41,11 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
}),
// eslint-disable-next-line no-restricted-syntax
page: async ({ params }: { params: Promise<TParams> }) => {
const session = await auth();
const integrations = await getIntegrationsWithPermissionsAsync(session);
const board = await getInitialBoard(await params);
const resolvedParams = await params;
const queryClient = getQueryClient();
// Prefetch item data
const [board, session] = await Promise.all([getInitialBoard(resolvedParams), auth()]);
const itemsMap = board.items.reduce((acc, item) => {
const existing = acc.get(item.kind);
if (existing) {
@@ -58,17 +56,20 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
return acc;
}, new Map<WidgetKind, Item[]>());
for (const [kind, items] of itemsMap) {
await prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(
new ErrorWithMetadata(
"Failed to prefetch widget",
{ widgetKind: kind, itemCount: items.length },
{ cause: error },
),
);
});
}
const [integrations] = await Promise.all([
getIntegrationsWithPermissionsAsync(session),
...Array.from(itemsMap).map(([kind, items]) =>
prefetchForKindAsync(kind, queryClient, items).catch((error) => {
logger.error(
new ErrorWithMetadata(
"Failed to prefetch widget",
{ widgetKind: kind, itemCount: items.length },
{ cause: error },
),
);
}),
),
]);
return (
<HydrationBoundary state={dehydrate(queryClient)}>
@@ -109,7 +109,7 @@ const AddMenu = () => {
}, [openAppSelectModal, createItem]);
return (
<Menu position="bottom-end" withArrow>
<Menu position="bottom-end">
<Menu.Target>
<HeaderButton w="auto" px={4}>
<Group gap={4} wrap="nowrap">
@@ -183,7 +183,7 @@ const SelectBoardsMenu = () => {
const { data: boards = [] } = clientApi.board.getAllBoards.useQuery();
return (
<Menu position="bottom-end" withArrow>
<Menu position="bottom-end">
<Menu.Target>
<HeaderButton w="auto" px={4}>
<IconReplace stroke={1.5} />
@@ -1,42 +1,19 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { createContext, useCallback, useContext } from "react";
const BoardReadyContext = createContext<{
isReady: boolean;
markAsReady: (id: string) => void;
} | null>(null);
export const BoardReadyProvider = ({ children }: PropsWithChildren) => {
const pathname = usePathname();
const utils = clientApi.useUtils();
const board = useRequiredBoard();
const [readySections, setReadySections] = useState<string[]>([]);
// Reset sections required for ready state
useEffect(() => {
return () => {
setReadySections([]);
};
}, [pathname, utils]);
useEffect(() => {
setReadySections((previous) => previous.filter((id) => board.sections.some((section) => section.id === id)));
}, [board.sections.length, setReadySections]);
const markAsReady = useCallback((id: string) => {
setReadySections((previous) => (previous.includes(id) ? previous : [...previous, id]));
}, []);
// eslint-disable-next-line @typescript-eslint/no-empty-function
const markAsReady = useCallback((_id: string) => {}, []);
return (
<BoardReadyContext.Provider
value={{
isReady: board.sections.length === readySections.length,
markAsReady,
}}
>
@@ -54,13 +31,3 @@ export const useMarkSectionAsReady = () => {
return context.markAsReady;
};
export const useIsBoardReady = () => {
const context = useContext(BoardReadyContext);
if (!context) {
throw new Error("BoardReadyProvider is required");
}
return context.isReady;
};
@@ -2,7 +2,7 @@
import type { PropsWithChildren } from "react";
import type { MantineColorsTuple } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem } from "@mantine/core";
import { colorsTuple, createTheme, darken, lighten, MantineProvider, rem, v8CssVariablesResolver } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { ColorScheme } from "@homarr/definitions";
@@ -24,6 +24,7 @@ export const BoardMantineProvider = ({
},
primaryColor: "primaryColor",
autoContrast: true,
defaultRadius: "sm",
fontSizes: {
"2xl": rem(24),
"3xl": rem(28),
@@ -32,7 +33,12 @@ export const BoardMantineProvider = ({
});
return (
<MantineProvider defaultColorScheme={defaultColorScheme} theme={theme} colorSchemeManager={colorSchemeManager}>
<MantineProvider
defaultColorScheme={defaultColorScheme}
theme={theme}
colorSchemeManager={colorSchemeManager}
cssVariablesResolver={v8CssVariablesResolver}
>
{children}
</MantineProvider>
);
@@ -83,7 +83,7 @@ export const ColorSettingsContent = ({ board }: Props) => {
<Anchor onClick={toggle}>{showPreview ? t("common.preview.hide") : t("common.preview.show")}</Anchor>
</Grid.Col>
<Grid.Col span={12}>
<Collapse in={showPreview}>
<Collapse expanded={showPreview}>
<Stack>
<ColorsPreview previewColor={form.values.primaryColor} />
<ColorsPreview previewColor={form.values.secondaryColor} />
@@ -15,7 +15,7 @@ export const InitFinish = async () => {
const tFinish = await getScopedI18n("init.step.finish");
return (
<Card w={64 * 6} maw="90vw" withBorder>
<Card w={64 * 6} maw="90vw">
<Stack>
<Text>{tFinish("description")}</Text>
@@ -34,7 +34,7 @@ export const InitGroup = () => {
};
return (
<Card w={64 * 6} maw="90vw" withBorder>
<Card w={64 * 6} maw="90vw">
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
<Stack>
<TextInput
@@ -13,7 +13,7 @@ interface FileInfoCardProps {
export const FileInfoCard = ({ file, onRemove }: FileInfoCardProps) => {
const tFileInfo = useScopedI18n("init.step.import.fileInfo");
return (
<Card w={64 * 12 + 8} maw="90vw">
<Card w={64 * 12 + 8} maw="90vw" withBorder={false}>
<Group justify="space-between" align="center" wrap="nowrap">
<Group>
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
@@ -20,7 +20,7 @@ export const InitImport = () => {
if (!file) {
return (
<Card w={64 * 12 + 8} maw="90vw" withBorder>
<Card w={64 * 12 + 8} maw="90vw">
<ImportDropZone
loading={isPending}
updateFile={(file) => {
@@ -30,7 +30,7 @@ export const InitSettings = () => {
return (
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
<Stack>
<Card w={64 * 12 + 8} maw="90vw" withBorder>
<Card w={64 * 12 + 8} maw="90vw">
<Stack gap="sm">
<Text fw={500}>{tSection("analytics.title")}</Text>
@@ -39,7 +39,7 @@ export const InitSettings = () => {
</Stack>
</Stack>
</Card>
<Card w={64 * 12 + 8} maw="90vw" withBorder>
<Card w={64 * 12 + 8} maw="90vw">
<Stack gap="sm">
<Text fw={500}>{tSection("crawlingAndIndexing.title")}</Text>
@@ -10,7 +10,7 @@ export const InitStart = async () => {
const tStart = await getScopedI18n("init.step.start");
return (
<Card w={64 * 6} maw="90vw" withBorder>
<Card w={64 * 6} maw="90vw">
<Stack>
<Text>{tStart("description")}</Text>
@@ -1,15 +1,15 @@
"use client";
import { Button, PasswordInput, Stack, TextInput } from "@mantine/core";
import { Button, Stack, TextInput } from "@mantine/core";
import type { z } from "zod/v4";
import { clientApi } from "@homarr/api/client";
import { signIn } from "@homarr/auth/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput } from "@homarr/ui";
import { userInitSchema } from "@homarr/validation/user";
export const InitUserForm = () => {
@@ -59,12 +59,10 @@ export const InitUserForm = () => {
>
<Stack gap="lg">
<TextInput label={t("field.username.label")} {...form.getInputProps("username")} />
<CustomPasswordInput
withPasswordRequirements
label={t("field.password.label")}
{...form.getInputProps("password")}
<UserCreatePasswordFields
passwordInputProps={form.getInputProps("password")}
confirmPasswordInputProps={form.getInputProps("confirmPassword")}
/>
<PasswordInput label={t("field.passwordConfirm.label")} {...form.getInputProps("confirmPassword")} />
<Button type="submit" fullWidth loading={isPending}>
{t("action.create")}
</Button>
@@ -4,7 +4,7 @@ import { InitUserForm } from "./init-user-form";
export const InitUser = () => {
return (
<Card w={64 * 6} maw="90vw" withBorder>
<Card w={64 * 6} maw="90vw">
<InitUserForm />
</Card>
);
+8 -5
View File
@@ -4,6 +4,7 @@ import { Inter } from "next/font/google";
import "@homarr/notifications/styles.css";
import "@homarr/spotlight/styles.css";
import "@homarr/ui/styles.css";
import "~/styles/color-scheme.scss";
import "~/styles/scroll-area.scss";
import { notFound } from "next/navigation";
@@ -25,6 +26,7 @@ import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { Analytics } from "~/components/layout/analytics";
import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation";
import { SearchEngineOptimization } from "~/components/layout/search-engine-optimization";
import { ServiceWorkerRegistration } from "~/components/layout/service-worker-registration";
import { getCurrentColorSchemeAsync } from "~/theme/color-scheme";
import { DayJsLoader } from "./_client-providers/dayjs-loader";
import { JotaiProvider } from "./_client-providers/jotai";
@@ -42,13 +44,13 @@ const fontSans = Inter({
export const generateMetadata = async (): Promise<Metadata> => ({
title: "Homarr",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
"A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.",
openGraph: {
title: "Homarr Dashboard",
description:
"Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.",
"A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.",
url: "https://homarr.dev",
siteName: "Homarr Documentation",
siteName: "Homarr",
},
icons: {
icon: "/logo/logo.png",
@@ -129,7 +131,7 @@ export default async function Layout(props: {
dir={direction}
data-mantine-color-scheme={colorScheme}
style={{
backgroundColor: colorScheme === "dark" ? "#242424" : "#fff",
backgroundColor: colorScheme === "dark" ? "#242424" : colorScheme === "auto" ? undefined : "#fff",
}}
suppressHydrationWarning
>
@@ -140,7 +142,8 @@ export default async function Layout(props: {
</head>
<body className={["font-sans", fontSans.variable].join(" ")}>
<StackedProvider>
<Notifications />
<Notifications pauseResetOnHover="notification" />
<ServiceWorkerRegistration />
{props.children}
</StackedProvider>
</body>
@@ -45,7 +45,6 @@ export const HeroBanner = async () => {
<Image
key={`grid-column-${columnIndex}-scroll-1-${index}`}
src={icon}
radius="md"
fit={"contain"}
w={50}
h={50}
@@ -57,7 +56,6 @@ export const HeroBanner = async () => {
<Image
key={`grid-column-${columnIndex}-scroll-2-${index}`}
src={icon}
radius="md"
fit={"contain"}
w={50}
h={50}
@@ -25,7 +25,14 @@ import {
Title,
UnstyledButton,
} from "@mantine/core";
import { IconKeyboard, IconLanguage, IconLibrary, IconPackage, IconUsers } from "@tabler/icons-react";
import {
IconKeyboard,
IconLanguage,
IconLibrary,
IconMoneybagHeart,
IconPackage,
IconUsers,
} from "@tabler/icons-react";
import { capitalize, objectEntries } from "@homarr/common";
import { hotkeys } from "@homarr/definitions";
@@ -37,6 +44,7 @@ import { createMetaTitle } from "~/metadata";
import type { PackageJsonDependencies } from "~/versions/package-reader";
import { getPackageVersion } from "~/versions/package-reader";
import type githubContributorsJson from "../../../../../../../static-data/contributors.json";
import type openCollectiveContributorsJson from "../../../../../../../static-data/opencollective-contributors.json";
import type crowdinContributorsJson from "../../../../../../../static-data/translators.json";
import classes from "./about.module.css";
@@ -71,6 +79,10 @@ export default async function AboutPage() {
res.json(),
)) as typeof crowdinContributorsJson;
const openCollectiveContributors = (await fetch(`${baseServerUrl}/api/about/contributors/opencollective`).then(
(res) => res.json(),
)) as typeof openCollectiveContributorsJson;
return (
<div>
<DynamicBreadcrumb />
@@ -78,9 +90,7 @@ export default async function AboutPage() {
<Group py="lg">
<Image src={homarrLogoPath} width={100} height={100} alt="" />
<Stack gap={0}>
<Title order={1} tt="uppercase">
Homarr
</Title>
<Title order={1}>Homarr</Title>
<Title order={2}>{t("version", { version })}</Title>
</Stack>
</Group>
@@ -136,6 +146,30 @@ export default async function AboutPage() {
</Flex>
</AccordionPanel>
</AccordionItem>
<AccordionItem value="sponsors">
<AccordionControl icon={<IconMoneybagHeart size="1rem" />}>
<Stack gap={0}>
<Text>{t("accordion.sponsors.title")}</Text>
<Text size="sm" c="dimmed">
{t("accordion.sponsors.subtitle", {
count: String(openCollectiveContributors.length),
})}
</Text>
</Stack>
</AccordionControl>
<AccordionPanel>
<Flex wrap="wrap" gap="xs">
{openCollectiveContributors.map((sponsor) => (
<GenericContributorLinkCard
key={sponsor.slug}
link={`https://opencollective.com/${sponsor.slug}`}
image={sponsor.imageUrl}
name={sponsor.name}
/>
))}
</Flex>
</AccordionPanel>
</AccordionItem>
<AccordionItem value="libraries">
<AccordionControl icon={<IconLibrary size="1rem" />}>
<Stack gap={0}>
@@ -159,9 +193,9 @@ export default async function AboutPage() {
href={`https://www.npmjs.com/package/${name}`}
target="_blank"
>
<Card withBorder radius="md" p="xs" className={classes.dependencyCard}>
<Card radius="md" p="xs" className={classes.dependencyCard}>
<Group gap="sm" wrap="nowrap">
<ThemeIcon variant="light" size="lg" radius="md">
<ThemeIcon variant="light" size="lg">
<IconPackage size="1.5rem" stroke={1.5} />
</ThemeIcon>
@@ -1,6 +1,6 @@
import { Fragment } from "react";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -11,9 +11,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { AppDeleteButton } from "./_app-delete-button";
@@ -39,34 +38,31 @@ export default async function AppsPage(props: AppsPageProps) {
const { items: apps, totalCount } = await api.app.getPaginated(searchParams);
const t = await getScopedI18n("app");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Title>{t("page.list.title")}</Title>
<Group justify="space-between" align="center">
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} flexExpand />
{session.user.permissions.includes("app-create") && (
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</MobileAffixButton>
)}
</Group>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
<Stack gap="sm">
{apps.map((app) => (
<AppCard key={app.id} app={app} />
))}
</Stack>
)}
const canCreate = session.user.permissions.includes("app-create");
{/* Added margin to not hide pagination behind affix-button */}
<Group justify="end" mb={48}>
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</ManageContainer>
return (
<ManagePageLayout
title={t("page.list.title")}
primaryAction={
canCreate ? (
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</MobileAffixButton>
) : undefined
}
toolbar={<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} flexExpand />}
footer={<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />}
floatingPrimaryAction={canCreate}
>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
<Stack gap="sm">
{apps.map((app) => (
<AppCard key={app.id} app={app} />
))}
</Stack>
)}
</ManagePageLayout>
);
}
@@ -79,7 +75,7 @@ const AppCard = async ({ app }: AppCardProps) => {
const session = await auth();
return (
<Card withBorder>
<Card>
<Group justify="space-between" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap" style={{ flex: "1" }}>
<Avatar
@@ -1,43 +1,38 @@
"use client";
import { Affix, Button, Menu } from "@mantine/core";
import { Button, Menu } from "@mantine/core";
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
import { useModalAction } from "@homarr/modals";
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
import { useI18n } from "@homarr/translation/client";
import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
export const CreateBoardButton = () => {
const t = useI18n();
const { openModal: openAddModal } = useModalAction(AddBoardModal);
const { openModal: openImportModal } = useModalAction(ImportBoardModal);
const buttonGroupContent = (
<>
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={openAddModal}>
{t("management.page.board.action.new.label")}
</Button>
<Menu position="bottom-end">
<Menu.Target>
<Button px="xs" ms={1}>
<IconChevronDown size="1rem" />
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={openImportModal} leftSection={<IconFileImport size="1rem" />}>
{t("board.action.oldImport.label")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</>
);
return (
<>
<Button.Group visibleFrom="md">{buttonGroupContent}</Button.Group>
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
<Button.Group>{buttonGroupContent}</Button.Group>
</Affix>
</>
<ManageMobilePrimaryAction>
<Button.Group>
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={openAddModal}>
{t("management.page.board.action.new.label")}
</Button>
<Menu position="bottom-end">
<Menu.Target>
<Button px="xs" ms={1}>
<IconChevronDown size="1rem" />
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={openImportModal} leftSection={<IconFileImport size="1rem" />}>
{t("board.action.oldImport.label")}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Button.Group>
</ManageMobilePrimaryAction>
);
};
@@ -9,12 +9,17 @@ import {
Group,
Menu,
MenuTarget,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconDeviceMobile, IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
import {
IconDeviceMobile,
IconDotsVertical,
IconHomeFilled,
IconLayoutDashboard,
IconLock,
IconWorld,
} from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -23,8 +28,8 @@ import { getScopedI18n } from "@homarr/translation/server";
import { Link, UserAvatar } from "@homarr/ui";
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { NoResults } from "~/components/no-results";
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
import { CreateBoardButton } from "./_components/create-board-button";
@@ -35,23 +40,22 @@ export default async function ManageBoardsPage() {
const canCreateBoards = session?.user.permissions.includes("board-create");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Group justify="space-between">
<Title mb="md">{t("title")}</Title>
{canCreateBoards && <CreateBoardButton />}
</Group>
<Grid mb={{ base: "xl", md: 0 }}>
<ManagePageLayout
title={t("title")}
primaryAction={canCreateBoards ? <CreateBoardButton /> : undefined}
floatingPrimaryAction={canCreateBoards}
>
{boards.length === 0 && <NoResults icon={IconLayoutDashboard} title={t("noResults.title")} />}
{boards.length > 0 && (
<Grid>
{boards.map((board) => (
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</Stack>
</ManageContainer>
)}
</ManagePageLayout>
);
}
@@ -66,16 +70,14 @@ const BoardCard = async ({ board }: BoardCardProps) => {
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;
return (
<Card radius="lg" withBorder>
<Card>
<CardSection p="sm" withBorder>
<Group justify="space-between" align="center">
<Group gap="sm">
<Tooltip label={t(`visibility.${visibility}`)}>
<VisibilityIcon size={20} stroke={1.5} />
</Tooltip>
<Text fw="bolder" tt="uppercase">
{board.name}
</Text>
<Text fw={700}>{board.name}</Text>
</Group>
<Group>
@@ -123,6 +125,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
style={{ borderTop: "none", borderBottom: "none", borderRight: "none", borderRadius: 0 }}
variant="default"
size="lg"
aria-label={t("action.settings.label")}
>
<IconDotsVertical size={16} stroke={1.5} />
</ActionIcon>
@@ -33,7 +33,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
const KindIcon = integrationSecretIcons[secret.kind];
return (
<Card withBorder>
<Card>
<Stack>
<Group justify="space-between">
<Group>
@@ -58,7 +58,12 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
</Tooltip>
)}
{isPublic ? (
<ActionIcon color="gray" variant="subtle" onClick={togglePublicSecretDisplay}>
<ActionIcon
color="gray"
variant="subtle"
onClick={togglePublicSecretDisplay}
aria-label={t(publicSecretDisplayOpened ? "common.action.hide" : "common.action.show")}
>
<DisplayIcon size={16} stroke={1.5} />
</ActionIcon>
) : null}
@@ -79,7 +84,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => {
</Button>
</Group>
</Group>
<Collapse in={editMode}>{children}</Collapse>
<Collapse expanded={editMode}>{children}</Collapse>
</Stack>
</Card>
);
@@ -12,7 +12,7 @@ interface FormType {
interface SecretKindsSegmentedControlProps<TFormType extends FormType> {
defaultKinds?: IntegrationSecretKind[];
secretKinds: IntegrationSecretKind[][];
form: UseFormReturnType<TFormType, (values: TFormType) => TFormType>;
form: UseFormReturnType<TFormType, TFormType>;
}
export const SecretKindsSegmentedControl = <TFormType extends FormType>({
@@ -18,7 +18,7 @@ export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestCo
const causeArray = useMemo(() => toCauseArray(error.cause), [error.cause]);
return (
<Card withBorder style={{ borderColor: getMantineColor("red", 8) }}>
<Card style={{ borderColor: getMantineColor("red", 8) }}>
<Stack>
<Stack gap="sm">
<Text size="lg" fw={500} c="red.8">
@@ -216,7 +216,7 @@ export const CertificateDetailsCard = ({ certificate }: CertificateDetailsProps)
const tCertificateField = useScopedI18n("certificate.field");
return (
<Card withBorder>
<Card>
<Text fw={500}>{tDetails("title")}</Text>
<Group justify="space-between">
<Text size="sm" c="dimmed">
@@ -226,7 +226,7 @@ const AppForm = ({ form, canCreateApps }: { form: UseFormReturnType<FormType>; c
description={t("integration.field.createApp.description")}
/>
<Collapse in={form.values.hasApp}>
<Collapse expanded={form.values.hasApp}>
<Fieldset legend={t("integration.field.app.sectionTitle")}>
<Stack gap="sm">
{canCreateApps && (
@@ -8,7 +8,7 @@
border: 1px solid transparent;
position: relative;
z-index: 0;
transition: transform 150ms ease;
transition: background-color 150ms ease;
overflow: hidden;
&[data-first="true"] {
@@ -20,11 +20,9 @@
}
&[data-active] {
transform: scale(1.01);
z-index: 1;
background-color: var(--mantine-color-body);
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
box-shadow: var(--mantine-shadow-md);
border-radius: var(--mantine-radius-lg);
}
}
@@ -7,9 +7,7 @@ import {
AccordionPanel,
ActionIcon,
ActionIconGroup,
Affix,
Anchor,
Box,
Button,
Divider,
Group,
@@ -24,9 +22,8 @@ import {
TableThead,
TableTr,
Text,
Title,
} from "@mantine/core";
import { IconChevronDown, IconChevronUp, IconPencil, IconPlugX } from "@tabler/icons-react";
import { IconChevronDown, IconPencil, IconPlugX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -37,8 +34,8 @@ import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { NoResults } from "~/components/no-results";
import { env } from "~/env";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
@@ -66,38 +63,23 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
const canCreateIntegrations = session.user.permissions.includes("integration-create");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
{canCreateIntegrations && (
<>
<Box>
<IntegrationSelectMenu>
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
<MenuTarget>
<Button rightSection={<IconChevronUp size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
</Affix>
</IntegrationSelectMenu>
</Box>
<Box visibleFrom="md">
<IntegrationSelectMenu>
<MenuTarget>
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
</IntegrationSelectMenu>
</Box>
</>
)}
</Group>
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
</Stack>
</ManageContainer>
<ManagePageLayout
title={t("page.list.title")}
primaryAction={
canCreateIntegrations ? (
<ManageMobilePrimaryAction>
<IntegrationSelectMenu>
<MenuTarget>
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
</IntegrationSelectMenu>
</ManageMobilePrimaryAction>
) : undefined
}
floatingPrimaryAction={canCreateIntegrations}
>
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
</ManagePageLayout>
);
}
@@ -4,17 +4,15 @@ import {
Anchor,
Group,
Image,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Title,
Tooltip,
} from "@mantine/core";
import { IconExternalLink } from "@tabler/icons-react";
import { IconExternalLink, IconPhoto } from "@tabler/icons-react";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
@@ -26,8 +24,9 @@ import { createLocalImageUrl } from "@homarr/icons/local";
import { getI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { NoResults } from "~/components/no-results";
import { CopyMedia } from "./_actions/copy-media";
import { DeleteMedia } from "./_actions/delete-media";
import { IncludeFromAllUsersSwitch } from "./_actions/show-all";
@@ -48,7 +47,7 @@ interface MediaListPageProps {
searchParams: Promise<inferSearchParamsFromSchema<typeof searchParamsSchema>>;
}
export default async function GroupsListPage(props: MediaListPageProps) {
export default async function MediaListPage(props: MediaListPageProps) {
const session = await auth();
if (!session) {
@@ -58,22 +57,31 @@ export default async function GroupsListPage(props: MediaListPageProps) {
const t = await getI18n();
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
const canUpload = session.user.permissions.includes("media-upload");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Title>{t("media.plural")}</Title>
<Group justify="space-between">
<Group>
<SearchInput placeholder={`${t("media.search")}...`} defaultValue={searchParams.search} />
{session.user.permissions.includes("media-view-all") && (
<IncludeFromAllUsersSwitch defaultChecked={searchParams.includeFromAllUsers} />
)}
</Group>
{session.user.permissions.includes("media-upload") && <UploadMediaButton />}
<ManagePageLayout
title={t("media.plural")}
primaryAction={
canUpload ? (
<ManageMobilePrimaryAction>
<UploadMediaButton />
</ManageMobilePrimaryAction>
) : undefined
}
toolbar={
<Group>
<SearchInput placeholder={`${t("media.search")}...`} defaultValue={searchParams.search} />
{session.user.permissions.includes("media-view-all") && (
<IncludeFromAllUsersSwitch defaultChecked={searchParams.includeFromAllUsers} />
)}
</Group>
}
footer={<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />}
floatingPrimaryAction={canUpload}
>
{medias.length === 0 && <NoResults icon={IconPhoto} title={t("media.noResults.title")} />}
{medias.length > 0 && (
<Table striped highlightOnHover>
<TableThead>
<TableTr>
@@ -90,13 +98,8 @@ export default async function GroupsListPage(props: MediaListPageProps) {
))}
</TableTbody>
</Table>
{/* Added margin to not hide pagination behind affix-button */}
<Group justify="end" mb={48}>
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</ManageContainer>
)}
</ManagePageLayout>
);
}
@@ -7,9 +7,7 @@ export default async function NotFound() {
return (
<Center h="100%">
<Stack align="center">
<Title order={1} tt="uppercase">
{t("title")}
</Title>
<Title order={1}>{t("title")}</Title>
<Text>{t("text")}</Text>
</Stack>
</Center>
+5 -5
View File
@@ -29,20 +29,20 @@ export default async function ManagementPage() {
<Space h="md" />
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
{statistics.map((statistic) => (
<Card component={Link} href={statistic.path} key={statistic.path} radius="lg">
<Card component={Link} href={statistic.path} key={statistic.path}>
<Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap">
<Text size="2.4rem" fw="bolder">
<Text size="xl" fw={700}>
{statistic.count}
</Text>
<Stack gap={0}>
<Text c="red" size="xs">
<Text c="dimmed" size="xs">
{t(`statisticLabel.${statistic.subtitleKey}`)}
</Text>
<Text fw="bold">{t(`statistic.${statistic.titleKey}`)}</Text>
<Text fw={600}>{t(`statistic.${statistic.titleKey}`)}</Text>
</Stack>
</Group>
<IconArrowRight />
<IconArrowRight size={16} stroke={1.5} color="var(--mantine-color-dimmed)" />
</Group>
</Card>
))}
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -10,9 +10,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { SearchEngineDeleteButton } from "./_search-engine-delete-button";
@@ -38,34 +37,31 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
const tEngine = await getScopedI18n("search.engine");
return (
<ManageContainer>
<DynamicBreadcrumb />
<Stack>
<Title>{tEngine("page.list.title")}</Title>
<Group justify="space-between" align="center">
<SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} flexExpand />
{session.user.permissions.includes("search-engine-create") && (
<MobileAffixButton component={Link} href="/manage/search-engines/new">
{tEngine("page.create.title")}
</MobileAffixButton>
)}
</Group>
{searchEngines.length === 0 && <SearchEngineNoResults />}
{searchEngines.length > 0 && (
<Stack gap="sm">
{searchEngines.map((searchEngine) => (
<SearchEngineCard key={searchEngine.id} searchEngine={searchEngine} />
))}
</Stack>
)}
const canCreate = session.user.permissions.includes("search-engine-create");
{/* Added margin to not hide pagination behind affix-button */}
<Group justify="end" mb={48}>
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</ManageContainer>
return (
<ManagePageLayout
title={tEngine("page.list.title")}
primaryAction={
canCreate ? (
<MobileAffixButton component={Link} href="/manage/search-engines/new">
{tEngine("page.create.title")}
</MobileAffixButton>
) : undefined
}
toolbar={<SearchInput placeholder={`${tEngine("search")}...`} defaultValue={searchParams.search} flexExpand />}
footer={<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />}
floatingPrimaryAction={canCreate}
>
{searchEngines.length === 0 && <SearchEngineNoResults />}
{searchEngines.length > 0 && (
<Stack gap="sm">
{searchEngines.map((searchEngine) => (
<SearchEngineCard key={searchEngine.id} searchEngine={searchEngine} />
))}
</Stack>
)}
</ManagePageLayout>
);
}
@@ -78,7 +74,7 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => {
const session = await auth();
return (
<Card withBorder>
<Card>
<Group justify="space-between" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap" style={{ flex: 1 }}>
<Avatar
@@ -41,8 +41,8 @@ export const AnalyticsSettings = ({ initialData }: AnalyticsSettingsProps) => {
<>
<Title order={2}>{t("title")}</Title>
<Card pos="relative" withBorder>
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<Card pos="relative">
<LoadingOverlay visible={isPending} />
<Stack>
<SwitchSetting form={form} formKey="enableGeneral" title={t("general.title")} text={t("general.text")} />
</Stack>
@@ -1,7 +1,7 @@
"use client";
import { Group, Text } from "@mantine/core";
import { IconMoon, IconSun } from "@tabler/icons-react";
import { IconDeviceDesktop, IconMoon, IconSun } from "@tabler/icons-react";
import type { ColorScheme } from "@homarr/definitions";
import { colorSchemes } from "@homarr/definitions";
@@ -34,6 +34,7 @@ export const AppearanceSettingsForm = ({ defaultValues }: { defaultValues: Serve
};
const appearanceIcons = {
auto: IconDeviceDesktop,
light: IconSun,
dark: IconMoon,
};
@@ -41,11 +41,11 @@ export const CrawlingAndIndexingSettings = ({ initialData }: CrawlingAndIndexing
<>
<Title order={2}>{t("title")}</Title>
<Card pos="relative" withBorder>
<Card pos="relative">
<Text c={"dimmed"} mb={"lg"}>
{t("warning")}
</Text>
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<LoadingOverlay visible={isPending} />
<Stack>
<SwitchSetting form={form} formKey="noIndex" title={t("noIndex.title")} text={t("noIndex.text")} />
<SwitchSetting form={form} formKey="noFollow" title={t("noFollow.title")} text={t("noFollow.text")} />
@@ -15,7 +15,7 @@ export const SwitchSetting = <TFormValue extends Record<string, boolean>>({
disabled,
}: {
form: Modify<
UseFormReturnType<TFormValue, () => TFormValue>,
UseFormReturnType<TFormValue, TFormValue>,
{
setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void;
}
@@ -2,7 +2,7 @@ import { headers } from "next/headers";
import { notFound } from "next/navigation";
import { Stack, Tabs, TabsList, TabsPanel, TabsTab } from "@mantine/core";
import { openApiDocument } from "@homarr/api";
import { openApiDocument } from "@homarr/api/open-api";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { extractBaseUrlFromHeaders } from "@homarr/common";
@@ -79,7 +79,7 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
{x509Certificates.map((cert) => (
<Card key={cert.fileName} withBorder>
<Card key={cert.fileName}>
<Group wrap="nowrap">
{cert.isError ? (
<IconAlertTriangle
@@ -58,6 +58,18 @@ const createColumns = (
return <ContainerStateBadge state={cell.row.original.state} />;
},
},
{
accessorKey: "host",
header: t("docker.field.host.label"),
size: 140,
Cell({ row }) {
return (
<Text truncate="end" title={row.original.host}>
{row.original.host}
</Text>
);
},
},
{
accessorKey: "image",
header: t("docker.field.containerImage.label"),
@@ -95,6 +107,23 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
refetchOnReconnect: false,
});
const relativeTime = useTimeAgo(data.timestamp);
const utils = clientApi.useUtils();
const { mutate, isPending } = clientApi.docker.invalidate.useMutation({
async onSuccess() {
await utils.docker.getContainers.invalidate();
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
const table = useTranslatedMantineReactTable({
data: data.containers,
enableDensityToggle: false,
@@ -113,35 +142,11 @@ export function DockerTable(initialData: RouterOutputs["docker"]["getContainers"
},
initialState: { density: "xs", showGlobalFilter: true },
renderTopToolbarCustomActions: () => {
const utils = clientApi.useUtils();
const { mutate, isPending } = clientApi.docker.invalidate.useMutation({
async onSuccess() {
await utils.docker.getContainers.invalidate();
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
});
},
onError() {
showErrorNotification({
title: tDocker("action.refresh.notification.error.title"),
message: tDocker("action.refresh.notification.error.message"),
});
},
});
return (
<Button
variant="default"
rightSection={<IconRefresh size="1rem" />}
onClick={() => mutate()}
loading={isPending}
>
{tDocker("action.refresh.label")}
</Button>
);
},
renderTopToolbarCustomActions: () => (
<Button variant="default" rightSection={<IconRefresh size="1rem" />} onClick={() => mutate()} loading={isPending}>
{tDocker("action.refresh.label")}
</Button>
),
renderToolbarAlertBannerContent: ({ groupedAlert, table }) => {
const dockerContainers = table.getSelectedRowModel().rows.map((row) => row.original);
return (
@@ -190,7 +195,7 @@ const ContainerActionBar = ({ selectedContainers }: ContainerActionBarProps) =>
<ContainerActionBarButton icon={IconPlayerStop} color="red" action="stop" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconRotateClockwise} color="orange" action="restart" selectedIds={selectedIds} />
<ContainerActionBarButton icon={IconTrash} color="red" action="remove" selectedIds={selectedIds} />
<Button leftSection={<IconCategoryPlus />} color={"red"} onClick={handleClick} variant="light" radius="md">
<Button leftSection={<IconCategoryPlus />} color={"red"} onClick={handleClick} variant="light">
{t("addToHomarr.label")}
</Button>
</Group>
@@ -241,7 +246,6 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => {
onClick={handleClickAsync}
loading={isPending}
variant="light"
radius="md"
>
{t(`${props.action}.label`)}
</Button>
@@ -4,28 +4,16 @@
overflow: hidden;
padding: var(--mantine-spacing-xs);
padding-left: calc(var(--mantine-spacing-xs) * 2);
@mixin hover {
box-shadow: var(--mantine-shadow-md);
}
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 6px;
background-image: linear-gradient(0, var(--mantine-color-blue-4), var(--mantine-color-blue-9));
}
border-left: 1px solid var(--mantine-color-blue-6);
background-color: light-dark(
var(--mantine-color-blue-0),
color-mix(in oklch, var(--mantine-color-blue-9), transparent 85%)
);
@mixin rtl {
padding-left: var(--mantine-spacing-xs);
padding-right: calc(var(--mantine-spacing-xs) * 2);
&::before {
left: unset;
right: 0;
}
border-left: none;
border-right: 1px solid var(--mantine-color-blue-6);
}
}
@@ -23,7 +23,6 @@ export function HeaderCard(props: HeaderCardProps) {
<Flex align="center" justify={isRTL ? "space-between" : "flex-start"} gap="md" direction={"row"}>
<ThemeIcon
size="xl"
radius="md"
variant="gradient"
gradient={{
deg: 0,
@@ -8,6 +8,8 @@ import { useCurrentLocale, useI18n } from "@homarr/translation/client";
import { ResourceIcon } from "~/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-gauge/resource-icon";
import classes from "./resource-gauge.module.css";
const KUBERNETES_BRAND_COLOR = "#326ce5";
interface KubernetesResourceGaugeProps {
kubernetesCapacity: KubernetesCapacity;
}
@@ -18,8 +20,8 @@ export function ResourceGauge(props: KubernetesResourceGaugeProps) {
const isRTL = Boolean(isLocaleRTL(currentLocale));
return (
<Paper radius="md" withBorder className={classes.paper} mt={20}>
<ThemeIcon className={classes.icon} size={60} radius={60} bg={"#326ce5"}>
<Paper className={classes.paper} mt={20}>
<ThemeIcon className={classes.icon} size={60} radius={60} bg={KUBERNETES_BRAND_COLOR}>
<ResourceIcon type={props.kubernetesCapacity.type} />
</ThemeIcon>
@@ -17,12 +17,7 @@ interface ResourceTileProps {
export function ResourceTile(props: ResourceTileProps) {
const t = useI18n();
return (
<Card
withBorder
component={Link}
href={`/manage/tools/kubernetes/${props.label}`}
className={classes.cardContainer}
>
<Card component={Link} href={`/manage/tools/kubernetes/${props.label}`} className={classes.cardContainer}>
<Group justify="space-between" wrap="nowrap">
<Image src={`/images/kubernetes/${props.label}.svg`} alt={props.label} width={64} height={64} />
<Group gap="xs">
@@ -2,12 +2,14 @@
import { Select } from "@mantine/core";
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants";
import type { LogLevel } from "@homarr/core/infrastructure/logs/constants";
import { useI18n } from "@homarr/translation/client";
import { useLogContext } from "./log-context";
const levelByValue = Object.fromEntries(logLevels.map((logLevel) => [logLevel, logLevel])) as Record<string, LogLevel>;
export const LogLevelSelection = () => {
const { level, setLevel } = useLogContext();
const t = useI18n();
@@ -19,7 +21,12 @@ export const LogLevelSelection = () => {
label: `${logLevelConfiguration[level].prefix} ${t(`log.level.option.${level}`)}`,
}))}
value={level}
onChange={(value) => setLevel(value as LogLevel)}
onChange={(value) => {
const nextLevel = value && levelByValue[value];
if (nextLevel) {
setLevel(nextLevel);
}
}}
checkIconPosition="right"
/>
);
@@ -1,85 +0,0 @@
"use client";
import { Button, Group, Stack } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeHomeBoardsSchema } from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
interface ChangeHomeBoardFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
}
export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeHomeBoards.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
homeBoardId: variables.homeBoardId,
mobileHomeBoardId: variables.mobileHomeBoardId,
});
showSuccessNotification({
message: t("user.action.changeHomeBoard.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeHomeBoard.notification.error.message"),
});
},
});
const form = useZodForm(userChangeHomeBoardsSchema, {
initialValues: {
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.general")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={t("management.page.user.setting.general.item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
{...form.getInputProps("mobileHomeBoardId")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeHomeBoardsSchema>;
@@ -1,78 +0,0 @@
"use client";
import { Button, Group, Select, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userChangeSearchPreferencesSchema } from "@homarr/validation/user";
interface ChangeSearchPreferencesFormProps {
user: RouterOutputs["user"]["getById"];
searchEnginesData: { value: string; label: string }[];
}
export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
defaultSearchEngineId: variables.defaultSearchEngineId,
openInNewTab: variables.openInNewTab,
});
showSuccessNotification({
message: t("user.action.changeSearchPreferences.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeSearchPreferences.notification.error.message"),
});
},
});
const form = useZodForm(userChangeSearchPreferencesSchema, {
initialValues: {
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
},
});
const handleSubmit = (values: FormType) => {
mutate({
userId: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userChangeSearchPreferencesSchema>;
@@ -1,83 +0,0 @@
"use client";
import { Button, Group, Radio, Stack } from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userFirstDayOfWeekSchema } from "@homarr/validation/user";
dayjs.extend(localeData);
interface FirstDayOfWeekProps {
user: RouterOutputs["user"]["getById"];
}
const weekDays = dayjs.weekdays(false);
export const FirstDayOfWeek = ({ user }: FirstDayOfWeekProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changeFirstDayOfWeek.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
firstDayOfWeek: variables.firstDayOfWeek,
});
showSuccessNotification({
message: t("user.action.changeFirstDayOfWeek.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changeFirstDayOfWeek.notification.error.message"),
});
},
});
const form = useZodForm(userFirstDayOfWeekSchema, {
initialValues: {
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
const inputProps = form.getInputProps("firstDayOfWeek");
const onChange = inputProps.onChange as (value: number) => void;
const value = (inputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Radio.Group {...inputProps} value={value} onChange={(value) => onChange(parseInt(value))}>
<Group mt="xs">
<Radio value="1" label={weekDays[1]} />
<Radio value="6" label={weekDays[6]} />
<Radio value="0" label={weekDays[0]} />
</Group>
</Radio.Group>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userFirstDayOfWeekSchema>;
@@ -0,0 +1,332 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import {
Button,
Card,
Divider,
Group,
Radio,
Select,
SimpleGrid,
Stack,
Switch,
Text,
TextInput,
Title,
} from "@mantine/core";
import type { DayOfWeek } from "@mantine/dates";
import dayjs from "dayjs";
import localeData from "dayjs/plugin/localeData";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { env } from "@homarr/common/env";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import {
userChangeHomeBoardsSchema,
userChangeSearchPreferencesSchema,
userEditProfileSchema,
userFirstDayOfWeekSchema,
userPingIconsEnabledSchema,
} from "@homarr/validation/user";
import type { Board } from "~/app/[locale]/boards/_types";
import { BoardSelect } from "~/components/board/board-select";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
dayjs.extend(localeData);
const userGeneralSettingsSchema = z.object({
name: userEditProfileSchema.shape.name,
email: userEditProfileSchema.shape.email,
homeBoardId: userChangeHomeBoardsSchema.shape.homeBoardId,
mobileHomeBoardId: userChangeHomeBoardsSchema.shape.mobileHomeBoardId,
defaultSearchEngineId: userChangeSearchPreferencesSchema.shape.defaultSearchEngineId,
openInNewTab: userChangeSearchPreferencesSchema.shape.openInNewTab,
ddgBangsEnabled: userChangeSearchPreferencesSchema.shape.ddgBangsEnabled,
firstDayOfWeek: userFirstDayOfWeekSchema.shape.firstDayOfWeek,
pingIconsEnabled: userPingIconsEnabledSchema.shape.pingIconsEnabled,
});
type FormValues = z.infer<typeof userGeneralSettingsSchema>;
const FIRST_DAY_OPTIONS: { value: DayOfWeek; labelKey: number }[] = [
{ value: 1, labelKey: 1 },
{ value: 6, labelKey: 6 },
{ value: 0, labelKey: 0 },
];
interface UserGeneralSettingsFormProps {
user: RouterOutputs["user"]["getById"];
boardsData: Pick<Board, "id" | "name" | "logoImageUrl">[];
searchEnginesData: { value: string; label: string }[];
showLanguageSelector?: boolean;
}
const buildInitialValues = (user: RouterOutputs["user"]["getById"]): FormValues => ({
name: user.name ?? "",
email: user.email ?? "",
homeBoardId: user.homeBoardId,
mobileHomeBoardId: user.mobileHomeBoardId,
defaultSearchEngineId: user.defaultSearchEngineId,
openInNewTab: user.openSearchInNewTab,
ddgBangsEnabled: user.ddgBangs,
firstDayOfWeek: user.firstDayOfWeek as DayOfWeek,
pingIconsEnabled: user.pingIconsEnabled,
});
export const UserGeneralSettingsForm = ({
user,
boardsData,
searchEnginesData,
showLanguageSelector = false,
}: UserGeneralSettingsFormProps) => {
const t = useI18n();
const tGeneral = useScopedI18n("management.page.user.setting.general");
const isCredentialsUser = user.provider === "credentials";
const editProfileMutation = clientApi.user.editProfile.useMutation();
const changeHomeBoardsMutation = clientApi.user.changeHomeBoards.useMutation();
const changeSearchPreferencesMutation = clientApi.user.changeSearchPreferences.useMutation();
const changeFirstDayOfWeekMutation = clientApi.user.changeFirstDayOfWeek.useMutation();
const changePingIconsEnabledMutation = clientApi.user.changePingIconsEnabled.useMutation();
const initialValues = buildInitialValues(user);
const initialValuesRef = useRef(initialValues);
const form = useZodForm(userGeneralSettingsSchema, {
initialValues,
});
const isDirtyRef = useRef(false);
isDirtyRef.current = form.isDirty();
const weekDays = useMemo(() => dayjs.weekdays(false), []);
const mutations = [
editProfileMutation,
changeHomeBoardsMutation,
changeSearchPreferencesMutation,
changeFirstDayOfWeekMutation,
changePingIconsEnabledMutation,
];
const isPending = mutations.some((m) => m.isPending);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (env.NODE_ENV === "development") return;
if (isDirtyRef.current) e.preventDefault();
};
window.addEventListener("beforeunload", handler);
return () => window.removeEventListener("beforeunload", handler);
}, []);
const handleSubmitAsync = async (values: FormValues) => {
const parsed = userGeneralSettingsSchema.safeParse(values);
if (!parsed.success) return;
const defaults = initialValuesRef.current;
const changed = <K extends keyof FormValues>(...fields: K[]) =>
fields.some((f) => (f === "email" ? (values[f] || null) !== (defaults[f] || null) : values[f] !== defaults[f]));
const saveActions: { when: boolean; action: () => Promise<unknown> }[] = [
{
when: isCredentialsUser && changed("name", "email"),
action: () =>
editProfileMutation.mutateAsync({ id: user.id, name: parsed.data.name, email: parsed.data.email ?? "" }),
},
{
when: changed("homeBoardId", "mobileHomeBoardId"),
action: () =>
changeHomeBoardsMutation.mutateAsync({
userId: user.id,
homeBoardId: parsed.data.homeBoardId,
mobileHomeBoardId: parsed.data.mobileHomeBoardId,
}),
},
{
when: changed("defaultSearchEngineId", "openInNewTab", "ddgBangsEnabled"),
action: () =>
changeSearchPreferencesMutation.mutateAsync({
userId: user.id,
defaultSearchEngineId: parsed.data.defaultSearchEngineId,
openInNewTab: parsed.data.openInNewTab,
ddgBangsEnabled: parsed.data.ddgBangsEnabled,
}),
},
{
when: changed("firstDayOfWeek"),
action: () =>
changeFirstDayOfWeekMutation.mutateAsync({ id: user.id, firstDayOfWeek: parsed.data.firstDayOfWeek }),
},
{
when: changed("pingIconsEnabled"),
action: () =>
changePingIconsEnabledMutation.mutateAsync({ id: user.id, pingIconsEnabled: parsed.data.pingIconsEnabled }),
},
];
const promises = saveActions.filter((s) => s.when).map((s) => s.action());
if (promises.length === 0) return;
try {
await Promise.all(promises);
const newValues = { ...parsed.data, email: parsed.data.email ?? "" };
initialValuesRef.current = newValues;
form.setInitialValues(newValues);
form.resetDirty();
await revalidatePathActionAsync(`/manage/users/${user.id}`);
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("common.notification.update.success"),
});
} catch {
showErrorNotification({
title: t("common.notification.update.error"),
message: t("common.notification.update.error"),
});
}
};
const handleDiscard = () => {
form.setValues(initialValuesRef.current);
form.resetDirty();
};
const firstDayOfWeekInputProps = form.getInputProps("firstDayOfWeek");
const firstDayOfWeekOnChange = firstDayOfWeekInputProps.onChange as (value: number) => void;
const firstDayOfWeekValue = (firstDayOfWeekInputProps.value as number).toString();
return (
<form onSubmit={form.onSubmit(handleSubmitAsync)}>
<Stack gap="lg">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg" verticalSpacing="lg">
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{t("user.name")}</Title>
{!isCredentialsUser && (
<Text c="dimmed" size="sm">
{t("management.page.user.fieldsDisabledExternalProvider")}
</Text>
)}
</Stack>
<Divider />
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isCredentialsUser}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.board.title")}</Title>
</Stack>
<Divider />
<BoardSelect
label={tGeneral("item.board.type.general")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("homeBoardId")}
/>
<BoardSelect
label={tGeneral("item.board.type.mobile")}
clearable
boards={boardsData}
w="100%"
withinPortal
{...form.getInputProps("mobileHomeBoardId")}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.search")}</Title>
</Stack>
<Divider />
<Select
label={t("user.field.defaultSearchEngine.label")}
w="100%"
data={searchEnginesData}
comboboxProps={{ withinPortal: true }}
{...form.getInputProps("defaultSearchEngineId")}
/>
<Switch
label={t("user.field.openSearchInNewTab.label")}
{...form.getInputProps("openInNewTab", { type: "checkbox" })}
/>
<Switch
label={t("user.field.ddgBangs.label")}
{...form.getInputProps("ddgBangsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
<Card withBorder bg="transparent">
<Stack gap="md">
<Stack gap={2}>
<Title order={3}>{tGeneral("item.language")}</Title>
</Stack>
<Divider />
{showLanguageSelector && <CurrentLanguageCombobox withinPortal />}
<Title order={4}>{tGeneral("item.firstDayOfWeek")}</Title>
<Radio.Group
{...firstDayOfWeekInputProps}
value={firstDayOfWeekValue}
onChange={(nextValue: string) => firstDayOfWeekOnChange(parseInt(nextValue, 10))}
>
<Group mt="xs" wrap="wrap">
{FIRST_DAY_OPTIONS.map(({ value: dayValue, labelKey }) => (
<Radio key={dayValue} value={dayValue.toString()} label={weekDays[labelKey]} />
))}
</Group>
</Radio.Group>
<Divider my="xs" />
<Title order={4}>{tGeneral("item.accessibility")}</Title>
<Switch
label={t("user.field.pingIconsEnabled.label")}
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
/>
</Stack>
</Card>
</SimpleGrid>
<div style={{ position: "sticky", bottom: 20 }}>
{form.isDirty() && (
<Card withBorder>
<Group justify="space-between" wrap="wrap">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>
<Button disabled={isPending} variant="default" onClick={handleDiscard}>
{t("common.action.discard")}
</Button>
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
{t("common.action.saveChanges")}
</Button>
</Group>
</Group>
</Card>
)}
</div>
</Stack>
</form>
);
};
@@ -1,69 +0,0 @@
"use client";
import { Button, Group, Stack, Switch } from "@mantine/core";
import type { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userPingIconsEnabledSchema } from "@homarr/validation/user";
interface PingIconsEnabledProps {
user: RouterOutputs["user"]["getById"];
}
export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.changePingIconsEnabled.useMutation({
async onSettled() {
await revalidatePathActionAsync(`/manage/users/${user.id}`);
},
onSuccess(_, variables) {
form.setInitialValues({
pingIconsEnabled: variables.pingIconsEnabled,
});
showSuccessNotification({
message: t("user.action.changePingIconsEnabled.notification.success.message"),
});
},
onError() {
showErrorNotification({
message: t("user.action.changePingIconsEnabled.notification.error.message"),
});
},
});
const form = useZodForm(userPingIconsEnabledSchema, {
initialValues: {
pingIconsEnabled: user.pingIconsEnabled,
},
});
const handleSubmit = (values: FormType) => {
mutate({
id: user.id,
...values,
});
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack gap="md">
<Switch
{...form.getInputProps("pingIconsEnabled", { type: "checkbox" })}
label={t("user.field.pingIconsEnabled.label")}
/>
<Group justify="end">
<Button type="submit" loading={isPending}>
{t("common.action.save")}
</Button>
</Group>
</Stack>
</form>
);
};
type FormType = z.infer<typeof userPingIconsEnabledSchema>;
@@ -2,7 +2,7 @@
import { useCallback } from "react";
import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -25,6 +25,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
},
});
const [opened, { toggle }] = useDisclosure(false);
const isMobile = useMediaQuery("(max-width: 48em)");
const { openConfirmModal } = useConfirmModal();
const t = useI18n();
const tManageAvatar = useScopedI18n("user.action.manageAvatar");
@@ -94,11 +95,11 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => {
}, [mutate, user.id, openConfirmModal, tManageAvatar]);
return (
<Box pos="relative">
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow>
<Box pos="relative" display="flex" style={{ justifyContent: isMobile ? "center" : undefined }}>
<Menu opened={opened} keepMounted onChange={toggle} position="bottom-start" withArrow withinPortal>
<Menu.Target>
<UnstyledButton onClick={toggle}>
<UserAvatar user={user} size={200} />
<UserAvatar user={user} size={isMobile ? 120 : 200} />
<Button
component="div"
pos="absolute"
@@ -1,94 +0,0 @@
"use client";
import { useCallback } from "react";
import { Button, Group, Stack, TextInput } from "@mantine/core";
import type { RouterInputs, RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { userEditProfileSchema } from "@homarr/validation/user";
interface UserProfileFormProps {
user: RouterOutputs["user"]["getById"];
}
export const UserProfileForm = ({ user }: UserProfileFormProps) => {
const t = useI18n();
const { mutate, isPending } = clientApi.user.editProfile.useMutation({
async onSettled() {
await revalidatePathActionAsync("/manage/users");
},
onSuccess(_, variables) {
// Reset form initial values to reset dirty state
form.setInitialValues({
name: variables.name,
email: variables.email ?? "",
});
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("user.action.editProfile.notification.success.message"),
});
},
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("user.error.usernameTaken")
: t("user.action.editProfile.notification.error.message");
showErrorNotification({
title: t("common.notification.update.error"),
message,
});
},
});
const form = useZodForm(userEditProfileSchema.omit({ id: true }), {
initialValues: {
name: user.name ?? "",
email: user.email ?? "",
},
});
// Only credentials users can edit their profile
const isProviderCredentials = user.provider === "credentials";
const handleSubmit = useCallback(
(values: FormType) => {
if (!isProviderCredentials) return;
mutate({
...values,
id: user.id,
});
},
[isProviderCredentials, mutate, user.id],
);
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.username.label")}
withAsterisk
{...form.getInputProps("name")}
/>
<TextInput
disabled={!isProviderCredentials}
label={t("user.field.email.label")}
{...form.getInputProps("email")}
/>
{isProviderCredentials && (
<Group justify="end">
<Button type="submit" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>
)}
</Stack>
</form>
);
};
type FormType = Omit<RouterInputs["user"]["editProfile"], "id">;
@@ -6,18 +6,13 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { ChangeHomeBoardForm } from "./_components/_change-home-board";
import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { FirstDayOfWeek } from "./_components/_first-day-of-week";
import { PingIconsEnabled } from "./_components/_ping-icons-enabled";
import { UserGeneralSettingsForm } from "./_components/_general-settings-form";
import { UserProfileAvatarForm } from "./_components/_profile-avatar-form";
import { UserProfileForm } from "./_components/_profile-form";
interface Props {
params: Promise<{
@@ -63,7 +58,7 @@ export default async function EditUserPage(props: Props) {
const boards = await api.board.getAllBoards();
const searchEngines = await api.searchEngine.getSelectable();
const isSelf = session?.user.id === user.id;
const isCredentialsUser = user.provider === "credentials";
return (
@@ -73,51 +68,25 @@ export default async function EditUserPage(props: Props) {
{t("management.page.user.fieldsDisabledExternalProvider")}
</Alert>
)}
<Title>{tGeneral("title")}</Title>
<Group gap="xl">
<Box flex={1}>
<UserProfileForm user={user} />
<Group gap="xl" align="flex-start" wrap="wrap">
<Box flex={1} miw={{ base: "100%", md: 540 }}>
<UserGeneralSettingsForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
searchEnginesData={searchEngines}
showLanguageSelector={isSelf}
/>
</Box>
<Box w={{ base: "100%", lg: 200 }}>
<Box w={{ base: "100%", lg: 260 }}>
<UserProfileAvatarForm user={user} />
</Box>
</Group>
{session?.user.id === user.id && (
<Stack mb="lg">
<Title order={2}>{tGeneral("item.language")}</Title>
<CurrentLanguageCombobox />
</Stack>
)}
<Stack mb="lg">
<Title order={2}>{tGeneral("item.board.title")}</Title>
<ChangeHomeBoardForm
user={user}
boardsData={boards.map((board) => ({
id: board.id,
name: board.name,
logoImageUrl: board.logoImageUrl,
}))}
/>
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.search")}</Title>
<ChangeSearchPreferencesForm user={user} searchEnginesData={searchEngines} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.firstDayOfWeek")}</Title>
<FirstDayOfWeek user={user} />
</Stack>
<Stack mb="lg">
<Title order={2}>{tGeneral("item.accessibility")}</Title>
<PingIconsEnabled user={user} />
</Stack>
<DangerZoneRoot>
<DangerZoneItem
label={t("user.action.delete.label")}
@@ -1,24 +1,22 @@
"use client";
import { useMemo } from "react";
import { Anchor, Button, Group, Text, Title, Tooltip } from "@mantine/core";
import { Anchor, Group, Text, Tooltip } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { useI18n } from "@homarr/translation/client";
import { Link, UserAvatar } from "@homarr/ui";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
interface UserListComponentProps {
initialUserList: RouterOutputs["user"]["getAll"];
credentialsProviderEnabled: boolean;
}
export const UserListComponent = ({ initialUserList, credentialsProviderEnabled }: UserListComponentProps) => {
const tUserList = useScopedI18n("management.page.user.list");
export const UserListComponent = ({ initialUserList }: UserListComponentProps) => {
const t = useI18n();
const { data, isLoading } = clientApi.user.getAll.useQuery(undefined, {
initialData: initialUserList,
@@ -69,21 +67,10 @@ export const UserListComponent = ({ initialUserList, credentialsProviderEnabled
enableFullScreenToggle: false,
layoutMode: "grid-no-grow",
getRowId: (row) => row.id,
renderTopToolbarCustomActions: () =>
credentialsProviderEnabled ? (
<Button component={Link} href="/manage/users/create">
{t("management.page.user.create.title")}
</Button>
) : null,
state: {
isLoading,
},
});
return (
<>
<Title mb="md">{tUserList("title")}</Title>
<MantineReactTable table={table} />
</>
);
return <MantineReactTable table={table} />;
};
@@ -1,20 +1,7 @@
"use client";
import { startTransition, useCallback, useState } from "react";
import {
Badge,
Button,
Card,
Group,
PasswordInput,
Stack,
Stepper,
Table,
Text,
TextInput,
Title,
Tooltip,
} from "@mantine/core";
import { Badge, Button, Card, Group, Stack, Stepper, Table, Text, TextInput, Title, Tooltip } from "@mantine/core";
import { useListState } from "@mantine/hooks";
import { IconPlus, IconUserCheck } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -24,10 +11,11 @@ import type { GroupPermissionKey } from "@homarr/definitions";
import { everyoneGroup, groupPermissions } from "@homarr/definitions";
import type { IsValid } from "@homarr/form";
import { useZodForm } from "@homarr/form";
import { UserCreatePasswordFields } from "@homarr/forms-collection";
import { useModalAction } from "@homarr/modals";
import { showErrorNotification } from "@homarr/notifications";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import { CustomPasswordInput, UserAvatar } from "@homarr/ui";
import { UserAvatar } from "@homarr/ui";
import { createCustomErrorParams } from "@homarr/validation/form/i18n";
import { userPasswordSchema } from "@homarr/validation/user";
@@ -149,7 +137,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
color={!generalForm.isValid() ? "red" : undefined}
>
<form>
<Card p="xl" shadow="md" withBorder>
<Card p="xl">
<Stack gap="md">
<TextInput
label={tUserField("username.label")}
@@ -165,27 +153,19 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
</Stepper.Step>
<Stepper.Step label={t("step.security.label")} allowStepSelect={false} allowStepClick={false}>
<form>
<Card p="xl" shadow="md" withBorder>
<Card p="xl">
<Stack gap="md">
<CustomPasswordInput
withPasswordRequirements
label={tUserField("password.label")}
<UserCreatePasswordFields
variant="filled"
withAsterisk
{...securityForm.getInputProps("password")}
/>
<PasswordInput
label={tUserField("passwordConfirm.label")}
variant="filled"
withAsterisk
{...securityForm.getInputProps("confirmPassword")}
passwordInputProps={securityForm.getInputProps("password")}
confirmPasswordInputProps={securityForm.getInputProps("confirmPassword")}
/>
</Stack>
</Card>
</form>
</Stepper.Step>
<Stepper.Step label={t("step.groups.label")} allowStepSelect={false} allowStepClick={false}>
<Card p="xl" shadow="md" withBorder>
<Card p="xl">
<GroupsForm
initialGroups={initialGroups}
addGroup={(groupId) =>
@@ -198,20 +178,20 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC
</Card>
</Stepper.Step>
<Stepper.Step label={t("step.review.label")} allowStepSelect={false} allowStepClick={false}>
<Card p="xl" shadow="md" withBorder>
<Card p="xl">
<Stack maw={300} align="center" mx="auto">
<UserAvatar
size="xl"
user={{ name: generalForm.values.username, email: generalForm.values.email ?? null, image: null }}
/>
<Text tt="uppercase" fw="bolder" size="xl">
<Text fw={700} size="xl">
{generalForm.values.username}
</Text>
</Stack>
</Card>
</Stepper.Step>
<Stepper.Completed>
<Card p="xl" shadow="md" withBorder>
<Card p="xl">
<Stack align="center" maw={300} mx="auto">
<IconUserCheck size="3rem" />
<Title order={2}>{t("step.completed.title")}</Title>
@@ -25,7 +25,7 @@ export const StepperNavigationComponent = ({
}: StepperNavigationComponentProps) => {
const t = useI18n();
return (
<Card shadow="md" withBorder>
<Card>
{!isComplete ? (
<Group justify="space-between" wrap="nowrap">
<Button
@@ -101,7 +101,7 @@ export const SaveAffix = ({ groupId }: SaveAffixProps) => {
<div style={{ position: "sticky", bottom: 20 }}>
<Transition transition="slide-up" mounted={form.isDirty()}>
{(transitionStyles) => (
<Card style={transitionStyles} withBorder>
<Card style={transitionStyles}>
<Group justify="space-between">
<Text fw={500}>{tForm("unsavedChanges")}</Text>
<Group>
@@ -68,7 +68,6 @@ const PermissionCard = async ({ group, isDanger }: PermissionCardProps) => {
return (
<Card
p="md"
withBorder
style={{
borderColor: isDanger ? "var(--mantine-color-red-8)" : undefined,
}}
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo, useState } from "react";
import { Group, TextInput } from "@mantine/core";
import { useMemo, useState } from "react";
import { TextInput } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -33,29 +33,20 @@ export const GroupsList = ({ groups }: GroupsListProps) => {
return (
<>
<Group justify="space-between">
<TextInput
leftSection={<IconSearch size={20} stroke={1.5} />}
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
placeholder={`${t("group.search")}...`}
style={{ flex: 1 }}
/>
<AddGroup />
</Group>
<TextInput
leftSection={<IconSearch size={20} stroke={1.5} />}
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
placeholder={`${t("group.search")}...`}
/>
<GroupsTable groups={filteredGroups} initialGroupIds={initialGroupIds} hasFilter={search.length !== 0} />
</>
);
};
const AddGroup = () => {
export const AddGroupButton = () => {
const t = useI18n();
const { openModal } = useModalAction(AddGroupModal);
const handleAddGroup = useCallback(() => {
openModal();
}, [openModal]);
return <MobileAffixButton onClick={handleAddGroup}>{t("group.action.create.label")}</MobileAffixButton>;
return <MobileAffixButton onClick={() => openModal()}>{t("group.action.create.label")}</MobileAffixButton>;
};
@@ -259,7 +259,7 @@ const SaveAffix = ({ visible, isPending, onDiscard, onSave }: SaveAffixProps) =>
<div style={{ position: "sticky", bottom: 20 }}>
<Transition transition="slide-up" mounted={visible}>
{(transitionStyles) => (
<Card style={transitionStyles} withBorder>
<Card style={transitionStyles}>
<Group justify="space-between">
<Text fw={500}>{t("common.unsavedChanges")}</Text>
<Group>
@@ -1,5 +1,5 @@
import { notFound } from "next/navigation";
import { Card, Group, Stack, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core";
import { Card, Group, Stack, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
import { IconChevronRight, IconUsersGroup } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
@@ -8,9 +8,8 @@ import { everyoneGroup } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { GroupsList } from "./_client";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { AddGroupButton, GroupsList } from "./_client";
import classes from "./groups.module.css";
export default async function GroupsListPage() {
@@ -26,11 +25,8 @@ export default async function GroupsListPage() {
const groupsWithoutEveryone = groups.filter((group) => group.name !== everyoneGroup);
return (
<ManageContainer size="xl">
<DynamicBreadcrumb />
<ManagePageLayout title={t("group.title")} primaryAction={<AddGroupButton />} floatingPrimaryAction>
<Stack>
<Title>{t("group.title")}</Title>
{dbEveryoneGroup && (
<UnstyledButton component={Link} href={`/manage/users/groups/${dbEveryoneGroup.id}`}>
<Card className={classes.everyoneGroup}>
@@ -53,6 +49,6 @@ export default async function GroupsListPage() {
<GroupsList groups={groupsWithoutEveryone} />
</Stack>
</ManageContainer>
</ManagePageLayout>
);
}
@@ -0,0 +1,20 @@
"use client";
import { Button } from "@mantine/core";
import { useModalAction } from "@homarr/modals";
import { InviteCreateModal } from "@homarr/modals-collection";
import { useScopedI18n } from "@homarr/translation/client";
import { MANAGE_ACTION_BUTTON_MIN_WIDTH } from "~/components/manage/manage-page.constants";
export const InviteCreateButton = () => {
const t = useScopedI18n("management.page.user.invite");
const { openModal } = useModalAction(InviteCreateModal);
return (
<Button miw={MANAGE_ACTION_BUTTON_MIN_WIDTH} onClick={() => openModal()}>
{t("action.new.title")}
</Button>
);
};
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo } from "react";
import { ActionIcon, Button, Title } from "@mantine/core";
import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
@@ -10,8 +10,7 @@ import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useConfirmModal, useModalAction } from "@homarr/modals";
import { InviteCreateModal } from "@homarr/modals-collection";
import { useConfirmModal } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
@@ -65,7 +64,6 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps
enableFullScreenToggle: false,
layoutMode: "grid-no-grow",
getRowId: (row) => row.id,
renderTopToolbarCustomActions: RenderTopToolbarCustomActions,
state: {
isLoading,
},
@@ -74,22 +72,7 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps
},
});
return (
<>
<Title mb="md">{t("title")}</Title>
<MantineReactTable table={table} />
</>
);
};
const RenderTopToolbarCustomActions = () => {
const t = useScopedI18n("management.page.user.invite");
const { openModal } = useModalAction(InviteCreateModal);
const handleNewInvite = useCallback(() => {
openModal();
}, [openModal]);
return <Button onClick={handleNewInvite}>{t("action.new.title")}</Button>;
return <MantineReactTable table={table} />;
};
const RenderRowActions = ({ row }: { row: MRT_Row<RouterOutputs["invite"]["getAll"][number]> }) => {
@@ -3,8 +3,11 @@ import { notFound } from "next/navigation";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { getScopedI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { InviteCreateButton } from "./_components/invite-create-button";
import { InviteListComponent } from "./_components/invite-list";
export default async function InvitesOverviewPage() {
@@ -17,11 +20,20 @@ export default async function InvitesOverviewPage() {
return notFound();
}
const t = await getScopedI18n("management.page.user.invite");
const initialInvites = await api.invite.getAll();
return (
<>
<DynamicBreadcrumb />
<ManagePageLayout
title={t("title")}
primaryAction={
<ManageMobilePrimaryAction>
<InviteCreateButton />
</ManageMobilePrimaryAction>
}
floatingPrimaryAction
>
<InviteListComponent initialInvites={initialInvites} />
</>
</ManagePageLayout>
);
}
@@ -4,8 +4,10 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { getScopedI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { createMetaTitle } from "~/metadata";
import { UserListComponent } from "./_components/user-list";
@@ -27,13 +29,23 @@ export default async function UsersPage() {
return notFound();
}
const t = await getScopedI18n("management.page.user");
const userList = await api.user.getAll();
const credentialsProviderEnabled = isProviderEnabled("credentials");
return (
<>
<DynamicBreadcrumb />
<UserListComponent initialUserList={userList} credentialsProviderEnabled={credentialsProviderEnabled} />
</>
<ManagePageLayout
title={t("list.title")}
primaryAction={
credentialsProviderEnabled ? (
<MobileAffixButton component={Link} href="/manage/users/create">
{t("create.title")}
</MobileAffixButton>
) : undefined
}
floatingPrimaryAction={credentialsProviderEnabled}
>
<UserListComponent initialUserList={userList} />
</ManagePageLayout>
);
}
@@ -99,7 +99,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
return (
<>
<Card withBorder w={dimensions.width} h={dimensions.height} p={dimensions.height >= 96 ? undefined : 4}>
<Card w={dimensions.width} h={dimensions.height} p={dimensions.height >= 96 ? undefined : 4}>
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
@@ -123,17 +123,35 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
</QueryErrorResetBoundary>
</Card>
<Affix bottom={12} right={72}>
<ActionIcon size={48} variant="default" radius="xl" onClick={handleOpenEditWidgetModal}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={handleOpenEditWidgetModal}
aria-label={t("common.action.edit")}
>
<IconPencil size={24} />
</ActionIcon>
</Affix>
<Affix bottom={12} right={72 + 60}>
<ActionIcon size={48} variant="default" radius="xl" onClick={toggleEditMode}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={toggleEditMode}
aria-label={editMode ? t("widgetPreview.toggle.disabled") : t("widgetPreview.toggle.enabled")}
>
{editMode ? <IconToggleLeft size={24} /> : <IconToggleRight size={24} />}
</ActionIcon>
</Affix>
<Affix bottom={12} right={72 + 120}>
<ActionIcon size={48} variant="default" radius="xl" onClick={openDimensionsModal}>
<ActionIcon
size={48}
variant="default"
radius="xl"
onClick={openDimensionsModal}
aria-label={t("widgetPreview.dimensions.title")}
>
<IconDimensions size={24} />
</ActionIcon>
</Affix>
Loaded 100 of 434 files, more files were not shown because too many files have changed in this diff. Show more