Compare commits

...
Author SHA1 Message Date
Thomas f18401c72e Merge branch 'dev' into fix/remove-board-loading-overlay 2026-05-22 08:59:52 +02: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
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
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
Thomas Camlong 40e53976e5 fix(lint): suppress no-empty-function for intentional no-op in BoardReadyProvider 2026-05-18 17:00:52 +02:00
Thomas Camlong 211fd648be fix: remove unused ready-tracking state from BoardReadyProvider
markAsReady is still called by gridstack sections but now acts as a
no-op since the loading overlay that consumed isReady was removed.
2026-05-18 14:59:47 +02:00
Thomas Camlong 26702fe078 perf(boards): remove loading overlay and show board immediately
The board was hidden behind a LoadingOverlay until every section
reported ready. This delays first paint unnecessarily — the server
already prefetches widget data so the board can render on arrival.

Also removes dead duplicate exports (updateBoardName, useUpdateBoard)
that already live in @homarr/boards/updater.
2026-05-18 14:47:53 +02: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 c83fdb2a6e chore(release): 1.60.1-beta.2 [skip ci] 2026-05-07 16:39:41 +00:00
homarr-releases[bot] 81fdd90e7a chore(release): release v1.60.1-beta 2026-05-07 16:38:37 +00:00
Thomas 5ee84f8cf4 perf: (next branch) reduce idle memory on startup (#5637) 2026-05-06 21:39:26 +00: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
Meier LukasandCopilot bfa8566284 ci: add beta release workflow (#5601)
Co-authored-by: Copilot <copilot@github.com>
2026-05-01 22:18:45 +02: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
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
84 changed files with 6000 additions and 7080 deletions

No files matched your search

+5 -1
View File
@@ -9,4 +9,8 @@ README.md
.git
dev
.build
e2e
e2e
apps/nextjs/.next
.env
.env.*
!.env.example
+8 -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,14 @@ 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.
+1
View File
@@ -33,6 +33,7 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.61.0
- 1.60.1-beta.1
- 1.60.0
- 1.59.3
+24 -5
View File
@@ -15,6 +15,13 @@ concurrency:
env:
FORCE_COLOR: 3
SKIP_ENV_VALIDATION: true
TURBO_API: ${{ secrets.TURBO_API }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
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:
@@ -22,6 +29,8 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup
uses: ./tooling/github/setup
@@ -31,31 +40,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 -- --cache --cache-location node_modules/.cache/.prettiercache
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: pnpm typecheck
run: pnpm turbo typecheck --affected
test:
runs-on: ubuntu-latest
@@ -78,11 +91,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
@@ -90,8 +104,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
@@ -105,10 +122,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
+10 -8
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 }}
@@ -54,7 +55,7 @@ jobs:
with:
private-key: ${{ secrets.RENOVATE_MERGE_PRIVATE_KEY }}
client-id: ${{ secrets.RENOVATE_MERGE_APP_ID }}
permission-contents: write # required to commit package.json & changelog changes, merge them to dev and publish the release
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'
@@ -66,7 +67,7 @@ jobs:
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.15.0
cache: "pnpm"
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
+3857 -3838
View File
File diff suppressed because it is too large. Load diff
-2
View File
@@ -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
+2 -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: {
@@ -83,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);
+2 -1
View File
@@ -44,9 +44,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 +56,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:",
@@ -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} />
@@ -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;
};
+6
View File
@@ -0,0 +1,6 @@
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("@homarr/tasks");
await import("@homarr/websocket");
}
}
+1 -3
View File
@@ -5,12 +5,11 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/index.ts"
".": "./src/main.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:*.node --external:@opentelemetry/api --external:deasync --external:bcrypt --outfile=tasks.cjs",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
@@ -24,7 +23,6 @@
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/cron-jobs-core": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
-40
View File
@@ -1,52 +1,12 @@
// This import has to be the first import in the file so that the agent is overridden before any other modules are imported.
import "./overrides";
import type { FastifyTRPCPluginOptions } from "@trpc/server/adapters/fastify";
import { fastifyTRPCPlugin } from "@trpc/server/adapters/fastify";
import fastify from "fastify";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import type { JobRouter } from "@homarr/cron-job-api";
import { jobRouter } from "@homarr/cron-job-api";
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "@homarr/cron-job-api/constants";
import { jobGroup } from "@homarr/cron-jobs";
import { db } from "@homarr/db";
import { JobManager } from "./job-manager";
import { onStartAsync } from "./on-start";
const logger = createLogger({ module: "tasksMain" });
const server = fastify({
maxParamLength: 5000,
});
server.register(fastifyTRPCPlugin, {
prefix: CRON_JOB_API_PATH,
trpcOptions: {
router: jobRouter,
createContext: ({ req }) => ({
manager: new JobManager(db, jobGroup),
apiKey: req.headers[CRON_JOB_API_KEY_HEADER] as string | undefined,
}),
onError({ path, error }) {
logger.error(new ErrorWithMetadata("Error in tasks tRPC handler", { path }, { cause: error }));
},
} satisfies FastifyTRPCPluginOptions<JobRouter>["trpcOptions"],
});
void (async () => {
await onStartAsync();
await jobGroup.initializeAsync();
await jobGroup.startAllAsync();
try {
await server.listen({ port: CRON_JOB_API_PORT });
logger.info("Tasks web server started successfully", { port: CRON_JOB_API_PORT });
} catch (err) {
logger.error(
new ErrorWithMetadata("Failed to start tasks web server", { port: CRON_JOB_API_PORT }, { cause: err }),
);
process.exit(1);
}
})();
+1 -1
View File
@@ -9,5 +9,5 @@
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
"exclude": ["node_modules", "*.cjs", "*.css"]
}
+3 -1
View File
@@ -4,10 +4,12 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/main.ts"
},
"main": "./src/main.ts",
"types": "./src/main.ts",
"scripts": {
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:deasync --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
"clean": "rm -rf .turbo node_modules",
"dev": "pnpm with-env tsx ./src/main.ts",
"format": "prettier --check . --ignore-path ../../.gitignore",
+1 -1
View File
@@ -9,5 +9,5 @@
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
"exclude": ["node_modules", "*.cjs", "*.css"]
}
+3 -3
View File
@@ -2,12 +2,12 @@
"name": "homarr-prebuilt-debian",
"private": true,
"dependencies": {
"better-sqlite3": "^12.9.0"
"better-sqlite3": "^12.10.0"
},
"packageManager": "pnpm@10.33.2",
"packageManager": "pnpm@10.33.4",
"engines": {
"node": ">=24.15.0",
"pnpm": ">=10.33.2"
"pnpm": ">=10.33.4"
},
"pnpm": {
"onlyBuiltDependencies": [
+6 -6
View File
@@ -9,17 +9,17 @@ importers:
.:
dependencies:
better-sqlite3:
specifier: ^12.9.0
version: 12.9.0
specifier: ^12.10.0
version: 12.10.0
packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
better-sqlite3@12.9.0:
resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
better-sqlite3@12.10.0:
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==}
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@@ -148,7 +148,7 @@ snapshots:
base64-js@1.5.1: {}
better-sqlite3@12.9.0:
better-sqlite3@12.10.0:
dependencies:
bindings: 1.5.0
prebuild-install: 7.1.3
+1 -1
View File
@@ -1,4 +1,4 @@
import { chromium } from "playwright";
import { chromium } from "@playwright/test";
import { GenericContainer } from "testcontainers";
import { describe, expect, test } from "vitest";
+1 -1
View File
@@ -1,4 +1,4 @@
import { chromium } from "playwright";
import { chromium } from "@playwright/test";
import { describe, test } from "vitest";
import { OnboardingActions } from "./shared/actions/onboarding-actions";
+1 -1
View File
@@ -1,5 +1,5 @@
import { createId } from "@paralleldrive/cuid2";
import type { Page } from "playwright";
import type { Page } from "@playwright/test";
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
import type { SqliteDatabase } from "../e2e-db";
@@ -1,5 +1,5 @@
import type { Page } from "@playwright/test";
import { eq } from "drizzle-orm";
import type { Page } from "playwright";
import { expect } from "vitest";
import * as sqliteSchema from "../../../packages/db/schema/sqlite";
+14 -12
View File
@@ -1,6 +1,6 @@
{
"name": "homarr",
"version": "1.61.0",
"version": "1.61.1-beta.1",
"private": true,
"scripts": {
"build": "cross-env CI=true turbo build",
@@ -15,7 +15,7 @@
"db:migration:sqlite:run": "pnpm -F db migration:sqlite:run",
"db:push": "pnpm -F db push:sqlite",
"db:studio": "pnpm -F db studio",
"dev": "turbo dev --parallel",
"dev": "pnpm -F nextjs dev",
"docker:dev": "docker compose -f ./development/development.docker-compose.yml up",
"format": "turbo format --continue -- --cache --cache-location node_modules/.cache/.prettiercache",
"format:fix": "turbo format --continue -- --write --cache --cache-location node_modules/.cache/.prettiercache",
@@ -36,6 +36,7 @@
"prettier": "@homarr/prettier-config",
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@playwright/test": "catalog:",
"@semantic-release/changelog": "catalog:",
"@semantic-release/commit-analyzer": "catalog:",
"@semantic-release/git": "catalog:",
@@ -59,10 +60,10 @@
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
"packageManager": "pnpm@10.33.2",
"packageManager": "pnpm@10.33.4",
"engines": {
"node": ">=24.15.0",
"pnpm": ">=10.33.2"
"pnpm": ">=10.33.4"
},
"pnpm": {
"onlyBuiltDependencies": [
@@ -79,23 +80,23 @@
"overrides": {
"@babel/helpers@<7.26.10": ">=7.29.2",
"@babel/runtime@<7.26.10": ">=7.29.2",
"axios@>=1.0.0 <1.8.2": ">=1.16.0",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.5",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.5",
"axios@>=1.0.0 <1.8.2": ">=1.16.1",
"brace-expansion@>=2.0.0 <=2.0.1": ">=5.0.6",
"brace-expansion@>=1.0.0 <=1.1.11": ">=5.0.6",
"esbuild@<=0.24.2": ">=0.28.0",
"form-data@>=4.0.0 <4.0.4": ">=4.0.5",
"hono@<4.6.5": ">=4.12.16",
"linkifyjs@<4.3.2": ">=4.3.2",
"hono@<4.6.5": ">=4.12.19",
"linkifyjs@<4.3.2": ">=4.3.3",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.11",
"prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.24.6",
"react-is": "^19.2.5",
"rollup@>=4.0.0 <4.22.4": ">=4.60.2",
"react-is": "^19.2.6",
"rollup@>=4.0.0 <4.22.4": ">=4.60.4",
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=8.0.10"
"vite@>=5.0.0 <=5.4.18": ">=8.0.13"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",
@@ -104,6 +105,7 @@
"allowUnusedPatches": true,
"ignoredBuiltDependencies": [
"@scarf/scarf",
"@swc/core",
"core-js-pure",
"protobufjs"
]
-1
View File
@@ -25,7 +25,6 @@
"@homarr/auth": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-job-api": "workspace:^0.1.0",
"@homarr/cron-job-status": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/db": "workspace:^0.1.0",
+19 -16
View File
@@ -2,33 +2,36 @@ import { observable } from "@trpc/server/observable";
import z from "zod/v4";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { jobGroupKeys, jobNameSchema } from "@homarr/cron-job-api";
import { cronJobApi } from "@homarr/cron-job-api/client";
import type { TaskStatus } from "@homarr/cron-job-status";
import { createCronJobStatusChannel } from "@homarr/cron-job-status";
import { jobGroup } from "@homarr/cron-jobs";
import { JobManager } from "@homarr/cron-jobs/job-manager";
import { createTRPCRouter, permissionRequiredProcedure } from "../trpc";
const jobGroupKeys = jobGroup.getKeys();
const jobNameSchema = z.enum(jobGroup.getKeys());
const logger = createLogger({ module: "cronJobsRouter" });
export const cronJobsRouter = createTRPCRouter({
triggerJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.trigger.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).triggerAsync(input);
}),
startJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.start.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).startAsync(input);
}),
stopJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.stop.mutate(input);
.mutation(({ input, ctx }) => {
new JobManager(ctx.db, jobGroup).stop(input);
}),
updateJobInterval: permissionRequiredProcedure
.requiresPermission("admin")
@@ -38,23 +41,23 @@ export const cronJobsRouter = createTRPCRouter({
cron: z.string(),
}),
)
.mutation(async ({ input }) => {
await cronJobApi.updateInterval.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).updateIntervalAsync(input.name, input.cron);
}),
disableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.disable.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).disableAsync(input);
}),
enableJob: permissionRequiredProcedure
.requiresPermission("admin")
.input(jobNameSchema)
.mutation(async ({ input }) => {
await cronJobApi.enable.mutate(input);
.mutation(async ({ input, ctx }) => {
await new JobManager(ctx.db, jobGroup).enableAsync(input);
}),
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async () => {
return await cronJobApi.getAll.query();
getJobs: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
return await new JobManager(ctx.db, jobGroup).getAllAsync();
}),
subscribeToStatusUpdates: permissionRequiredProcedure.requiresPermission("admin").subscription(() => {
return observable<TaskStatus>((emit) => {
+23
View File
@@ -5,3 +5,26 @@ export const capitalize = <T extends string>(str: T) => {
export const isNullOrWhitespace = (value: string | null): value is null => {
return value == null || value.trim() === "";
};
export const bestMatch = <TItem extends Record<string, unknown>>(
search: string,
options: TItem[],
by: (item: TItem) => string,
) => {
if (options.length === 0) return null;
const normalizedSearch = search.toLowerCase().trim();
return options.reduce<TItem | null>((best, current) => {
const currentMatchIndex = by(current).toLowerCase().indexOf(normalizedSearch);
if (currentMatchIndex === -1) return best;
if (best === null) return current;
const bestMatchIndex = by(best).toLowerCase().indexOf(normalizedSearch);
if (currentMatchIndex < bestMatchIndex) return current;
if (by(current).length < by(best).length && currentMatchIndex === bestMatchIndex) return current;
return best;
}, null);
};
+40 -3
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { describe, expect, test } from "vitest";
import { capitalize } from "../string";
import { bestMatch, capitalize } from "../string";
const capitalizeTestCases = [
["hello", "Hello"],
@@ -12,8 +12,45 @@ const capitalizeTestCases = [
describe("capitalize should capitalize the first letter of a string", () => {
capitalizeTestCases.forEach(([input, expected]) => {
it(`should capitalize ${input} to ${expected}`, () => {
test(`should capitalize ${input} to ${expected}`, () => {
expect(capitalize(input)).toEqual(expected);
});
});
});
describe("bestMatch should find the best match in an array of options", () => {
test("should find exact match for 'nginx'", () => {
const options = [
{ name: "my-nginx" },
{ name: "nginx-proxy" },
{ name: "nginx" },
{ name: "redis" },
{ name: "postgres" },
];
const result = bestMatch("nginx", options, ({ name }) => name);
expect(result).toEqual({ name: "nginx" });
});
test("should find closest match for 'nginx' when exact match is not available", () => {
const options = [{ name: "redis" }, { name: "nginx-proxy" }, { name: "my-nginx" }, { name: "postgres" }];
const result = bestMatch("nginx", options, ({ name }) => name);
expect(result).toEqual({ name: "nginx-proxy" });
});
test("should return null if no match is found", () => {
const options = [{ name: "redis" }, { name: "postgres" }];
const result = bestMatch("nginx", options, ({ name }) => name);
expect(result).toBeNull();
});
test("should return null if options array is empty", () => {
const options: { name: string }[] = [];
const result = bestMatch("nginx", options, ({ name }) => name);
expect(result).toBeNull();
});
});
+19 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { getPortFromUrl } from "../url";
import { getPortFromUrl, isAbsoluteUrl } from "../url.js";
describe("getPortFromUrl", () => {
test.each([
@@ -38,3 +38,21 @@ describe("getPortFromUrl", () => {
expect(act).toThrowError("Unsupported protocol: ftp:");
});
});
describe("isAbsoluteUrl", () => {
test.each([
["http://example.com", true],
["https://example.com", true],
["ftp://example.com", true],
["file:///path/to/file", true],
["mailto:johndoe@example.com", true],
["/relative/path", false],
["relative/path", false],
])("should return %s for URL: %s", (urlString, expected) => {
// Act
const result = isAbsoluteUrl(urlString);
// Assert
expect(result).toBe(expected);
});
});
+6
View File
@@ -38,3 +38,9 @@ export const getPortFromUrl = (url: URL): number => {
throw new Error(`Unsupported protocol: ${url.protocol}`);
};
const absoluteUrlRegex = /^[a-z]+:(\/\/)?/;
export const isAbsoluteUrl = (urlOrPath: string): boolean => {
return absoluteUrlRegex.test(urlOrPath.toLowerCase());
};
-9
View File
@@ -1,9 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
/** @type {import('typescript-eslint').Config} */
export default [
{
ignores: [],
},
...baseConfig,
];
-46
View File
@@ -1,46 +0,0 @@
{
"name": "@homarr/cron-job-api",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./src/index.ts",
"./env": "./src/env.ts",
"./constants": "./src/constants.ts",
"./client": "./src/client.ts"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@tanstack/react-query": "catalog:",
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"@trpc/tanstack-react-query": "catalog:",
"react": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/react": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
-20
View File
@@ -1,20 +0,0 @@
import { createTRPCClient, httpLink } from "@trpc/client";
import type { JobRouter } from ".";
import { CRON_JOB_API_KEY_HEADER, CRON_JOB_API_PATH, CRON_JOB_API_PORT } from "./constants";
import { env } from "./env";
export const cronJobApi = createTRPCClient<JobRouter>({
links: [
httpLink({
url: `${getBaseUrl()}${CRON_JOB_API_PATH}`,
headers: {
[CRON_JOB_API_KEY_HEADER]: env.CRON_JOB_API_KEY,
},
}),
],
});
function getBaseUrl() {
return `http://localhost:${CRON_JOB_API_PORT}`;
}
-3
View File
@@ -1,3 +0,0 @@
export const CRON_JOB_API_PORT = 3002;
export const CRON_JOB_API_PATH = "/trpc";
export const CRON_JOB_API_KEY_HEADER = "cron-job-api-key";
-11
View File
@@ -1,11 +0,0 @@
import { z } from "zod/v4";
import { env as commonEnv } from "@homarr/common/env";
import { createEnv } from "@homarr/core/infrastructure/env";
export const env = createEnv({
server: {
CRON_JOB_API_KEY: commonEnv.NODE_ENV === "development" ? z.string().default("test") : z.string(),
},
experimental__runtimeEnv: process.env,
});
-85
View File
@@ -1,85 +0,0 @@
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import type { JobGroupKeys } from "@homarr/cron-jobs";
import { jobGroup } from "@homarr/cron-jobs";
import { env } from "./env";
export const jobGroupKeys = jobGroup.getKeys();
export const jobNameSchema = z.enum(jobGroup.getKeys());
export interface IJobManager {
startAsync(name: JobGroupKeys): Promise<void>;
triggerAsync(name: JobGroupKeys): Promise<void>;
stop(name: JobGroupKeys): void;
updateIntervalAsync(name: JobGroupKeys, cron: string): Promise<void>;
disableAsync(name: JobGroupKeys): Promise<void>;
enableAsync(name: JobGroupKeys): Promise<void>;
getAllAsync(): Promise<
{
name: JobGroupKeys;
cron: string;
preventManualExecution: boolean;
preventCustomInterval: boolean;
isEnabled: boolean;
}[]
>;
}
const t = initTRPC
.context<{
manager: IJobManager;
apiKey?: string;
}>()
.create();
const createTrpcRouter = t.router;
const apiKeyProcedure = t.procedure.use(({ ctx, next }) => {
if (ctx.apiKey !== env.CRON_JOB_API_KEY) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Missing or invalid API key",
});
}
return next({
ctx: {
...ctx,
apiKey: undefined, // Clear the API key after checking
},
});
});
export const jobRouter = createTrpcRouter({
start: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.startAsync(input);
}),
trigger: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.triggerAsync(input);
}),
stop: apiKeyProcedure.input(jobNameSchema).mutation(({ input, ctx }) => {
ctx.manager.stop(input);
}),
updateInterval: apiKeyProcedure
.input(
z.object({
name: jobNameSchema,
cron: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
await ctx.manager.updateIntervalAsync(input.name, input.cron);
}),
disable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.disableAsync(input);
}),
enable: apiKeyProcedure.input(jobNameSchema).mutation(async ({ input, ctx }) => {
await ctx.manager.enableAsync(input);
}),
getAll: apiKeyProcedure.query(({ ctx }) => {
return ctx.manager.getAllAsync();
}),
});
export type JobRouter = typeof jobRouter;
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "@homarr/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
+4 -2
View File
@@ -5,7 +5,8 @@
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts"
".": "./index.ts",
"./job-manager": "./src/job-manager.ts"
},
"typesVersions": {
"*": {
@@ -38,7 +39,8 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"semver-parser": "catalog:"
"semver-parser": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
+40 -29
View File
@@ -28,34 +28,45 @@ import { updateCheckerJob } from "./jobs/update-checker";
import { weatherJob } from "./jobs/weather";
import { createCronJobGroup } from "./lib";
export const jobGroup = createCronJobGroup({
analytics: analyticsJob,
iconsUpdater: iconsUpdaterJob,
ping: pingJob,
smartHomeEntityState: smartHomeEntityStateJob,
mediaServer: mediaServerJob,
mediaOrganizer: mediaOrganizerJob,
downloads: downloadsJob,
dnsHole: dnsHoleJob,
mediaRequestStats: mediaRequestStatsJob,
mediaRequestList: mediaRequestListJob,
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
firewallCpu: firewallCpuJob,
firewallMemory: firewallMemoryJob,
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
timetable: timetableJob,
tracearr: tracearrJob,
umamiActiveVisitors: umamiActiveVisitorsJob,
});
const getJobGroup = () => {
return createCronJobGroup({
analytics: analyticsJob,
iconsUpdater: iconsUpdaterJob,
ping: pingJob,
smartHomeEntityState: smartHomeEntityStateJob,
mediaServer: mediaServerJob,
mediaOrganizer: mediaOrganizerJob,
downloads: downloadsJob,
dnsHole: dnsHoleJob,
mediaRequestStats: mediaRequestStatsJob,
mediaRequestList: mediaRequestListJob,
rssFeeds: rssFeedsJob,
indexerManager: indexerManagerJob,
healthMonitoring: healthMonitoringJob,
updateChecker: updateCheckerJob,
mediaTranscoding: mediaTranscodingJob,
minecraftServerStatus: minecraftServerStatusJob,
dockerContainers: dockerContainersJob,
networkController: networkControllerJob,
firewallCpu: firewallCpuJob,
firewallMemory: firewallMemoryJob,
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
timetable: timetableJob,
tracearr: tracearrJob,
umamiActiveVisitors: umamiActiveVisitorsJob,
});
};
// Add global type augmentation for homarr
declare global {
var cronJobs: ReturnType<typeof getJobGroup> | undefined;
}
global.cronJobs ??= getJobGroup();
export const jobGroup = global.cronJobs;
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
@@ -1,7 +1,6 @@
import { Cron } from "croner";
import { createLogger } from "@homarr/core/infrastructure/logs";
import type { IJobManager } from "@homarr/cron-job-api";
import type { jobGroup as cronJobGroup, JobGroupKeys } from "@homarr/cron-jobs";
import type { Database, InferInsertModel } from "@homarr/db";
import { eq } from "@homarr/db";
@@ -9,7 +8,7 @@ import { cronJobConfigurations } from "@homarr/db/schema";
const logger = createLogger({ module: "jobManager" });
export class JobManager implements IJobManager {
export class JobManager {
constructor(
private db: Database,
private jobGroup: typeof cronJobGroup,
+87 -1
View File
@@ -1,6 +1,7 @@
import { createId, objectKeys } from "@homarr/common";
import {
createDocumentationLink,
credentialsAdminGroup,
everyoneGroup,
getIntegrationDefaultUrl,
getIntegrationName,
@@ -11,12 +12,24 @@ import { defaultServerSettings, defaultServerSettingsKeys } from "@homarr/server
import type { Database } from "..";
import { eq } from "..";
import { getMaxGroupPositionAsync } from "../queries";
import {
getServerSettingByKeyAsync,
insertServerSettingByKeyAsync,
updateServerSettingByKeyAsync,
} from "../queries/server-setting";
import { groups, integrations, onboarding, searchEngines } from "../schema";
import {
boards,
groupMembers,
groupPermissions,
groups,
integrations,
layouts,
onboarding,
searchEngines,
sections,
users,
} from "../schema";
import type { Integration } from "../schema";
export const seedDataAsync = async (db: Database) => {
@@ -25,6 +38,10 @@ export const seedDataAsync = async (db: Database) => {
await seedServerSettingsAsync(db);
await seedDefaultSearchEnginesAsync(db);
await seedDefaultIntegrationsAsync(db);
if (process.env.DEMO_MODE === "true") {
await seedDemoUserAsync(db);
}
};
const seedEveryoneGroupAsync = async (db: Database) => {
@@ -191,3 +208,72 @@ const seedDefaultIntegrationsAsync = async (db: Database) => {
console.log(`Created ${createdCount} default integrations through seeding process`);
};
const seedDemoUserAsync = async (db: Database) => {
const existingUser = await db.query.users.findFirst({
where: eq(users.name, "demo"),
});
if (existingUser) {
console.log("Skipping seeding of demo user as it already exists");
return;
}
const userId = createId();
await db.insert(users).values({
id: userId,
name: "demo",
email: "demo@example.com",
password: "$2b$10$c5AaQb0oj32/bui5Z55PyOb7pY.iEkXYqgdD8MjS9LjSjLMeOkZu2",
});
const maxPosition = await getMaxGroupPositionAsync(db);
const groupId = createId();
await db.insert(groups).values({
id: groupId,
name: credentialsAdminGroup,
ownerId: userId,
position: maxPosition + 1,
});
await db.insert(groupPermissions).values({
groupId,
permission: "admin",
});
await db.insert(groupMembers).values({
groupId,
userId,
});
await db.update(onboarding).set({
step: "finish",
previousStep: "settings",
});
const boardId = createId();
await db.insert(boards).values({
id: boardId,
name: "default",
isPublic: false,
creatorId: userId,
});
await db.insert(sections).values({
id: createId(),
kind: "empty",
xOffset: 0,
yOffset: 0,
boardId,
});
await db.insert(layouts).values({
id: createId(),
name: "Base",
columnCount: 12,
breakpoint: 0,
boardId,
});
await db.update(users).set({ homeBoardId: boardId }).where(eq(users.id, userId));
console.log(
"Your instance has enabled demo mode. Homarr therefore created example data for your demo. You can disable demo mode anytime by setting the env 'DEMO_MODE' to 'false' or unset it.",
);
};
@@ -93,6 +93,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/roles"
| "/docs/tags/search"
| "/docs/tags/search-engines"
| "/docs/tags/secrets"
| "/docs/tags/security"
| "/docs/tags/self-signed"
| "/docs/tags/seo"
@@ -198,6 +199,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/tracearr"
| "/docs/integrations/transmission"
| "/docs/integrations/truenas"
| "/docs/integrations/umami"
| "/docs/integrations/unifi-controller"
| "/docs/integrations/unraid"
| "/docs/management/api"
@@ -244,6 +246,7 @@ export type HomarrDocumentationPath =
| "/docs/widgets/system-resources"
| "/docs/widgets/timetable"
| "/docs/widgets/tracearr"
| "/docs/widgets/umami"
| "/docs/widgets/video"
| "/docs/widgets/weather"
| ""
-1
View File
@@ -376,7 +376,6 @@ export const integrationDefs = {
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/umami.svg",
category: ["analytics"],
defaultUrl: "https://api.umami.is/v1",
// @ts-expect-error - docs page will be created when integration is merged
documentationUrl: createDocumentationLink("/docs/integrations/umami"),
},
// This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page)
@@ -1,3 +1,4 @@
import { isAbsoluteUrl } from "@homarr/common";
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ImageProxy } from "@homarr/image-proxy";
@@ -85,10 +86,10 @@ export class TracearrIntegration extends Integration {
data: await Promise.all(
json.data.map(async (stream) => {
if (stream.userAvatarUrl) {
stream.userAvatarUrl = await this.proxyImageAsync(imageProxy, stream.userAvatarUrl, stream.id, "avatar");
stream.userAvatarUrl = await this.proxyImageAsync(imageProxy, stream.userAvatarUrl);
}
if (stream.posterUrl) {
stream.posterUrl = await this.proxyImageAsync(imageProxy, stream.posterUrl, stream.id, "poster");
stream.posterUrl = await this.proxyImageAsync(imageProxy, stream.posterUrl);
}
return stream;
}),
@@ -120,12 +121,7 @@ export class TracearrIntegration extends Integration {
...json,
data: await Promise.all(
json.data.map(async (violation) => {
violation.user.avatarUrl = await this.proxyImageAsync(
imageProxy,
violation.user.avatarUrl,
violation.user.id,
"avatar",
);
violation.user.avatarUrl = await this.proxyImageAsync(imageProxy, violation.user.avatarUrl);
return violation;
}),
),
@@ -156,12 +152,7 @@ export class TracearrIntegration extends Integration {
...json,
data: await Promise.all(
json.data.map(async (session) => {
session.user.avatarUrl = await this.proxyImageAsync(
imageProxy,
session.user.avatarUrl,
session.user.id,
"avatar",
);
session.user.avatarUrl = await this.proxyImageAsync(imageProxy, session.user.avatarUrl);
return session;
}),
),
@@ -194,23 +185,12 @@ export class TracearrIntegration extends Integration {
};
}
private async proxyImageAsync(
imageProxy: ImageProxy,
url: string | null | undefined,
uniqueId: string,
discriminator: string,
): Promise<string | null> {
if (!url) return null;
private async proxyImageAsync(imageProxy: ImageProxy, urlOrPath: string | null | undefined): Promise<string | null> {
if (!urlOrPath) return null;
try {
// ImageProxy doesn't support fallback urls so we need to remove them
const cleanUrl = url.replace(/&fallback=[^&]+/, "").replace(/\?fallback=[^&]+&?/, "?");
// Build the full URL, then inject _uid as the FIRST query param.
// This is critical because ImageProxy uses bcrypt which truncates at 72 bytes.
// Without this, all long avatar/poster URLs from the same session hash identically
// since they share the same first 72+ bytes.
const baseUrl = this.url(cleanUrl as `/${string}`).toString();
const prefix = `_uid=${discriminator}_${uniqueId}&`;
const fullUrl = baseUrl.includes("?") ? baseUrl.replace("?", `?${prefix}`) : `${baseUrl}?${prefix}`;
const cleanUrl = urlOrPath.replace(/&fallback=[^&]+/, "").replace(/\?fallback=[^&]+&?/, "?");
const fullUrl = isAbsoluteUrl(urlOrPath) ? cleanUrl : this.url(cleanUrl as `/${string}`).toString();
return await imageProxy.createImageAsync(fullUrl, this.getAuthHeaders());
} catch {
+2 -6
View File
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
import type { ContainerInfo, ContainerStats } from "dockerode";
import type Dockerode from "dockerode";
import { bestMatch } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import { db, like, or } from "@homarr/db";
@@ -82,12 +83,7 @@ async function getContainersWithStatsAsync() {
name: container.Names[0]?.split("/")[1] ?? "Unknown",
host: container.instance,
state: container.State as ContainerState,
iconUrl:
dbIcons.find((icon) => {
const extractedImage = extractImage(container);
if (!extractedImage) return false;
return icon.name.toLowerCase().includes(extractedImage.toLowerCase());
})?.url ?? null,
iconUrl: bestMatch(extractImage(container), dbIcons, (icon) => icon.name)?.url ?? null,
cpuUsage,
memoryUsage,
image: container.Image,
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+163 -160
View File
@@ -159,7 +159,7 @@
"label": "在新标签页中打开搜索结果"
},
"ddgBangs": {
"label": ""
"label": "启用 DuckDuckGo bangs"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "DuckDuckGo bangs 切换成功"
},
"error": {
"message": ""
"message": "无法切换 DuckDuckGo bangs"
}
}
},
@@ -1093,7 +1093,7 @@
},
"colorScheme": {
"options": {
"auto": "",
"auto": "自动",
"light": "日间",
"dark": "暗黑"
}
@@ -1540,13 +1540,13 @@
}
},
"timetable": {
"name": "",
"description": "",
"title": "",
"noStation": "",
"name": "时刻表",
"description": "显示车站的出发时间",
"title": "出发时间 {station}",
"noStation": "未选择车站",
"option": {
"station": {
"label": ""
"label": "车站"
}
}
},
@@ -1621,11 +1621,11 @@
}
},
"anchorNote": {
"name": "",
"description": "",
"name": "锚点笔记",
"description": "显示来自锚点的所选笔记",
"option": {
"noteId": {
"label": ""
"label": "笔记"
},
"showTitle": {
"label": "显示标题"
@@ -1637,16 +1637,16 @@
"empty": "选择要显示的笔记",
"emptyContent": "此笔记没有内容",
"notFound": "未找到笔记",
"noNotes": "",
"integrationRequired": "",
"noNotes": "未找到笔记",
"integrationRequired": "请先选择锚点集成",
"updatedAt": "更新时间:{date}",
"untitled": "未命名",
"edit": "编辑",
"save": "保存",
"cancel": "取消",
"readOnlyViewer": "",
"saveForbidden": "",
"saveFailed": ""
"readOnlyViewer": "您对该笔记只有查看权限,编辑已禁用。",
"saveForbidden": "您没有编辑此笔记的权限。",
"saveFailed": "保存此笔记失败,请重试。"
},
"iframe": {
"name": "iFrame",
@@ -1680,7 +1680,7 @@
"label": "允许地理位置"
},
"allowModals": {
"label": ""
"label": "允许弹窗"
}
},
"error": {
@@ -2056,156 +2056,156 @@
}
},
"tracearr": {
"name": "",
"description": "",
"name": "Tracearr",
"description": "增强型媒体服务器监控,提供详细的流与转码数据",
"option": {
"showStreams": {
"label": ""
"label": "显示活跃流"
},
"showStats": {
"label": ""
"label": "显示统计摘要"
},
"showRecentActivity": {
"label": ""
"label": "显示近期活动"
},
"showViolations": {
"label": ""
"label": "显示违规记录"
}
},
"stats": {
"activeStreams": "",
"totalUsers": "",
"transcodes": "",
"bandwidth": ""
"activeStreams": "活跃流",
"totalUsers": "总用户数",
"transcodes": "转码数",
"bandwidth": "带宽"
},
"streams": {
"empty": "",
"user": "",
"media": "",
"state": "",
"quality": "",
"device": "",
"progress": "",
"empty": "无活跃流",
"user": "用户",
"media": "媒体",
"state": "状态",
"quality": "画质",
"device": "设备",
"progress": "进度",
"videoDecision": {
"directPlay": "",
"transcode": "",
"directStream": ""
"directPlay": "直接播放",
"transcode": "转码",
"directStream": "直接串流"
}
},
"recentActivity": {
"title": "",
"empty": "",
"watched": "",
"partial": ""
"title": "近期活动",
"empty": "暂无近期活动",
"watched": "已观看",
"partial": "部分观看"
},
"violations": {
"title": "",
"empty": "",
"rule": "",
"user": ""
"title": "违规记录",
"empty": "暂无近期违规",
"rule": "规则",
"user": "用户"
},
"noSectionsEnabled": "",
"noSectionsEnabled": "未启用任何分区",
"error": {
"noIntegration": "",
"internalServerError": ""
"noIntegration": "未配置 Tracearr 集成",
"internalServerError": "获取 Tracearr 数据失败"
}
},
"umami": {
"name": "",
"description": "",
"visitors": "",
"active": "",
"pageviews": "",
"visits": "",
"bounceRate": "",
"avgDuration": "",
"name": "Umami 分析",
"description": "显示来自 Umami 分析实例的访客统计",
"visitors": "访客",
"active": "活跃",
"pageviews": "浏览量",
"visits": "访问数",
"bounceRate": "跳出率",
"avgDuration": "平均",
"option": {
"websiteId": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noWebsites": "",
"configureIntegrationFirst": ""
"label": "网站",
"description": "选择您的 Umami 实例所追踪的网站",
"placeholder": "选择一个网站",
"loading": "正在加载网站……",
"noWebsites": "未找到网站",
"configureIntegrationFirst": "请先添加 Umami 集成"
},
"timeFrame": {
"label": "",
"description": "",
"label": "时间范围",
"description": "用于展示访客数据的时间段",
"option": {
"today": "",
"24h": "",
"7d": "",
"30d": "",
"month": "",
"lastMonth": ""
"today": "今天",
"24h": "过去 24 小时",
"7d": "过去 7 天",
"30d": "过去 30 天",
"month": "本月",
"lastMonth": "上月"
}
},
"eventName": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"none": "",
"configureWebsiteFirst": ""
"label": "事件",
"description": "显示特定事件的总发生次数",
"placeholder": "选择一个事件",
"loading": "正在加载事件……",
"noEvents": "未找到事件",
"none": "无事件",
"configureWebsiteFirst": "请先选择网站以加载事件"
},
"chartType": {
"label": "",
"description": "",
"label": "图表类型",
"description": "图表的显示样式",
"option": {
"bar": "",
"sparkline": ""
"bar": "条形图",
"sparkline": "迷你趋势图"
}
},
"chartStyle": {
"label": "",
"description": "",
"label": "图表样式",
"description": "如何在图表中显示事件序列",
"option": {
"grouped": "",
"overlay": ""
"grouped": "并排",
"overlay": "叠加(子集)"
}
},
"viewMode": {
"label": "",
"description": "",
"label": "视图",
"description": "组件主体中要显示的内容",
"option": {
"chart": "",
"events": "",
"topPages": "",
"topReferrers": ""
"chart": "图表",
"events": "事件图表",
"topPages": "热门页面",
"topReferrers": "热门来源"
}
},
"eventNames": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"configureWebsiteFirst": ""
"label": "事件",
"description": "选择一个或多个要图表化的事件",
"placeholder": "选择事件……",
"loading": "正在加载事件……",
"noEvents": "未找到事件",
"configureWebsiteFirst": "请先选择网站以加载事件"
},
"topCount": {
"label": "",
"description": ""
"label": "显示数量",
"description": "要显示的热门项目数量(最大 500)"
}
},
"event": {
"occurrences": "",
"label": ""
"occurrences": "事件",
"label": "事件"
},
"topPages": {
"direct": ""
"direct": "直接"
},
"topReferrers": {
"direct": ""
"direct": "直接"
},
"events": {
"selectPrompt": ""
"selectPrompt": "选择要图表化的事件"
},
"error": {
"noIntegration": "",
"noWebsite": "",
"noData": "",
"internalServerError": ""
"noIntegration": "未配置 Umami 集成",
"noWebsite": "未配置网站",
"noData": "无可用数据",
"internalServerError": "获取 Umami 数据失败"
}
},
"common": {
@@ -2304,50 +2304,50 @@
}
},
"immich-serverStats": {
"name": "",
"description": "",
"name": "Immich 服务器统计",
"description": "有关您的 Immich 实例的信息",
"option": {
"showUsers": {
"label": "",
"description": ""
"label": "显示用户",
"description": "显示实例上的用户数量"
},
"showPhotos": {
"label": "显示图片",
"description": ""
"description": "显示实例上的照片数量"
},
"showVideos": {
"label": "显示视频",
"description": ""
"description": "显示实例上的视频数量"
},
"showStorage": {
"label": "显示存储",
"description": ""
"description": "显示已使用的总存储空间"
}
},
"users": "",
"photos": "",
"videos": "",
"storage": ""
"users": "用户",
"photos": "照片",
"videos": "视频",
"storage": "已用存储"
},
"immich-albumCarousel": {
"name": "",
"name": "Immich 相册",
"option": {
"albumId": {
"label": "",
"description": ""
"label": "相册ID",
"description": "这是一个唯一标识符,用于指向您的 Immich 相册。您可以通过打开相册并从 URL 中复制那段唯一字符串来找到该 ID,例如:'a21aca09-9f05-47dc-b1a1-7b53c547226e'"
},
"rotationIntervalSeconds": {
"label": "",
"description": ""
"label": "间隔(秒)",
"description": "每 X 秒切换一次相册中的图片。最小为 1,最大不得超过 3600。"
},
"showPhotoInfo": {
"label": "显示图片信息",
"description": ""
"description": "在幻灯片底部显示图片日期和当前索引"
}
},
"description": "",
"noAlbumSelected": "",
"noPhotos": ""
"description": "Immich 相册图片幻灯片",
"noAlbumSelected": "未选择相册,请编辑组件并选择一个相册",
"noPhotos": "此相册中没有照片"
},
"downloads": {
"name": "下载客户端",
@@ -2658,7 +2658,7 @@
"label": "隐藏描述"
},
"showPosterImage": {
"label": ""
"label": "显示封面图"
}
}
},
@@ -2873,28 +2873,28 @@
"option": {}
},
"speedtestTracker": {
"name": "",
"description": "",
"name": "网速测试追踪器",
"description": "显示 网速测试追踪器 实例中的测速结果。",
"option": {
"showLatestResult": {
"label": ""
"label": "显示最新结果"
},
"showStats": {
"label": ""
"label": "显示平均值"
},
"showRecentResults": {
"label": ""
"label": "显示近期结果"
},
"showPingGraph": {
"label": ""
"label": "显示 Ping 图表"
}
},
"latestResult": "",
"averages": "",
"tests": "",
"recentResults": "",
"recentResultsSpeed": "",
"recentResultsPing": "",
"latestResult": "最新结果",
"averages": "平均值",
"tests": "{count}次测试",
"recentResults": "近期结果",
"recentResultsSpeed": "速度",
"recentResultsPing": "Ping",
"download": "下载",
"upload": "上传",
"ping": "Ping",
@@ -2902,13 +2902,13 @@
"avgDownload": "平均下载",
"avgUpload": "平均上传",
"avgPing": "平均Ping",
"healthy": "",
"unhealthy": "",
"ok": "",
"healthy": "健康",
"unhealthy": "不健康",
"ok": "正常",
"fail": "失败",
"noSectionsEnabled": "",
"noSectionsEnabled": "请在组件选项中至少启用一个模块。",
"error": {
"internalServerError": ""
"internalServerError": "获取测速数据失败"
}
},
"systemResources": {
@@ -3680,7 +3680,7 @@
"defaultColorScheme": {
"label": "默认配色方案",
"options": {
"auto": "",
"auto": "自动",
"light": "日间",
"dark": "暗黑"
}
@@ -3708,7 +3708,7 @@
"label": "Minecraft 服务状态"
},
"timetable": {
"label": ""
"label": "时刻表"
},
"iconsUpdater": {
"label": "图标更新"
@@ -3780,10 +3780,10 @@
"label": "天气"
},
"tracearr": {
"label": ""
"label": "Tracearr"
},
"umamiActiveVisitors": {
"label": ""
"label": "Umami 活跃访客"
}
},
"interval": {
@@ -3907,8 +3907,8 @@
"subtitle": "在许多语言中,有{count}人贡献了翻译"
},
"sponsors": {
"title": "",
"subtitle": ""
"title": "赞助商",
"subtitle": "{count}人通过财务方式支持该项目"
},
"libraries": {
"title": "相关的库",
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "端口"
},
"host": {
"label": "宿主机"
}
},
"action": {
@@ -4496,7 +4499,7 @@
"title": "全局命令",
"option": {
"colorScheme": {
"auto": "",
"auto": "切换外观模式",
"light": "切换到日间模式",
"dark": "切换到暗黑模式"
},
@@ -4548,13 +4551,13 @@
"group": {
"searchEngine": {
"title": "搜索引擎",
"tipDdgBangs": "",
"tipDdgBangs": "提示:你也可以在此使用任何 DuckDuckGo Bangs,例如 !something",
"bang": {
"searchWithQuery": "",
"pressEnterToOpen": "",
"engineSelected": "",
"typeQueryToContinue": "",
"emptyBangHint": ""
"searchWithQuery": "使用“‘’{query}”搜索“{label}”",
"pressEnterToOpen": "按 Enter 键打开",
"engineSelected": "已选择{label}({bang})",
"typeQueryToContinue": "输入搜索查询以继续",
"emptyBangHint": "输入 bang,例如 !yt,然后按空格键进行选择"
},
"children": {
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "crwdns1920:0crwdne1920:0"
},
"host": {
"label": "crwdns4249:0crwdne4249:0"
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porty"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porte"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Ports"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Θύρες"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Puertos"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Ports"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "יציאות"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Portovi"
},
"host": {
"label": ""
}
},
"action": {
+126 -123
View File
@@ -159,7 +159,7 @@
"label": "Nyissa meg a keresési eredményeket új ablakban"
},
"ddgBangs": {
"label": ""
"label": "DuckDuckGo parancsok engedélyezése"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "DuckDuckGo parancsok sikeresen bekapcsolódott"
},
"error": {
"message": ""
"message": "DuckDuckGo parancsokkat nem sikerült bekapcsolni"
}
}
},
@@ -1093,7 +1093,7 @@
},
"colorScheme": {
"options": {
"auto": "",
"auto": "Automatikus",
"light": "Világos",
"dark": "Sötét"
}
@@ -2111,101 +2111,101 @@
}
},
"umami": {
"name": "",
"description": "",
"visitors": "",
"active": "",
"pageviews": "",
"visits": "",
"bounceRate": "",
"avgDuration": "",
"name": "Umami Analitika",
"description": "Umami elemzési rendszeréből származó látogatói statisztikák megjelenítése",
"visitors": "látogatók",
"active": "aktív",
"pageviews": "oldalletöltések",
"visits": "látogatások",
"bounceRate": "visszapattan",
"avgDuration": "átl",
"option": {
"websiteId": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noWebsites": "",
"configureIntegrationFirst": ""
"label": "Honlap",
"description": "Válasszon ki egy olyan webhelyet, amelyet az Ön Umami-példánya követ",
"placeholder": "Válasszon ki egy weboldalt",
"loading": "Weboldalak betöltése...",
"noWebsites": "Nem található weboldal",
"configureIntegrationFirst": "Először csatlakoztassa az Umami integrációt"
},
"timeFrame": {
"label": "",
"description": "",
"label": "Időkeret",
"description": "Látogatói adatok megjelenítésének időtartama",
"option": {
"today": "",
"24h": "",
"7d": "",
"30d": "",
"month": "",
"lastMonth": ""
"today": "Ma",
"24h": "Elmúlt 24 óra",
"7d": "Elmúlt 7 nap",
"30d": "Elmúlt 30 nap",
"month": "Ez a hónap",
"lastMonth": "Előző hónap"
}
},
"eventName": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"none": "",
"configureWebsiteFirst": ""
"label": "Esemény",
"description": "Egy adott esemény összes előfordulásának megjelenítése",
"placeholder": "Válasszon ki egy eseményt",
"loading": "Események betöltése...",
"noEvents": "Nem található esemény",
"none": "Nincs esemény",
"configureWebsiteFirst": "Először válasszon ki egy weboldalt az események betöltéséhez"
},
"chartType": {
"label": "",
"description": "",
"label": "Diagramtípus",
"description": "A diagram megjelenítési stílusa",
"option": {
"bar": "",
"sparkline": ""
"bar": "Oszlopdiagram",
"sparkline": "Mikrodiagram"
}
},
"chartStyle": {
"label": "",
"description": "",
"label": "Diagramstílus",
"description": "Hogyan jeleníthetjük meg az eseménysorozatot a diagramon",
"option": {
"grouped": "",
"overlay": ""
"grouped": "Egymás mellett",
"overlay": "Átfedés (részhalmaz)"
}
},
"viewMode": {
"label": "",
"description": "",
"label": "Megtekintés",
"description": "Mit jelenítsen meg a widget törzse",
"option": {
"chart": "",
"events": "",
"topPages": "",
"topReferrers": ""
"chart": "Ábra",
"events": "Eseménynaptár",
"topPages": "Legnépszerűbb oldalak",
"topReferrers": "Legjobb hivatkozások"
}
},
"eventNames": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"configureWebsiteFirst": ""
"label": "Események",
"description": "Válasszon ki egy vagy több eseményt a grafikonhoz",
"placeholder": "Események kiválasztása...",
"loading": "Események betöltése...",
"noEvents": "Nem található esemény",
"configureWebsiteFirst": "Először válasszon ki egy weboldalt az események betöltéséhez"
},
"topCount": {
"label": "",
"description": ""
"label": "Items to show",
"description": "Megjelenítendő legnépszerűbb elemek száma (max. 500)"
}
},
"event": {
"occurrences": "",
"label": ""
"occurrences": "események",
"label": "események"
},
"topPages": {
"direct": ""
"direct": "Közvetlen"
},
"topReferrers": {
"direct": ""
"direct": "Közvetlen"
},
"events": {
"selectPrompt": ""
"selectPrompt": "Válassza ki a grafikonra felveendő eseményeket"
},
"error": {
"noIntegration": "",
"noWebsite": "",
"noData": "",
"internalServerError": ""
"noIntegration": "Nincs beállítva az Umami-integráció",
"noWebsite": "Nincs beállított webhely",
"noData": "Nincs elérhető adat",
"internalServerError": "Umami-adatok letöltése nem sikerült"
}
},
"common": {
@@ -2745,15 +2745,15 @@
}
},
"importForm": {
"title": ""
"title": "Importálás a Docker-ből"
},
"example": {
"label": ""
"label": "Példa"
},
"invalid": "",
"invalid": "A tároló meghatározása érvénytelen, kérjük, ellenőrizze az értékeket",
"noProvider": {
"label": "",
"tooltip": ""
"label": "Nincs Szolgáltató",
"tooltip": "A szolgáltató adatai nem értelmezhetők; kérjük, az importálás után állítsa be azokat manuálisan"
}
}
},
@@ -2826,34 +2826,34 @@
}
},
"error": {
"integrationsDisconnected": "",
"unknownContentOption": ""
"integrationsDisconnected": "Nincs elérhető adat, az összes integráció le van választva",
"unknownContentOption": "A hálózati állapot-widget ismeretlen tartalmi beállítása: "
},
"name": "",
"description": ""
"name": "Hálózat állapota",
"description": "A hálózathoz csatlakozó eszközök megjelenítése"
},
"networkController": {
"error": {
"internalServerError": ""
"internalServerError": "A hálózati vezérlő összefoglalójának letöltése nem sikerült"
}
},
"firewall": {
"name": "",
"description": "",
"name": "Tűzfal-felügyelet",
"description": "Megjeleníti a tűzfalak összefoglalóját",
"tab": {
"system": "",
"interfaces": ""
"system": "Rendszer",
"interfaces": "Interfészek"
},
"error": {
"internalServerError": ""
"internalServerError": "Nem sikerült adatokat lekérni a tűzfalról"
},
"option": {
"interfaces": ""
"interfaces": "Megjelenítendő hálózati interfészek"
},
"widget": {
"fwname": "",
"version": "",
"versiontitle": "",
"fwname": "Név",
"version": "Verzió",
"versiontitle": "Verziók",
"cputitle": "CPU használat",
"memorytitle": "Memória használat",
"cpu": "CPU",
@@ -2942,63 +2942,63 @@
"cpu": "CPU",
"memory": "RAM",
"gpu": "GPU",
"network": "",
"up": "",
"down": ""
"network": "NET",
"up": "FÖNN",
"down": "LENN"
}
},
"systemDisks": {
"name": "",
"description": "",
"name": "Rendszer lemezek",
"description": "A rendszer lemezhasználata",
"option": {
"showTemperatureIfAvailable": {
"label": ""
"label": "Ha elérhető, mutassa a hőmérsékletet"
},
"displayMode": {
"label": "",
"label": "Megjelenítési mód",
"option": {
"percentage": {
"label": ""
"label": "Százalék (pl: 76%)"
},
"absolute": {
"label": ""
"label": "Abszolút értékek (pl. 800 Gb / 1 Tb)"
},
"free": {
"label": ""
"label": "A szabad hely aránya (pl. 24% szabad)"
}
}
},
"showBackgroundBar": {
"label": ""
"label": "Háttérhasználati sáv megjelenítése"
}
},
"status": {
"unhealthy": ""
"unhealthy": "Hibás"
}
}
},
"widgetPreview": {
"toggle": {
"enabled": "",
"disabled": ""
"enabled": "Szerkesztési mód engedélyezve",
"disabled": "Szerkesztési mód tiltva"
},
"dimensions": {
"title": ""
"title": "Dimenzió váltása"
}
},
"board": {
"action": {
"duplicate": {
"title": "",
"message": "",
"title": "Tábla duplikálása",
"message": "Ezzel a {name} tábla teljes tartalmával együtt duplikálódik. Ha a widgetek olyan integrációkra hivatkoznak, amelyek használata nem engedélyezett, azok eltávolításra kerülnek.",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Tábla duplikálva",
"message": "A tábla sikeresen duplikálódott"
},
"error": {
"title": "",
"message": ""
"title": "Nem lehet duplikálni a táblát",
"message": "Nem tudtam duplikálni a táblát"
}
}
},
@@ -3006,44 +3006,44 @@
"notification": {
"success": {
"title": "A módosítások sikeresen alkalmazva",
"message": ""
"message": "A tábla sikeresen elmentve"
},
"error": {
"title": "Nem sikerült alkalmazni a változtatásokat",
"message": ""
"message": "A táblát nem sikerült elmenteni"
}
},
"confirmLeave": {
"title": "",
"message": ""
"title": "Mentetlen változtatások",
"message": "Nem mentett változtatásai vannak, biztosan el akarja hagyni az oldalt?"
}
},
"oldImport": {
"label": "",
"label": "Importálás a homarr 1.0.0 előtti verzióból",
"notification": {
"success": {
"title": "",
"message": ""
"title": "Sikeres importálás",
"message": "A tábla sikeresen importálva"
},
"error": {
"title": "",
"message": ""
"title": "Importálás nem sikerült",
"message": "Táblát nem sikerült importálni; további részletekért nézd meg a naplófájlokat"
}
},
"form": {
"file": {
"label": "",
"invalidError": ""
"label": "JSON fájl kiválasztása",
"invalidError": "Érvénytelen konfigurációs fájl"
},
"apps": {
"label": "Alkalmazások",
"avoidDuplicates": {
"label": "",
"description": ""
"label": "Kerülje az ismétlődéseket",
"description": "Nem veszi figyelembe azokat az alkalmazásokat, amelyeknél már létezik azonos href-vel rendelkező alkalmazás"
},
"onlyImportApps": {
"label": "",
"description": ""
"label": "Csak alkalmazások importálása",
"description": "Csak az alkalmazásokat adja hozzá, a táblát manuálisan kell újra létrehozni"
}
},
"name": {
@@ -3272,15 +3272,15 @@
"description": ""
},
"private": {
"title": "",
"description": ""
"title": "Nyilvános tábla készítése",
"description": "Biztosan nyilvánossá szeretnéd tenni ezt a fórumot? Ezzel a fórum mindenki számára elérhetővé válik."
}
}
},
"delete": {
"label": "",
"label": "Törölje ezt a táblát",
"description": "",
"button": "",
"button": "Törölje ezt a táblát",
"confirm": {
"title": "Tábla törlése",
"description": ""
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Portok"
},
"host": {
"label": ""
}
},
"action": {
+76 -73
View File
@@ -159,7 +159,7 @@
"label": "Apri risultati di ricerca in una nuova scheda"
},
"ddgBangs": {
"label": ""
"label": "Abilita bangs DuckDuckGo"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "DuckDuckGo bangs attivato con successo"
},
"error": {
"message": ""
"message": "Impossibile attivare i bangs DuckDuckGo"
}
}
},
@@ -1093,7 +1093,7 @@
},
"colorScheme": {
"options": {
"auto": "",
"auto": "Automatico",
"light": "Chiaro",
"dark": "Scuro"
}
@@ -2111,101 +2111,101 @@
}
},
"umami": {
"name": "",
"description": "",
"visitors": "",
"active": "",
"pageviews": "",
"visits": "",
"bounceRate": "",
"avgDuration": "",
"name": "Umami Analytics",
"description": "Visualizza le statistiche dei visitatori dall'istanza Umami analytics",
"visitors": "visitatori",
"active": "attivo",
"pageviews": "pagine viste",
"visits": "visite",
"bounceRate": "rimbalzo",
"avgDuration": "media",
"option": {
"websiteId": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noWebsites": "",
"configureIntegrationFirst": ""
"label": "Sito web",
"description": "Seleziona un sito web tracciato dalla tua istanza di Umami",
"placeholder": "Seleziona un sito web",
"loading": "Caricamento siti web...",
"noWebsites": "Nessun sito web trovato",
"configureIntegrationFirst": "Connetti prima un'integrazione Umami"
},
"timeFrame": {
"label": "",
"description": "",
"label": "Intervallo di tempo",
"description": "Il periodo di tempo per visualizzare i dati dei visitatori per",
"option": {
"today": "",
"24h": "",
"7d": "",
"30d": "",
"month": "",
"lastMonth": ""
"today": "Oggi",
"24h": "Ultime 24 ore",
"7d": "Ultimi 7 giorni",
"30d": "Ultimi 30 giorni",
"month": "Questo mese",
"lastMonth": "Mese precedente"
}
},
"eventName": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"none": "",
"configureWebsiteFirst": ""
"label": "Evento",
"description": "Mostra le occorrenze totali di un evento specifico",
"placeholder": "Seleziona un evento",
"loading": "Caricamento eventi...",
"noEvents": "Nessun evento trovato",
"none": "Nessun evento",
"configureWebsiteFirst": "Seleziona un sito web prima di caricare gli eventi"
},
"chartType": {
"label": "",
"description": "",
"label": "Tipo di grafico",
"description": "Stile di visualizzazione del grafico",
"option": {
"bar": "",
"sparkline": ""
"bar": "Grafico a barre",
"sparkline": "Grafico a linee compatto"
}
},
"chartStyle": {
"label": "",
"description": "",
"label": "Stile del grafico",
"description": "Come visualizzare la serie di eventi nel grafico",
"option": {
"grouped": "",
"overlay": ""
"grouped": "Affiancati",
"overlay": "Sovrapponi (subset)"
}
},
"viewMode": {
"label": "",
"description": "",
"label": "Vista",
"description": "Cosa mostrare nel corpo del widget",
"option": {
"chart": "",
"events": "",
"topPages": "",
"topReferrers": ""
"chart": "Grafico",
"events": "Grafico degli eventi",
"topPages": "Pagine più visitate",
"topReferrers": "Referrer principali"
}
},
"eventNames": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"configureWebsiteFirst": ""
"label": "Eventi",
"description": "Scegli uno o più eventi per il grafico",
"placeholder": "Seleziona eventi...",
"loading": "Caricamento eventi...",
"noEvents": "Nessun evento trovato",
"configureWebsiteFirst": "Seleziona prima un sito web per caricare gli eventi"
},
"topCount": {
"label": "",
"description": ""
"label": "Elementi da mostrare",
"description": "Numero di elementi principali da visualizzare (max 500)"
}
},
"event": {
"occurrences": "",
"label": ""
"occurrences": "eventi",
"label": "eventi"
},
"topPages": {
"direct": ""
"direct": "Diretto"
},
"topReferrers": {
"direct": ""
"direct": "Diretto"
},
"events": {
"selectPrompt": ""
"selectPrompt": "Seleziona gli eventi per il grafico"
},
"error": {
"noIntegration": "",
"noWebsite": "",
"noData": "",
"internalServerError": ""
"noIntegration": "Nessuna integrazione di Umami configurata",
"noWebsite": "Nessun sito web configurato",
"noData": "Nessun dato disponibile",
"internalServerError": "Recupero dati Umami fallito"
}
},
"common": {
@@ -3680,7 +3680,7 @@
"defaultColorScheme": {
"label": "Schema colore predefinito",
"options": {
"auto": "",
"auto": "Automatico",
"light": "Chiaro",
"dark": "Scuro"
}
@@ -3783,7 +3783,7 @@
"label": "Tracearr"
},
"umamiActiveVisitors": {
"label": ""
"label": "Visitatori Attivi Umami"
}
},
"interval": {
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porte"
},
"host": {
"label": "Host"
}
},
"action": {
@@ -4496,7 +4499,7 @@
"title": "Comandi generali",
"option": {
"colorScheme": {
"auto": "",
"auto": "Attiva/Disattiva modalità aspetto",
"light": "Passa al tema chiaro",
"dark": "Passa al tema scuro"
},
@@ -4548,13 +4551,13 @@
"group": {
"searchEngine": {
"title": "Motori di ricerca",
"tipDdgBangs": "",
"tipDdgBangs": "Suggerimento: puoi usare anche i bang di DuckDuckGo, ad esempio !qualcosa",
"bang": {
"searchWithQuery": "",
"pressEnterToOpen": "",
"engineSelected": "",
"typeQueryToContinue": "",
"emptyBangHint": ""
"searchWithQuery": "Cerca \"{query}\" con {label}",
"pressEnterToOpen": "Premere Invio per aprire",
"engineSelected": "{label} selezionato (!{bang})",
"typeQueryToContinue": "Digita la tua query di ricerca per continuare",
"emptyBangHint": "Digita un bang, ad es. !yt, poi premi Spazio per selezionare"
},
"children": {
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "ポート"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "포트"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Prievadai"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Poorten"
},
"host": {
"label": ""
}
},
"action": {
+76 -73
View File
@@ -159,7 +159,7 @@
"label": "Åpne søkeresultater i ny fane"
},
"ddgBangs": {
"label": ""
"label": "Aktiver DuckDuckGo bangs"
}
},
"error": {
@@ -252,10 +252,10 @@
"changeDdgBangs": {
"notification": {
"success": {
"message": ""
"message": "DuckDuckGo bangs er endret vellykket"
},
"error": {
"message": ""
"message": "Kan ikke slå av/på DuckDuckGo bangs"
}
}
},
@@ -1093,7 +1093,7 @@
},
"colorScheme": {
"options": {
"auto": "",
"auto": "Auto",
"light": "Lys",
"dark": "Mørk"
}
@@ -2111,101 +2111,101 @@
}
},
"umami": {
"name": "",
"description": "",
"visitors": "",
"active": "",
"pageviews": "",
"visits": "",
"bounceRate": "",
"avgDuration": "",
"name": "Umami Analytics",
"description": "Vise besøkende statistikk fra din Umami-analyseinstans",
"visitors": "besøkende",
"active": "aktiv",
"pageviews": "sidevisninger",
"visits": "besøk",
"bounceRate": "tilbakesende",
"avgDuration": "gjennomsnitt",
"option": {
"websiteId": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noWebsites": "",
"configureIntegrationFirst": ""
"label": "Nettsted",
"description": "Velg et nettsted som spores av Umami-instansen din",
"placeholder": "Velg en nettside",
"loading": "Laster inn nettsteder...",
"noWebsites": "Ingen nettsteder ble funnet",
"configureIntegrationFirst": "Koble til en Umami integrasjon først"
},
"timeFrame": {
"label": "",
"description": "",
"label": "Tidsramme",
"description": "Tidsperiode som skal vise besøkende data for",
"option": {
"today": "",
"24h": "",
"7d": "",
"30d": "",
"month": "",
"lastMonth": ""
"today": "I dag",
"24h": "Siste 24 timer",
"7d": "Siste 7 dager",
"30d": "Siste 30 dager",
"month": "Denne måneden",
"lastMonth": "Siste måned"
}
},
"eventName": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"none": "",
"configureWebsiteFirst": ""
"label": "Hendelse",
"description": "Vis totale forekomster av en bestemt hendelse",
"placeholder": "Velg en hendelse",
"loading": "Laster inn hendelser...",
"noEvents": "Ingen hendelser funnet",
"none": "Ingen hendelser",
"configureWebsiteFirst": "Velg en nettside først for å laste inn hendelser"
},
"chartType": {
"label": "",
"description": "",
"label": "Diagramtype",
"description": "Vis stil for diagrammet",
"option": {
"bar": "",
"sparkline": ""
"bar": "Søylediagram",
"sparkline": "Minigraf"
}
},
"chartStyle": {
"label": "",
"description": "",
"label": "Diagramtype",
"description": "Hvordan vise hendelsesserier i diagrammet",
"option": {
"grouped": "",
"overlay": ""
"grouped": "Side om side",
"overlay": "Overlegg (delmengde)"
}
},
"viewMode": {
"label": "",
"description": "",
"label": "Visning",
"description": "Hva skal vises i widgetkroppen",
"option": {
"chart": "",
"events": "",
"topPages": "",
"topReferrers": ""
"chart": "Diagram",
"events": "Hendelser diagram",
"topPages": "Toppsider",
"topReferrers": "Topp referenter"
}
},
"eventNames": {
"label": "",
"description": "",
"placeholder": "",
"loading": "",
"noEvents": "",
"configureWebsiteFirst": ""
"label": "Hendelser",
"description": "Velg ett eller flere hendelser i diagrammet",
"placeholder": "Velg en hendelse...",
"loading": "Laster inn hendelser...",
"noEvents": "Ingen hendelser funnet",
"configureWebsiteFirst": "Velg en nettside først for å laste inn hendelser"
},
"topCount": {
"label": "",
"description": ""
"label": "Gjenstander å vise",
"description": "Antall oppføringer som skal vises (maks 500)"
}
},
"event": {
"occurrences": "",
"label": ""
"occurrences": "hendelser",
"label": "hendelser"
},
"topPages": {
"direct": ""
"direct": "Direkte"
},
"topReferrers": {
"direct": ""
"direct": "Direkte"
},
"events": {
"selectPrompt": ""
"selectPrompt": "Velg hendelser for å diagram"
},
"error": {
"noIntegration": "",
"noWebsite": "",
"noData": "",
"internalServerError": ""
"noIntegration": "Ingen Umami integrasjon konfigurert",
"noWebsite": "Ingen nettsted konfigurert",
"noData": "Ingen data tilgjengelig",
"internalServerError": "Kunne ikke hente Umami data"
}
},
"common": {
@@ -3680,7 +3680,7 @@
"defaultColorScheme": {
"label": "Standard fargetema",
"options": {
"auto": "",
"auto": "Auto",
"light": "Lys",
"dark": "Mørk"
}
@@ -3783,7 +3783,7 @@
"label": "Tracearr"
},
"umamiActiveVisitors": {
"label": ""
"label": "Umami Aktive Besøkende"
}
},
"interval": {
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porter"
},
"host": {
"label": "Vert"
}
},
"action": {
@@ -4496,7 +4499,7 @@
"title": "Globale kommandoer",
"option": {
"colorScheme": {
"auto": "",
"auto": "Bytt visningsmodus",
"light": "Bytt til lys modus",
"dark": "Bytt til mørk modus"
},
@@ -4548,13 +4551,13 @@
"group": {
"searchEngine": {
"title": "Søkemotorer",
"tipDdgBangs": "",
"tipDdgBangs": "Tips: Du kan også bruke DuckDuckGo-bangs her, f.eks. !something",
"bang": {
"searchWithQuery": "",
"pressEnterToOpen": "",
"engineSelected": "",
"typeQueryToContinue": "",
"emptyBangHint": ""
"searchWithQuery": "Søk etter \"{query}\" med {label}",
"pressEnterToOpen": "Trykk på Enter for å åpne",
"engineSelected": "{label} valgt (!{bang})",
"typeQueryToContinue": "Skriv inn søkeordet for å fortsette",
"emptyBangHint": "Skriv inn en bang, f.eks. !yt, og trykk deretter på mellomrom for å velge"
},
"children": {
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porty"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Portas"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": ""
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porturi"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Порты"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Porty"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Vrata"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Portar"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Bağlantı noktaları"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Порти"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "Cổng"
},
"host": {
"label": ""
}
},
"action": {
+3
View File
@@ -3970,6 +3970,9 @@
},
"ports": {
"label": "端口"
},
"host": {
"label": ""
}
},
"action": {
+1203 -2259
View File
File diff suppressed because it is too large. Load diff
+47 -45
View File
@@ -13,7 +13,7 @@ catalog:
"@dnd-kit/sortable": ^10.0.0
"@dnd-kit/utilities": ^3.2.2
"@drizzle-team/brocli": ^0.12.0
'@eslint/js': 10.0.1
"@eslint/js": 10.0.1
"@extractus/feed-extractor": 7.2.1
"@gitbeaker/rest": ^43.8.0
"@homarr/gridstack": ^1.12.0
@@ -33,46 +33,46 @@ catalog:
"@mantine/notifications": ^8.3.18
"@mantine/spotlight": ^8.3.18
"@mantine/tiptap": ^8.3.18
"@million/lint": 1.0.14
"@next/eslint-plugin-next": 16.2.4
"@next/eslint-plugin-next": 16.2.6
"@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0
"@playwright/test": ^1.60.0
"@semantic-release/changelog": ^6.0.3
"@semantic-release/commit-analyzer": ^13.0.1
"@semantic-release/git": ^10.0.1
"@semantic-release/github": ^12.0.6
"@semantic-release/github": ^12.0.8
"@semantic-release/npm": ^13.1.5
"@semantic-release/release-notes-generator": ^14.1.0
"@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.41.1
"@tanstack/react-query": ^5.100.9
"@tanstack/react-query-devtools": ^5.100.9
"@tanstack/react-query-next-experimental": ^5.100.9
"@tabler/icons-react": ^3.44.0
"@tanstack/react-query": ^5.100.10
"@tanstack/react-query-devtools": ^5.100.10
"@tanstack/react-query-next-experimental": ^5.100.10
"@testcontainers/mysql": ^11.14.0
"@testcontainers/postgresql": ^11.14.0
"@testcontainers/redis": ^11.14.0
"@tiptap/extension-color": 3.22.5
"@tiptap/extension-highlight": 3.22.5
"@tiptap/extension-image": 3.22.5
"@tiptap/extension-link": ^3.22.5
"@tiptap/extension-placeholder": ^3.22.5
"@tiptap/extension-table": 3.22.5
"@tiptap/extension-table-cell": 3.22.5
"@tiptap/extension-table-header": 3.22.5
"@tiptap/extension-table-row": 3.22.5
"@tiptap/extension-task-item": 3.22.5
"@tiptap/extension-task-list": 3.22.5
"@tiptap/extension-text-align": 3.22.5
"@tiptap/extension-text-style": 3.22.5
"@tiptap/extension-underline": 3.22.5
"@tiptap/react": ^3.22.5
"@tiptap/starter-kit": ^3.22.5
"@tiptap/extension-color": 3.23.4
"@tiptap/extension-highlight": 3.23.4
"@tiptap/extension-image": 3.23.4
"@tiptap/extension-link": ^3.23.4
"@tiptap/extension-placeholder": ^3.23.4
"@tiptap/extension-table": 3.23.4
"@tiptap/extension-table-cell": 3.23.4
"@tiptap/extension-table-header": 3.23.4
"@tiptap/extension-table-row": 3.23.4
"@tiptap/extension-task-item": 3.23.4
"@tiptap/extension-task-list": 3.23.4
"@tiptap/extension-text-align": 3.23.4
"@tiptap/extension-text-style": 3.23.4
"@tiptap/extension-underline": 3.23.4
"@tiptap/react": ^3.23.4
"@tiptap/starter-kit": ^3.23.4
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0
"@trpc/server": ^11.17.0
"@trpc/tanstack-react-query": ^11.17.0
"@turbo/gen": ^2.9.8
"@turbo/gen": ^2.9.14
"@types/adm-zip": 0.5.8
"@types/bcrypt": 6.0.0
"@types/better-sqlite3": 7.6.13
@@ -81,7 +81,7 @@ catalog:
"@types/crypto-js": ^4.2.2
"@types/css-modules": ^1.0.5
"@types/dockerode": ^4.0.1
"@types/node": ^24.12.2
"@types/node": ^24.12.4
"@types/node-unifi": ^2.5.1
"@types/pg": ^8.20.0
"@types/prismjs": ^1.26.6
@@ -93,15 +93,15 @@ catalog:
"@types/xml2js": ^0.4.14
"@umami/node": ^0.4.0
"@vitejs/plugin-react": ^5.2.0
"@vitest/coverage-v8": ^4.1.5
"@vitest/ui": ^4.1.5
"@vitest/coverage-v8": ^4.1.6
"@vitest/ui": ^4.1.6
"@xterm/addon-canvas": ^0.7.0
"@xterm/addon-fit": 0.11.0
"@xterm/xterm": ^6.0.0
adm-zip: 0.5.17
babel-plugin-react-compiler: ^1.0.0
bcrypt: ^6.0.0
better-sqlite3: ^12.9.0
better-sqlite3: ^12.10.0
chroma-js: ^3.2.0
clsx: ^2.1.1
concurrently: ^9.2.1
@@ -121,21 +121,21 @@ catalog:
drizzle-orm: ^0.45.2
drizzle-zod: ^0.8.3
esbuild: ^0.28.0
eslint: ^10.3.0
eslint: ^10.4.0
eslint-config-prettier: ^10.1.8
eslint-config-turbo: ^2.9.8
eslint-config-turbo: ^2.9.14
eslint-plugin-import: ^2.32.0
eslint-plugin-jsx-a11y: ^6.10.2
eslint-plugin-react: ^7.37.5
eslint-plugin-react-hooks: ^6.1.1
fast-xml-parser: ^5.7.2
fast-xml-parser: ^5.8.0
fastify: ^5.8.5
flag-icons: ^7.5.0
glob: ^13.0.6
ical.js: ^2.2.1
ioredis: 5.10.1
isomorphic-dompurify: ^3.12.0
jotai: ^2.19.1
isomorphic-dompurify: ^3.13.0
jotai: ^2.20.0
jsdom: ^29.1.1
json5: ^2.2.3
ldapts: 8.1.7
@@ -144,9 +144,9 @@ catalog:
mantine-react-table: 2.0.0-beta.9
maria2: ^0.4.1
mysql2: 3.22.3
next: 16.2.4
next: 16.2.6
next-auth: 5.0.0-beta.31
next-intl: 4.11.0
next-intl: 4.12.0
node-loader: ^2.1.0
octokit: ^5.0.5
pg: ^8.20.0
@@ -155,8 +155,8 @@ catalog:
prettier-plugin-packagejson: ^3.0.2
prismjs: ^1.30.0
proxmox-api: 1.1.1
react: 19.2.5
react-dom: 19.2.5
react: 19.2.6
react-dom: 19.2.6
react-error-boundary: ^6.1.1
react-markdown: ^10.1.0
react-quill-new: ^3.8.3
@@ -167,24 +167,24 @@ catalog:
semver-parser: ^4.1.8
superjson: 2.2.6
svgson: ^5.3.1
swagger-ui-react: ^5.32.5
swagger-ui-react: ^5.32.6
testcontainers: ^11.14.0
timezones-ical-library: ^2.2.0
trpc-to-openapi: ^3.2.0
tsdav: ^2.2.0
tsdav: ^2.2.2
tsx: 4.20.4
turbo: ^2.9.8
turbo: ^2.9.14
typescript: ^5.9.3
typescript-eslint: ^8.58.2
undici: 7.24.6
use-deep-compare-effect: ^1.8.1
video.js: ^8.23.7
vite-tsconfig-paths: ^6.1.1
vitest: ^4.1.5
vitest: ^4.1.6
winston: 3.19.0
ws: ^8.20.0
ws: ^8.20.1
xml2js: ^0.6.2
yaml: 2.8.4
yaml: 2.9.0
zod: ^4.4.3
zod-form-data: ^3.0.1
zod-validation-error: ^5.0.0
@@ -193,3 +193,5 @@ minimumReleaseAge: 4320
minimumReleaseAgeExclude:
- next
- "@next/*"
# Renovate security update: turbo@2.9.14
- turbo@2.9.14
+1 -11
View File
@@ -16,8 +16,6 @@ fi
# Auth secret is generated every time the container starts as it is required, but not used because we don't need JWTs or Mail hashing
export AUTH_SECRET=$(openssl rand -base64 32)
# 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)
# Start nginx proxy
# 1. Replace the HOSTNAME in the nginx template file
@@ -38,21 +36,13 @@ else
REDIS_PID=$!
fi
node apps/tasks/tasks.cjs &
TASKS_PID=$!
node apps/websocket/wssServer.cjs &
WSS_PID=$!
node apps/nextjs/server.js &
NEXTJS_PID=$!
# Function to handle SIGTERM and shut down services
terminate() {
echo "Received SIGTERM. Shutting down..."
kill -TERM $NGINX_PID $TASKS_PID $WSS_PID $NEXTJS_PID 2>/dev/null
kill -TERM $NGINX_PID $NEXTJS_PID 2>/dev/null
wait
# kill redis-server last because of logging of other services and only if $REDIS_PID is set
if [ -n "$REDIS_PID" ]; then
+1 -1
View File
@@ -1 +1 @@
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated
[{"login":"ajnart","avatar_url":"https://avatars.githubusercontent.com/u/49837342?v=4"},{"login":"manuel-rw","avatar_url":"https://avatars.githubusercontent.com/u/30572287?v=4"},{"login":"Meierschlumpf","avatar_url":"https://avatars.githubusercontent.com/u/63781622?v=4"},{"login":"SeDemal","avatar_url":"https://avatars.githubusercontent.com/u/26098587?v=4"},{"login":"lammersbjorn","avatar_url":"https://avatars.githubusercontent.com/u/71191962?v=4"},{"login":"LarveyOfficial","avatar_url":"https://avatars.githubusercontent.com/u/39219859?v=4"},{"login":"c00ldude1oo","avatar_url":"https://avatars.githubusercontent.com/u/15677803?v=4"},{"login":"InterN0te","avatar_url":"https://avatars.githubusercontent.com/u/10882916?v=4"},{"login":"Aimsucks","avatar_url":"https://avatars.githubusercontent.com/u/1828486?v=4"},{"login":"JannesV","avatar_url":"https://avatars.githubusercontent.com/u/5984244?v=4"},{"login":"MauriceNino","avatar_url":"https://avatars.githubusercontent.com/u/18658092?v=4"},{"login":"hillaliy","avatar_url":"https://avatars.githubusercontent.com/u/950010?v=4"},{"login":"Skeeww","avatar_url":"https://avatars.githubusercontent.com/u/9639992?v=4"},{"login":"RichyHBM","avatar_url":"https://avatars.githubusercontent.com/u/5472275?v=4"},{"login":"Bonfire","avatar_url":"https://avatars.githubusercontent.com/u/5704760?v=4"},{"login":"wieli99","avatar_url":"https://avatars.githubusercontent.com/u/46794825?v=4"},{"login":"ishaanparlikar","avatar_url":"https://avatars.githubusercontent.com/u/43013132?v=4"},{"login":"Zareix","avatar_url":"https://avatars.githubusercontent.com/u/29818713?v=4"},{"login":"tuggan","avatar_url":"https://avatars.githubusercontent.com/u/4325608?v=4"},{"login":"Spillebulle","avatar_url":"https://avatars.githubusercontent.com/u/46653946?v=4"},{"login":"spkesDE","avatar_url":"https://avatars.githubusercontent.com/u/1682314?v=4"},{"login":"j3lte","avatar_url":"https://avatars.githubusercontent.com/u/2557568?v=4"},{"login":"dslatt","avatar_url":"https://avatars.githubusercontent.com/u/5211001?v=4"},{"login":"Malong11-007","avatar_url":"https://avatars.githubusercontent.com/u/40298510?v=4"},{"login":"StefanB7","avatar_url":"https://avatars.githubusercontent.com/u/22056509?v=4"},{"login":"souravghosh01","avatar_url":"https://avatars.githubusercontent.com/u/37548139?v=4"},{"login":"SR3u","avatar_url":"https://avatars.githubusercontent.com/u/2380522?v=4"},{"login":"Rikpat","avatar_url":"https://avatars.githubusercontent.com/u/33869814?v=4"},{"login":"NormanJS","avatar_url":"https://avatars.githubusercontent.com/u/83409549?v=4"},{"login":"No-Maines-Land","avatar_url":"https://avatars.githubusercontent.com/u/20687725?v=4"},{"login":"stark1tty","avatar_url":"https://avatars.githubusercontent.com/u/69326376?v=4"},{"login":"Momcilo42","avatar_url":"https://avatars.githubusercontent.com/u/10118137?v=4"},{"login":"marius-arch","avatar_url":"https://avatars.githubusercontent.com/u/72494359?v=4"},{"login":"Lumilias","avatar_url":"https://avatars.githubusercontent.com/u/10852161?v=4"},{"login":"Havock94","avatar_url":"https://avatars.githubusercontent.com/u/7635248?v=4"},{"login":"Lollyde","avatar_url":"https://avatars.githubusercontent.com/u/11347524?v=4"},{"login":"krishnamuppaneni","avatar_url":"https://avatars.githubusercontent.com/u/4703366?v=4"},{"login":"devtobi","avatar_url":"https://avatars.githubusercontent.com/u/28538704?v=4"},{"login":"TyxTang","avatar_url":"https://avatars.githubusercontent.com/u/90897955?v=4"},{"login":"alotlikebeans","avatar_url":"https://avatars.githubusercontent.com/u/19630511?v=4"},{"login":"WillyJL","avatar_url":"https://avatars.githubusercontent.com/u/49810075?v=4"},{"login":"anonysoul","avatar_url":"https://avatars.githubusercontent.com/u/105715150?v=4"},{"login":"cospeedster","avatar_url":"https://avatars.githubusercontent.com/u/52749645?v=4"},{"login":"diederbert","avatar_url":"https://avatars.githubusercontent.com/u/162878798?v=4"},{"login":"digdilem","avatar_url":"https://avatars.githubusercontent.com/u/4052275?v=4"},{"login":"dinghino","avatar_url":"https://avatars.githubusercontent.com/u/14729459?v=4"},{"login":"gfomichev","avatar_url":"https://avatars.githubusercontent.com/u/1596146?v=4"},{"login":"gnattu","avatar_url":"https://avatars.githubusercontent.com/u/12995396?v=4"},{"login":"tuxpizza","avatar_url":"https://avatars.githubusercontent.com/u/84710786?v=4"},{"login":"xmarkurion","avatar_url":"https://avatars.githubusercontent.com/u/46654687?v=4"},{"login":"huanga","avatar_url":"https://avatars.githubusercontent.com/u/732970?v=4"},{"login":"taos15","avatar_url":"https://avatars.githubusercontent.com/u/122130728?v=4"},{"login":"arghyadipchak","avatar_url":"https://avatars.githubusercontent.com/u/32430374?v=4"},{"login":"AuthorShin","avatar_url":"https://avatars.githubusercontent.com/u/4959043?v=4"},{"login":"benniblot","avatar_url":"https://avatars.githubusercontent.com/u/47495914?v=4"},{"login":"bebsworthy","avatar_url":"https://avatars.githubusercontent.cLine truncated
+11 -46
View File
@@ -1,57 +1,19 @@
{
"$schema": "https://turborepo.org/schema.json",
"globalDependencies": ["**/.env"],
"globalEnv": [
"AUTH_LDAP_BASE",
"AUTH_LDAP_BIND_DN",
"AUTH_LDAP_BIND_PASSWORD",
"AUTH_LDAP_GROUP_CLASS",
"AUTH_LDAP_GROUP_FILTER_EXTRA_ARG",
"AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE",
"AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE",
"AUTH_LDAP_SEARCH_SCOPE",
"AUTH_LDAP_URI",
"AUTH_OIDC_CLIENT_ID",
"AUTH_OIDC_CLIENT_NAME",
"AUTH_OIDC_CLIENT_SECRET",
"AUTH_OIDC_FORCE_USERINFO",
"AUTH_OIDC_ISSUER",
"AUTH_OIDC_SCOPE_OVERWRITE",
"AUTH_OIDC_GROUPS_ATTRIBUTE",
"AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE",
"AUTH_LDAP_USERNAME_ATTRIBUTE",
"AUTH_LDAP_USER_MAIL_ATTRIBUTE",
"AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG",
"AUTH_OIDC_AUTO_LOGIN",
"AUTH_LOGOUT_REDIRECT_URL",
"AUTH_PROVIDERS",
"AUTH_SESSION_EXPIRY_TIME",
"CI",
"DISABLE_REDIS_LOGS",
"DB_URL",
"DB_HOST",
"DB_USER",
"DB_PASSWORD",
"DB_NAME",
"DB_PORT",
"DB_DRIVER",
"DOCKER_HOSTNAMES",
"DOCKER_PORTS",
"DOCKER_SOCKET_PATHS",
"NODE_ENV",
"PORT",
"LOCAL_CERTIFICATE_PATH",
"LOG_LEVEL",
"SECRET_ENCRYPTION_KEY",
"SKIP_ENV_VALIDATION"
],
"remoteCache": { "signature": false },
"globalEnv": ["CI", "NODE_ENV", "SKIP_ENV_VALIDATION"],
"ui": "stream",
"futureFlags": {
"affectedUsingTaskInputs": true
},
"tasks": {
"topo": {
"dependsOn": ["^topo"]
"dependsOn": ["^topo"],
"inputs": ["package.json"]
},
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", "!**/*.test.ts", "!**/*.test.tsx", "!**/*.spec.ts", "!**/__tests__/**"],
"outputs": [".next/**", "!.next/cache/**", "next-env.d.ts", ".output/**", ".vercel/output/**"]
},
"dev": {
@@ -59,15 +21,18 @@
"cache": false
},
"format": {
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/.prettiercache"],
"outputLogs": "new-only"
},
"lint": {
"dependsOn": ["^topo"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/.eslintcache"]
},
"typecheck": {
"dependsOn": ["^topo"],
"inputs": ["$TURBO_DEFAULT$"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
},
"clean": {