Compare commits

..
Author SHA1 Message Date
Thomas Camlong 2ebfe659b2 revert(perf): drop the dockerode and docker-modem pnpm patches
Both patches came out of this work and both are removed at the maintainer's
request. The finding was real - ssh2's WASM heap costs +14.0 MiB RSS on require,
and @grpc/grpc-js another 14.6 MiB across 61 files, neither of which a
socket-based Docker install ever uses - but patching a dependency means every
future dockerode or docker-modem bump fails until someone regenerates the patch,
and that recurring cost is not worth roughly 25 MiB.

The finding stays on record in SERVER-MEMORY.md because the fix belongs upstream:
both packages require a module at file scope while using it in exactly one
already-guarded branch, so a PR to each would fix it for every consumer with no
patch to maintain here.

The two remaining patches, @types/node-unifi and trpc-to-openapi, predate this
work and are untouched. allowUnusedPatches was already set on dev and is left
alone. Lockfile regenerated: patchedDependencies now lists only those two.

Also sharpens the Cache Components entry. The SqliteError was simply an unmigrated
build database, which is fixable - but feeding the build a database would be the
wrong fix, because serverSetting is per-installation data on the user's volume and
a shell prerendered against the build machine's database would bake one install's
settings into every other install's first paint. The error was Next.js correctly
reporting a request-time read sitting in the shell path.
2026-08-14 11:16:55 +02:00
Thomas Camlong ca0bb36664 docs(perf): Cache Components tested and rejected, with the numbers
Enabled cacheComponents: true on Next 16.3.0-preview.9 and built. The flag works
and the usual migration friction is absent - zero files export dynamic,
revalidate or fetchCache.

Two blockers. The [locale] dynamic root segment needs generateStaticParams,
which is cheap: the guide is explicit that one value suffices, because
dynamicParams: true now serves the App Shell for unlisted params and upgrades in
the background. Not 53 routes x 38 locales.

Past that, 111 routes fail with the same error - SqliteError: no such table:
serverSetting. Prerendering runs at build time and the root layout
unconditionally awaits auth, two DB reads and a cookie read. For a self-hosted
app whose database is created at runtime on the user's volume there is nothing to
query during a build. Deferring those reads means moving the providers that wrap
children inside Suspense, which leaves a static shell of little more than
<html><body> - the exact failure the migration guide warns about.

And the prize is ~16-21 ms: measured TTFB is ~3 ms for a route that skips the
root layout against ~19-24 ms for one that goes through it. Meanwhile the board -
the main screen - is already dynamic(..., { ssr: false }) with a skeleton, so it
already paints an instant shell and Cache Components has no server render to
unblock there.

Recorded with both caveats: the ~20 ms is warm local SQLite and would be larger
on a network database, and the useful idea inside this - not blocking the first
byte on auth plus two DB queries - can be had by streaming those reads in the
existing layout, without the migration.

Experiment fully reverted; no code changes in this commit.
2026-08-14 11:11:50 +02:00
Thomas Camlong 839b4721fb perf(iframe): lazy-load the iframe widget
Also from PR #6582. An iframe is an entire browsing context, so one scrolled off
a tall board is the single most expensive thing a widget can load eagerly - and
this one had no loading attribute at all.

Records what else was mined from that PR and declined, notably its dynamic()
for BoardItemMenu: that component already early-returns null outside edit mode,
so there is no per-item overlay cost, and dynamic() fetches its chunk when the
component renders rather than when it returns something - so the chunk would
still load on every board render. It only pays off with the isEditMode guard
hoisted to the call site, which is a board-rendering change worth measuring
first rather than adopting on faith.
2026-08-14 11:04:08 +02:00
Thomas Camlong 40374803d4 perf(widgets): stop building collapsed Accordion panels
From reading PR #6582, which adds keepMounted={false} to the two coolify
accordions. The widget sweep had checked Tabs and never checked Accordion, so it
missed this on all five sites - an honest gap that PR closed for two of them.

Accordion behaves exactly like Tabs, verified in the installed version rather
than assumed: with three marker children per panel, the default renders 3 of 3
children of the *collapsed* panel and keepMounted={false} renders 0.

It matters more here than in Tabs, because collapsed is an accordion section's
normal state rather than the exception:

  coolify instance-card          three remote-data lists: servers, applications,
                                 services - all built while collapsed
  coolify single-instance-layout same three lists
  health-monitoring cluster      per-node resource tables   (beyond #6582)
  firewall                       interface rows             (beyond #6582)

Left mounted: the multiReleasesRepositories input, which holds selection state
and is edit-modal only rather than on the board render path.

In every fixed case the open/closed state lives in the parent, so unmounting a
collapsed panel discards nothing.

The spec is renamed tabs-keep-mounted -> keep-mounted since it now pins both
components, and three verdicts in the audit table are corrected: coolify was
recorded "clean (bounded)" and firewall triaged on other grounds, while both were
carrying this cost.
2026-08-14 11:02:13 +02:00
Thomas Camlong 2de3c86b31 fix(docs): exclude generated output from the docs typecheck
CI failed @homarr/docs:typecheck with a wall of TS1473 "An import declaration can
only be used at the top level of a module" pointing at
build/__server/assets/js/*.js - Docusaurus's own emitted bundles, not source.

The docs tsconfig includes **/*.js with allowJs enabled and excludes only
node_modules, so anything the build writes into the package is typechecked as
source. The turbo `typecheck` task dependsOn ["^topo"] and not on its own
package's `build`, so `docusaurus build` and `tsc --noEmit` run concurrently -
and tsc can observe the transient build/__server/ directory before the build
removes it again.

That makes it a latent race rather than a deterministic failure, which is why it
has not bitten before: it needs @homarr/docs to be in the affected set, and
merging dev brought apps/docs changes in. Locally it passes either way, because a
completed build leaves no __server directory behind.

Verified against a control rather than assumed: with build/__server/assets/js
containing a nested import declaration, the current tsconfig reports 0 errors and
the same tsconfig without the exclude reports the 2 TS1473 errors, matching CI.
2026-08-13 19:08:08 +02:00
ajnart be0043ab81 style: auto-fix lint and format 2026-08-13 16:41:32 +00:00
Thomas Camlong 89f081538c Merge origin/dev into feat/phase1-perf-impl
The branch was 12 commits behind dev and GitHub reported it CONFLICTING, which is
why the real CI workflow had not run since 2026-08-10 - a conflicting PR has no
mergeable ref, so pull_request-triggered workflows are skipped. Only the
lightweight checks (semantic title, CodeQL, CodeRabbit) were still reporting,
which made the PR look green while nothing was actually being tested.

Three conflicts, all of them this branch's barrel-diet work meeting dev's new
What's Up Docker integration. In every case the branch's structure is kept and
WUD is absorbed into it rather than the reverse:

  integrations/base/creator.ts   keep the lazy loader map; add wud as
                                () => import(...) rather than dev's static import,
                                which would have undone the lazy creator
  integrations/src/index.ts      keep the slim barrel; re-export WudStats and
                                WudContainerUpdate as `export type`, which is
                                erased at build and so adds no runtime module
  widgets/src/index.tsx          stays deleted - this was the barrel that put
                                every widget in the initial client graph

Registering WUD in the slimmed structures then needed three more entries that the
merge could not infer: base/kind-map.ts (type-only kind -> class map),
widgets/src/import.ts (type-only WidgetImports map, both the import and the
member). Each was found by typecheck rather than guessed at.

Verified: 37/37 packages typecheck, 1866/1881 unit tests pass. The 3 failures are
home-assistant and pi-hole, both testcontainers-based; both pass 11/11 in
isolation and fail only under 150-file parallel load with the container's payload
half-initialised.
2026-08-13 18:40:08 +02:00
Thomas Camlong fd351e4adf test(widgets): pin the Tabs behaviour that keepMounted={false} depends on
health-monitoring and media-missing pass keepMounted={false} because Mantine
otherwise builds the inactive panel's whole subtree. That came from reading
Mantine's docs, which is a weak basis for a prop that trades a remount cost for a
mount saving: if Mantine ever stopped rendering inactive panels, the prop would be
pure loss and nobody would notice.

Verified in the installed version instead. With three marker children per panel,
the default renders 3 of 3 children of the *inactive* panel into the DOM, and
keepMounted={false} renders 0. If that changes, this fails and the prop should
come back out.

Also records two things checked while re-reading the audit:

_inputs/ is excluded from the widget sweep on purpose, not by oversight - its
2,748 lines are option editors reached only from the edit modal and the
custom-widget form, and the one widget importing from it uses `import type`,
which is erased at build.

The "statically analyzable dynamic imports" rule has exactly one exception,
translation/src/mapping.ts building import(`./lang/${language}.json`) over 36
languages. It stands: the rule targets accidental over-inclusion, and every
language is genuinely reachable since the user can switch at runtime. Server-side
message loading is already memoised per locale, so it is not per-request either.
2026-08-13 18:28:29 +02:00
Thomas Camlong 43cc40eb66 docs(perf): optimizePackageImports is inert here - Turbopack makes it a no-op
Seven Mantine entry points were missing from optimizePackageImports while
core/hooks/icons-react were listed. Added all seven, rebuilt, and the client
bundle came out byte-for-byte identical: 482 files, 18050.7 KiB, same largest
chunk, same chunk hashes.

Identical output down to the hashes normally means a cached build, and
turbopackFileSystemCacheForBuild is enabled here, so it got checked rather than
written up as a null result. The Next.js docs are explicit: "This is not needed
when using Turbopack. Turbopack automatically analyzes and optimizes imports
without requiring this configuration." The build banner confirms Turbopack.

So the change is reverted and the existing three-entry list documented as dead
config, kept only as a webpack fallback. This also retires the sub-question of
whether @homarr/ui - the largest barrel at 111 importers - could be optimised
this way: it cannot, and it does not need to be.

Two notes for anyone revisiting: Next 16 with Turbopack prints no size columns in
its route table and emits no app-build-manifest.json, so per-route first-load JS
is unavailable from build output; these numbers are total bytes under
.next/static/chunks.
2026-08-13 15:36:12 +02:00
Thomas Camlong 320c51ac89 fix(http): stop destroying superseded agents, which could abort live requests
Follow-up to the agent caching change. When the trust material changes the caches
are cleared, and that path called agent.destroy() on the node https agents.

Node's documentation for agent.destroy() is "Destroy any sockets that are
currently in use by the agent" - so an admin uploading a certificate could have
aborted an HTTPS request that was already in flight, for instance a nextcloud
calendar sync. The agents run without keepAlive (verified: new https.Agent({})
reports keepAlive false on node 24), so destroying bought nothing anyway; each
socket closes on its own once its response completes.

undici's close() is kept, because it waits for enqueued requests to finish and
therefore cannot interrupt one in flight.

Also verified the correctness premise of sharing a single agent across every
integration, which the previous commit asserted but did not test: three origins
served by one agent each got their own pooled connection, four requests over one
connection apiece, with no response misrouted between them.
2026-08-13 15:36:12 +02:00
Thomas Camlong 09df2910c5 docs(perf): check the widgets against Vercel's React/Next performance rules
Sourced with the skills CLI - npx skills find "react performance", then
npx skills use affaan-m/ecc@react-performance, an adaptation of vercel-labs
react-best-practices: 70+ rules across 8 categories. Every mechanically
checkable rule was run against the widgets.

Seven already hold, and that is worth recording as much as a finding: no widget
registers a scroll listener, no numeric && can render a literal 0, the nested
array scans that exist are over single-digit collections, React.cache() is
applied, TanStack Query does the deduplication, content-visibility is on the six
lists that need it, and the two components-declared-in-render were already found
and fixed.

One rule this codebase deliberately contradicts: the skill prefers React 19
<Activity> over unmounting for tabs, and this pass did the opposite with
keepMounted={false}. The rule optimises for switching; a dashboard optimises for
mounting. Every board load mounts every widget while a user switches a widget's
inner tabs rarely, so building a hidden monitoring view on every page load buys
a faster switch nobody performs. Where state loss would show - custom-api's user
JSX, the edit modal's forms - the panels stay mounted.

Two candidates declined rather than guessed at, both recorded with reasoning:
the 20+ `data = []` defaults only destabilise while data is undefined, since
react-query returns a stable reference once loaded; and the 7 unlisted Mantine
entry points need two production builds and a first-load-JS diff before anyone
claims they help. @homarr/ui is not eligible at all despite 111 importers,
because Next's barrel optimiser only rewrites pure re-export files.

Also records the process error behind the earlier "no skills CLI" claim: the
entries in ~/.claude/skills are symlinks, which find -type f skips, so the
directory looked empty right after cat had succeeded on a file inside it.
2026-08-11 16:40:27 +02:00
Thomas Camlong 083a926719 docs(perf): give every one of the 50 widgets a verdict
Completes the sweep: 11 fixed, 8 triaged, 10 read and found clean, 21 verified
bounded. Each verdict records the evidence behind it, because "I read this" and
"its list cannot get long" are different strengths of claim and should not look
alike in a table.

The four widgets the ranking flagged as most likely to hide something all came
back clean, and three were already bounded before I looked - traefik slices to
what its width shows, umami to its own limit option, beszel-alerts to
options.maxHistoryItems. system-resources looked worst of all with 14 unmemoised
maps, until it turned out none of the six chart components it feeds is memoised
either, so a stable array identity would preserve nothing.
2026-08-11 16:33:33 +02:00
Thomas Camlong ec79a15023 perf(widgets): stop building hidden tab panels, and drop a third-party image fallback
Mantine's Tabs keeps inactive panels mounted by default. On Mantine 9 with
React 19 that uses <Activity>, which preserves state and unmounts effects but
still builds the entire subtree - so a two-tab widget pays for both while
showing one.

  health-monitoring  both the system and the cluster monitoring view were built
  media-missing      both tabs built their full card list, each card with a
                     poster and a blurred backdrop

Two other Tabs users are deliberately left mounted: custom-api panels can hold
user component state, and the widget edit modal would reset half-filled forms.

media-missing also rendered every card into a scrolling tile with no lazy
loading, the same shape as media-releases. Its card height is fixed per density,
so contain-intrinsic-size is exact rather than a guess.

calendar fell back to https://placehold.co/400x400 when a poster failed to
load, so a self-hosted dashboard made a third-party request - once per failing
event - to draw a grey square, and showed nothing at all on a host without
internet access. Same grey square, inline.

The rss feed handler logged the whole serialised feed entry through a raw
console.debug, which no log level could suppress, and reported a match count
that was really the regex capture-group count.

Adds scripts/benchmarks/widget-inventory.mjs and docs/perf/WIDGET-AUDIT.md, a
verdict for all 50 widgets.
2026-08-11 16:30:22 +02:00
Thomas Camlong cf240addea perf(image-proxy): stream proxied images instead of buffering them
forwardImageAsync awaited response.arrayBuffer(), so the whole image sat in
server memory before anything was sent to the client. A media board asks for
dozens of posters and backdrops at once, so that is dozens of full images held
simultaneously - and a client that gives up midway still cost a complete
server-side download.

Forwarding response.body instead lets backpressure reach upstream. Measured
with 30 concurrent 2 MiB images and deliberately slow clients, one fresh
process per arm because the allocator does not return freed spans:

  buffered   +116.0 MiB, +117.8 MiB peak RSS
  streamed    +74.2 MiB,  +78.8 MiB peak RSS

-36% peak RSS on that load, and it scales with concurrency times image size.
Content-Length is now forwarded too, so clients still get a progress bar.
2026-08-11 16:09:37 +02:00
Thomas Camlong 3ba2800db9 perf(http): reuse the certificate agent instead of building one per request
fetchWithTrustedCertificatesAsync has 125 call sites and none of them pass a
dispatcher, so every HTTP request Homarr makes - every integration poll, every
proxied image - built a brand new EnvHttpProxyAgent. An agent owns its
connection pool and its TLS session cache, so a fresh one per request means a
full TLS handshake every time and no keep-alive reuse at all.

Measured against a local TLS server, 60 sequential requests:

  fresh agent per request   5.11 ms/req   60 TLS handshakes
  one shared agent          1.69 ms/req    2 TLS handshakes

3.0x faster and 30x fewer handshakes on loopback; over a real network the gap
is larger, since that is where handshake round-trips actually cost something.

Agent construction itself is cheap (0.004 ms, 1.7 KiB) because undici builds
the TLS store lazily at connect time - so the cost was never construction, it
was the handshake that a fresh pool forces.

The trust material is still read on every call and the cached agents are keyed
on a hash of it, so adding or removing a certificate - including by dropping a
file into the mounted folder rather than going through the UI - takes effect on
the very next request, exactly as before. Superseded agents are closed so their
idle sockets are released.
2026-08-11 16:09:37 +02:00
Thomas Camlong 9b54edd2db docs(perf): correct the "16 MiB tRPC subtree" lever - it was already removed
This branch has repeatedly named a 16.10 MiB subtree of 460 files, reached by every API
route, as the one remaining server-side lever with real headroom, attributing it to the tRPC
router importing every integration eagerly. Both halves are wrong.

The integration creator was already fully lazy: every kind in creator.ts is
`() => import(...)`, so no integration class loads until one is constructed. The earlier
barrel diet had done this properly - the claim was never checked against the code.

The subtree was an artefact of route preloading, measured before preloadEntriesOnStart:
false shipped, when Next loaded all 74 routes' module graphs at startup. Re-measured on the
current build, boot is 706 files and 10.56 MiB:

     3.73 MiB    28 files  .next/server/chunks (the app's own boot graph)
     2.16 MiB    14 files  better-sqlite3 (2.1 MiB of it the native binary)
     1.75 MiB   359 files  next/dist
     1.58 MiB    25 files  next/dist/compiled
     0.45 MiB    77 files  mysql2        <- known, structurally blocked
     0.07 MiB    13 files  pg            <- same

There is no single large server lever left. What remains is diffuse: a full multi-tab session
loads 15.44 MiB of app chunks across 241 files, and both the ~41 MiB of retained source and
the ~38 MiB of compiler zones scale with that. Reducing it means shipping less application
JavaScript per route, which is a broad refactor with no single target.
2026-08-11 10:22:31 +02:00
Thomas Camlong 37dead1690 docs(perf): verify the off-screen changes, and record two instruments that lied
The off-screen work was committed unmeasured; this closes that debt. Measured on two
containers restored from identical data:

  image requests   138, 135  ->  22, 22   (-84%)
  image bytes        12.27 MiB -> 0.03 MiB
  layout objects  11611, 7836  ->  3694, 3705  (-53%)

A 99.8% drop in image bytes is not believable, so it was checked instead of reported. Two
checks were bad instruments before one worked:

  - Counting decoded images via getBoundingClientRect() reported 80/80 loaded in BOTH arms.
    Reading geometry forces layout, which forces content-visibility subtrees to render, which
    triggers the very image loads being tested for. The instrument caused the result.
  - Screenshots came back byte-identical in both arms, because this environment shows an
    onboarding overlay and media-requests renders blank with unreachable integrations. Good
    evidence of no visual difference; useless as evidence that images render.
  - Counting img.complete && naturalWidth > 0 reads no geometry and gave the answer: 80/80
    decoded in both arms, 138 responses before against 25 after.

So nothing is visually broken. The 80 imgs are integration icons sharing URLs, deduplicated to
~25 unique fetches in both arms; the ~113 avoided requests are other widgets' off-screen
artwork, chiefly media-requests' full-size per-card backdrops, which is also where the 12 MiB
came from.

Stated limit: media-requests renders blank in this environment, so its visible-card behaviour
after the change is unverified. Media-releases is verified at 80/80 decoded.
2026-08-11 10:20:33 +02:00
Thomas Camlong d2e86895f4 docs(perf): rewrite the client doc with the measured overlay results
Replaces the earlier version with what was actually measured, and closes out all 54
findings from the widget audit rather than leaving categories open:

  overlay-per-row (12)      5 fixed; 7 are functional tooltips with real labels
  index-key (11)            all safe - static text splits, or stateless rows from
                            user-configured JSONPath data with no stable id
  nested-component (2)      both fixed
  inline-style-per-row (29) left alone; a fresh object per row only defeats memoisation
                            if the child is memoised, and these are not

Measured, deterministic per-widget counts:

  mediaReleases      340 comp / 8387 fib  ->  170 / 8057
  calendar           171 comp / 1195 fib  ->   75 /  907
  beszelSystemTable   36 comp / 1286 fib  ->   18 / 1230

Records two metrics that turned out unusable - Chrome's board-level JSEventListeners read
7905 to 17114 across six runs of identical code, and its Nodes counter includes detached
nodes - and one claim withdrawn: the media-releases tooltip fix measured zero, the win for
that widget came entirely from the shared OverflowBadge fix.
2026-08-11 10:11:08 +02:00
Thomas Camlong afa68687eb perf(widget-inputs): stabilise the drag handle component type
Last of the two nested-component findings from the widget audit. `Handle` was declared in the
render body and passed to a child as `handle={Handle}` - a component *type*. A type that
changes identity makes React unmount and remount the subtree instead of updating it, and this
component re-renders on every frame of a drag as dnd-kit updates `transform`, so the handle was
being torn down and rebuilt continuously while being dragged.

Wrapped in useMemo keyed on the two things it closes over.

Measured result of the whole overlay/remount sweep, per widget, deterministic component and
fiber counts on identical restored data:

  widget                    before              after
  mediaReleases      340 comp / 8387 fib   170 / 8057   (-50% components)
  calendar           171 comp / 1195 fib    75 /  907   (-56% components, -24% fibers)
  beszelSystemTable   36 comp / 1286 fib    18 / 1230   (-50% components)
  downloads / mediaRequests / app        unchanged - their overlays are functional

Two corrections to earlier claims in this branch:

  - The media-releases win came from the shared OverflowBadge fix, NOT from the tooltip change.
    Measured on the build with only the tooltip fix, mediaReleases was still at 340; it dropped
    to 170 once OverflowBadge stopped mounting a Popover when nothing overflows. The tooltip fix
    remains zero on this board because every release has a description.
  - The board-level "event listeners" metric is unusable. Across six runs of identical code it
    read 7905, 8000, 8166, 8179, 10360 and 17114. Chrome's Nodes counter has the same problem
    (23072 on one run against ~8850 on the rest) because it counts detached nodes awaiting
    collection, while "elements under body" stayed at 6691-6726 throughout. Only the per-widget
    component and fiber counts are stable enough to compare, so those are what is reported.
2026-08-11 10:09:59 +02:00
Thomas Camlong 2534103627 perf(ui): stop mounting Popover machinery for overlays that cannot open
Two more instances of the same Mantine behaviour, found by searching for overlay components
that take a `disabled` prop rather than by measuring another board.

OverflowBadge (packages/ui) mounted a Popover and a Popover.Dropdown unconditionally - both
when `disablePopover` was set and, more importantly, when nothing overflowed at all, which is
the common case. It is shared UI, used by media-releases, releases, the docker table and the
widget inputs, so the cost repeated per row wherever a list renders it. The rendered markup is
unchanged: a Popover.Target only forwards a ref, so returning the badges directly is
visually identical.

beszel disk-usage mounted a HoverCard per table row with `disabled={filesystems.length === 0}`,
and already fell back to a plain progress bar inside its own target for that case - so for a
system reporting no extra filesystems the entire overlay existed to render nothing. Hoisted
that branch out; the now-unreachable ternary and the dead `disabled` prop went with it.

Measured result of this class of fix, on the calendar which had the worst ratio:

  interactive components  171 -> 75   (-56%)
  fibers                 1195 -> 907  (-24%)

Honest note on the media-releases tooltip fix committed earlier: it measured **zero** on the
test board. Every release there has a description and the option is on, so all 80 tooltips are
legitimately enabled and none are skipped. It only helps boards where descriptions are absent
or the option is off. The mechanism is right; this board simply is not the case it helps.

Also confirmed NOT worth changing: downloads mounts 30 Tooltips but all three per row are
functional (integration name, hover detail, progress), so there is nothing disabled to skip.
Replacing them with native title attributes would change the UX, not just the cost.
2026-08-11 10:04:12 +02:00
Thomas Camlong 365b2ed13e perf(calendar): stop mounting a HoverCard per day cell that can never open
Third instance of the same pattern, and the worst ratio yet. Mantine mounts a HoverCard's
Popover machinery - Popover, PopoverTarget and PopoverDropdown - even when it is disabled.
A month grid is around 42 cells and `disabled` is `isEditMode || eventsForDate.length === 0`,
so on a typical month most days mounted three components for a card that can never open, and
in edit mode every single day did.

Measured on a real board: 42 Popovers, 42 PopoverDropdowns and 42 PopoverTargets - 126 of
this widget's 171 listener-attaching components - in a widget with only 251 DOM nodes. That
is a far worse components-per-node ratio than media-releases, which had 340 across 2,951
nodes.

Returning the day cell directly when disabled is behaviour-identical: a disabled HoverCard
never opens, so nothing observable is lost.

This is now the third place this exact Mantine behaviour has cost real work - after the
shared board context menu and the media-release tooltips. The general rule: Mantine overlay
components mount their full machinery regardless of `disabled`, so `disabled` is a
visual-suppression prop, not a cost-avoidance one. Deciding before rendering is what avoids
the cost.
2026-08-11 09:58:13 +02:00
Thomas Camlong 7e961d3da4 perf(beszel-system-table): hoist PercentCell out of the render body
A static audit of every widget (scripts/benchmarks/audit-widget-render.mjs, added here)
found this: PercentCell was declared inside the widget body, which makes it a new component
type on every render. React cannot match a new type to the previous one, so it unmounted and
remounted every cell's subtree - Group, Text and Progress - instead of updating it. The
widget re-renders about once a second from its live Beszel subscription, and the cell is used
by three columns, so that was three subtrees per system torn down and rebuilt every second.

Hoisted to module scope and wrapped in memo. `size` is now passed as a prop; it was the only
thing the nested version closed over.

The audit covers all widget .tsx files and ranks findings by whether they sit inside a
.map(), since those multiply by row count:

    29  inline-style-per-row
    12  overlay-per-row
    11  index-key
     2  nested-component

Deliberately not fixing the other 52 yet. The 29 inline styles are the least likely to
matter - a fresh object per row only defeats memoisation if the child is memoised, and mostly
these are not - so changing 29 call sites would be churn without a measurement to justify it.
The 11 index keys are only harmful where a list reorders, which needs checking per widget. The
remaining overlay-per-row sites are the same pattern already fixed twice (context menus, then
media-release tooltips) and are the most likely to be worth it: calendar mounts 42 Popovers
and downloads 30 Tooltips, both confirmed at runtime.

Not measured: the mechanism is certain but the size depends on how often the widget renders
and how many systems are in the table.
2026-08-11 09:52:37 +02:00
Thomas Camlong f5b7818c53 perf(media-releases): stop mounting a Popover per card for tooltips that never show
Attributing listener-attaching components per widget found the same anti-pattern the
shared board context menu already fixed once, still present in list widgets:

  interact   nodes  fibers  kind / which components
       340    2951    8387  mediaReleases
                            UnstyledButton x90  TooltipFloating x80  Popover x80
                            PopoverDropdown x80  PopoverTarget x10
       171     251    1195  calendar
                            UnstyledButton x45  Popover x42  PopoverDropdown x42
                            PopoverTarget x42
        46     264     898  downloads
                            Tooltip x30

Mantine mounts a TooltipFloating's Popover machinery even when the tooltip is disabled, so
wrapping every card unconditionally produced 80 TooltipFloating, 80 Popover and 80
PopoverDropdown components - 240 of this widget's 340 listener-attaching components - for
tooltips that only show on hover, on cards that are 91% off-screen.

Deciding before rendering means a card with no description, or with the option off, mounts
none of it. Behaviour is unchanged for cards that do have a description.

Also extends measure-widget-cost.mts to count these components per widget, which is what
found it. The board's 8,165 event listeners were never attributable before; now they are.

Not yet measured: the listener reduction depends on how many releases carry a description,
which is integration data. The mechanism is certain (the components are simply not created)
but the size of the win on a given board is not, and calendar's 42 Popovers and downloads'
30 Tooltips are untouched. Tracked in docs/perf/NEXT-STEPS.md.
2026-08-11 09:42:59 +02:00
Thomas Camlong fee2e6a6af docs(perf): track which off-screen applications are measured and which are not
Four commits now apply the off-screen row treatment; only the first was A/B'd. Records
which is which, and warns how to read a result for the three widgets that do not overdraw
on the test backup - a measurement there will correctly show no change, because the benefit
only exists once an integration returns more rows than the tile can show.

Also records two things that look like gaps and are not:

  - content-visibility was applied to five of the 26 ScrollArea widgets on purpose. It
    costs containment work and can be wrong on self-measuring content such as charts and
    the notebook, so the rule was: apply where list length is decided by remote data.
  - React cache() is server-only. It already wraps getBoardByNameAsync, getHomeBoardAsync,
    getRscServerSettingsAsync and getQueryClient; it does nothing for client widget
    rendering, where react-query does the caching and its gcTime was already verified at
    5 minutes rather than the 24 hours the original fix catalogue assumed.
2026-08-11 09:38:59 +02:00
Thomas Camlong 040f105946 perf(widgets): share the off-screen row primitive, and apply it to the data-driven lists
Extracts the media-releases fix into packages/widgets/src/common/offscreen-rows.ts and
applies it to the remaining widgets whose list length is decided by remote data rather
than by the tile: rssFeed, notifications and the calendar event list. Images in those
lists also get loading="lazy" - the rss cards carry two each, a backdrop and a poster.

The measured basis, from media-releases where the pattern was found:

  image requests   135 -> 55    (-59%)
  layout objects  8346 -> 5117  (-41%)

Scope was chosen deliberately rather than applied everywhere. 26 widgets use ScrollArea,
but on the measured board only two rendered more than their tile could show, and
content-visibility is not free: it costs a little containment work, and on content that
measures itself - charts, editors - skipping layout can be actively wrong. The widgets
changed here are the ones where overdraw is a function of how much data an integration
returns, so a user with a busier instance hits it even though this board does not. Chart
and fixed-size widgets are deliberately left alone.

Honest scope note: unlike the media-releases change, these three are NOT measured. On the
test board rssFeed renders 7 nodes and the calendar's 7 rows fit their tile, so there is
nothing to improve here to observe - the benefit only appears with more data than this
backup has. They are typechecked and lint-clean. docs/perf/NEXT-STEPS.md tracks them
alongside the media-requests change as pending verification.

A duplicate `style` prop on the notifications Card was caught by typecheck and merged
rather than left to override the existing colour and text-decoration styles.
2026-08-11 09:37:48 +02:00
Thomas Camlong 8f94ea47e3 docs(perf): add a handoff with the unverified item, the remaining lever, and the clean-PR set
Records where this stopped so it can be picked up cleanly:

  1. c526dd2b7 (media-requests content-visibility) is typechecked but NOT measured - the
     only performance claim on this branch not A/B'd before commit. Verify with
     measure-board-paint.mts, three repeats; revert if image requests do not fall.
  2. The one lever with headroom left is the tRPC router's eager integration imports - a
     16.10 MiB subtree of 460 files reached by every API route, which pays twice because
     both the ~41 MiB of retained source and the ~38 MiB of V8 compiler zones scale with
     how much JavaScript the server compiles. Every allocator and V8-flag avenue is closed.
  3. Deferred with prizes attached: ASCII-escaping bundles (9.45 MiB, needs its own PR),
     lazy mysql2/pg (21.4 MiB, structurally blocked), beszelSystemStats chart SVG.
  4. The proposed set for the clean PR, six changes producing -26% peak anon and -52% at
     idle for +18% CPU - with the gRPC patch flagged as hygiene rather than a measured win,
     and the +30% board-load figure flagged as not established.
2026-08-10 19:12:28 +02:00
Thomas Camlong c526dd2b77 perf(media-requests): apply the off-screen fix to the request list — NOT YET MEASURED
Second instance of the pattern fixed in mediaReleases. The overdraw tool, corrected to
find row containers by child count rather than by uniform height, found only one other
widget genuinely rendering more than its tile can show:

  nodes  rows  vis  offscr  imgs   tile  content  kind
   2951    80    7    2693    80    539     7206  mediaReleases            (already fixed)
    393    20    6     275    60    405     1512  mediaRequests-requestList

1,512 px of cards in a 405 px tile, and 20 cards carrying 60 images between them - each
card renders a full-tile backdrop plus a poster. Same treatment: content-visibility with
contain-intrinsic-size on the card, loading="lazy" on both images.

IMPORTANT: this is typechecked but **not measured**. Every other performance claim in this
branch was verified by an A/B before being committed; this one was not, because the session
ended first. It is committed so the work is not lost, and it is the first thing to verify
next - the same measurement used for mediaReleases applies:

  STRESS_BASE_URL=http://127.0.0.1:PORT node --experimental-strip-types \
    scripts/benchmarks/measure-board-paint.mts

Expect image requests to fall; do not expect JS heap to move, since decoded bitmaps live
outside JSHeapUsedSize.

Every other widget on the board fits its tile and needs nothing. The tool's "visible"
column misreads chart widgets (beszelSystemStats reports 28 rows / 0 visible because its
container is an SVG whose groups have no layout height) - content height versus tile height
is the trustworthy signal, and by that measure only these two overdraw.
2026-08-10 19:11:38 +02:00
Thomas Camlong c73b139dbd docs(perf): split the findings into per-subject markdown files
PHASE2-FINDINGS.md had grown past 1,200 lines mixing server memory, client rendering,
methodology and a long list of rejected experiments, which made it unusable as a
reference. Split under docs/perf/:

  README.md              headline results and the change-by-change ratio table
  SERVER-MEMORY.md       what the container's RAM is made of, layer by layer
  CLIENT-PERFORMANCE.md  dashboard cost in Chrome: DOM, fibers, images, paint
  REJECTED.md            ideas implemented, measured and abandoned, with numbers
  METHODOLOGY.md         the tools, and the measurement traps that gave wrong answers

PHASE2-FINDINGS.md is now an index so existing links keep working.

METHODOLOGY.md is the part worth keeping: every wrong number in this investigation came
from an instrument rather than the application, and each trap is written down - snapshots
cannot explain peaks, V8 truncates snapshot strings to ~1 KiB, snapshot objects are not
live handles, measuring without a forced GC measures V8's growth, taking a CDP snapshot
added 134 MiB to the process being measured, per-component React attribution silently
returns nothing on a production build, a fixed port can measure a leftover container, and
a server-side change that moves a client-side metric means the workload changed.
2026-08-10 19:08:20 +02:00
Thomas Camlong 7550766f67 perf(media-releases): stop rendering 73 off-screen cards' images and layout
Per-widget attribution of the dashboard found one widget responsible for 44% of the
board's DOM: mediaReleases at 2,951 nodes and 8,387 fibers, including 1,935 Mantine
Box components, against a board total of 9,293 nodes.

The cause is that it maps over every release the integrations return, with no cap.
Measured on a real board: the tile is 539 px tall and its content is 7,206 px, so of
80 rendered cards only 7 are visible. The other 73 cost DOM, fibers, and - most
expensively - a poster and a CSS backdrop image each, all fetched and decoded for
pixels nobody can see. Decoded bitmaps are far larger in memory than their files.

Fixed with content-visibility: auto plus contain-intrinsic-size on each card, so the
browser skips layout, paint, raster and background-image fetch for off-screen cards,
and loading="lazy" on the poster. Nothing leaves the DOM, so scrolling and in-page
search are unchanged, and the intrinsic size keeps the scrollbar from jumping.

Measured over three repeats per side on identical restored data:

  image requests    135, 135, 135  ->  55, 55, 56   (-59%, deterministic)
  image bytes            12.27 MiB  ->  8.41 MiB    (-31%)
  layout objects  8346, 7815, 7815  ->  5117, 4696, 4636  (-41%)

Two things this does NOT show, stated because a single run suggested otherwise:

  - JS heap did not clearly improve. Across four runs it was 46-88 MiB before against
    74-86 MiB after; the before spread is 2x, so no claim either way. This is expected:
    decoded image memory lives outside JSHeapUsedSize, so the actual saving from 81
    fewer image fetches is invisible to that metric.
  - Task duration and time-to-settled overlap between arms and support no conclusion.

Adds the tools this needed: measure-widget-cost.mts (per-widget DOM nodes, fibers and
heaviest components), measure-board-paint.mts (image requests, bytes, style/layout/task
time, layout objects), measure-widget-overdraw.mjs (rendered versus visible rows).
2026-08-10 19:05:17 +02:00
Thomas Camlong 5fb8e9223a docs(findings): the 38 MiB of compiler zones is V8's optimiser, and not reachable by flags
Closes the largest unexplained item in the peak breakdown, then closes it off as a lever.

Attribution: peak_malloced_memory goes 4.5 -> 42.1 MiB the moment a board's modules
compile, while current malloced_memory stays at 1.0 - memory V8 takes to compile, frees
back to malloc, and musl never returns to the kernel. Confirmed by re-reading the
--jitless runs, where the spike never occurs at all (4.5 MiB at every stage).

--jitless is unusable because it under-renders the page, but V8 can cap the optimisation
tier instead of removing codegen. --max-opt is rejected by NODE_OPTIONS and accepted on
the command line, so it was wired behind HOMARR_MAX_OPT and A/B'd on one image:

  peak anon      276.0 -> 282.8 MiB  (+2%)
  boot idle       64.0 ->  72.3 MiB  (+13%)
  board loaded   186.1 -> 179.2 MiB  (-4%)
  4 tabs         270.8 -> 238.5 MiB  (-12%)
  after soak     199.0 -> 214.3 MiB  (+8%)
  CPU             28.1 ->  27.8 s    (unchanged)

Null result. Render integrity was verified explicitly this time - listeners 7,784 in all
four runs, DOM nodes within 40 - so unlike jitless the page rendered fully and the
comparison is valid.

The mechanism explains it: measured under --max-opt=2, peak_malloced_memory at board load
is 42.1 MiB, identical to Turbofan. Maglev's zones are just as large. Capping the tier does
not reduce zone memory; only not compiling does, and that breaks rendering.

So this 38 MiB is not reachable through V8 configuration. It scales with how much
JavaScript V8 must compile, which returns it to the single remaining server-side lever:
ship less server code. Reverted.
2026-08-10 18:43:31 +02:00
Thomas Camlong dc46057544 docs(findings): definitive server-RAM A/B, and jemalloc rejected with evidence
Two things this settles.

First, the end-to-end number. Every Phase 2 result so far was an incremental A/B against
the build immediately before it, which cannot be summed. Rebuilt the merge-base
(06724cfeb) from a clean worktree and ran 2+2 alternated against HEAD, reporting cgroup
anonymous memory - the figure that counts against a container limit and cannot be
reclaimed:

  stage              before     after
  peak anon        376.6 ->   279.3 MiB  (-26%)
  boot idle        130.5 ->    62.4 MiB  (-52%)
  after sign-in    247.4 ->   142.9 MiB  (-42%)
  dashboard        293.4 ->   170.4 MiB  (-42%)
  4 tabs           366.5 ->   258.2 MiB  (-30%)
  after soak       323.7 ->   200.8 MiB  (-38%)
  peak node RSS    430.7 ->   336.2 MiB  (-22%)

No before/after range overlaps at any stage. Costs: CPU 23.5 -> 27.7s (+18%, solid,
non-overlapping). Median board load reads +30% but is NOT established - individual loads
were [506,383,1233] and [848,431,574] before against [780,808,515] and [621,579,1004]
after, heavily overlapping with no cold-then-warm pattern, so six samples per side cannot
separate a real regression from the variance.

compare-stress-runs.mjs now leads with anonMiB, which the harness recorded all along but
the report was ignoring in favour of memory.current - a figure that includes page cache
and swings 60+ MiB between runs of a byte-identical image.

Second, jemalloc. The largest attributed-but-unfixed item is ~38 MiB of V8 zone memory
that musl frees to its own pools and never returns to the kernel, so an allocator that
decays spans back to the OS looked like the answer. Tested on the byte-identical image
with only HOMARR_MALLOC differing, asserting the "Using jemalloc" log line:

  boot idle    62.4 ->  80.3 MiB  (+29%)
  after soak  215.5 -> 279.6 MiB  (+30%)
  4 tabs      259.9 -> 319.6 MiB  (+23%)
  CPU          28.0 ->  24.5 s    (-13%)

The hypothesis was backwards. jemalloc keeps more freed memory on purpose - thread caches
plus time-based dirty-page decay - and spends it on speed. 13% cheaper CPU, 18-30% more
resident memory. Reverted, and it closes off "swap the allocator" as a RAM direction:
musl is already the frugal choice here.
2026-08-10 18:25:00 +02:00
Thomas Camlong f5a9933dd8 docs(findings): the Beszel coalescing fix measured zero, and was reverted
Second correction in the same area. Having found that three Beszel widgets account for
82% of idle DOM churn, the obvious fix was to batch their subscription updates instead
of calling setState per record. Implemented, built, measured:

  DOM mutations / 45s   733 -> 748
  React commits / 45s    55 ->  55

Identical. Beszel emits roughly one record per second and the flush interval was also
one second, so each flush batched exactly one record. Coalescing at the arrival rate
does nothing - arithmetic that should have come before the implementation.

That also exposes a unit error in the previous commit's numbers: DOM mutations are not
re-renders. The real rates are 1.22 React commits/sec for the whole board and 13.3 DOM
mutations per commit, because a chart redraw touches many nodes. The stats widget's
"10.4 mutations/sec" is ~0.8 renders/sec - three live-monitoring widgets each refreshing
about once a second, which is what they exist to do.

So 73 commits/min is not pathological, it is the feature working, and there is no defect
to fix here. Reverted rather than shipped: keeping speculative complexity that measured
zero - an extra timer plus unmount and system-change teardown - is the trade this work
rejects everywhere else.

What survives is narrower: idle churn is widget-driven rather than app-shell driven, and
legitimate. The 650 MB browser heap therefore comes from interaction and multi-tab use,
not idle churn, and remains unexplained.
2026-08-10 17:52:23 +02:00
Thomas Camlong 9f1a15b9bf docs(findings): the App Router churn claim was wrong; idle churn is Beszel
Corrects the largest wrong claim in this work. Earlier rounds reported that ~80% of
idle React commits entered at Next's internal HeadManagerContext and that "the App
Router root re-renders about once a second", and named it the highest-value next step.

Both are false. Measuring the same idle churn against boards of different sizes:

  board      items   idle commits/min
  hey            0                0.0
  footbol        3                0.0
  default       28               76.0

A 3-item board commits zero times per minute. If the router re-rendered on a timer,
every board would churn regardless of its contents.

The original attribution was a tooling artefact. measure-rerenders.mts decides a fiber
re-rendered from fiber.actualDuration, which only a React *profiling* build populates.
On a production build every fiber fails that test and both attribution tables come back
empty - and an empty table reads as "nothing re-rendered" rather than "this build cannot
tell you". The harness now says which case it is, and waits for the board container to be
attached rather than visible so a zero-item board can be measured at all - that control
is what disproved the claim.

Adds measure-widget-churn.mts, which attributes churn on a production build via DOM
mutations keyed to the nearest [data-type="item"] ancestor. On the 28-item board, idle
for 45s:

  470  beszelSystemStats      64%
   77  beszelSystemTable      11%
   52  beszelSystemGrid        7%
   22  dockerContainers        3%
   93  (outside any widget)   13%

Three Beszel widgets are 82% of all idle churn, one of them 64% on its own at 10.4
mutations per second.

Also logs what Next.js itself costs, since it was assumed to be the problem: its loaded
source is a flat 3.47 MiB (364 framework files + 32 vendored) and does not grow at all
between boot and a fully loaded multi-tab session, while Homarr's own chunks grow from
3.73 to 15.44 MiB. Next is 16.1 MiB on disk; the largest things it vendors are its
next-server runtime (1.60), @edge-runtime (0.85), edge-runtime (0.51) and compression
(0.30). The framework is not where this application's memory goes.
2026-08-10 17:45:12 +02:00
Thomas Camlong ab5b8dad43 docs(findings): attribute the native bucket at peak, and record two disproven theories
Closes the last unexplained part of the peak footprint. The 118.8 MiB 'other
anonymous' smaps bucket is now mostly accounted for: V8 large-object space 34.5,
external buffers 24.6, and ~38 MiB of V8 parser/compiler zone memory that was freed
back to malloc but never returned to the kernel by musl - peak_malloced_memory goes
4.6 -> 42.2 MiB at board load while current malloced_memory stays at 1.0.

Two theories tested and rejected rather than asserted:

  - 'Fewer loaded files means less compiler zone memory, so shipping less server JS
    pays twice.' Wrong. peak_malloced_memory is identical (42.1-42.5) across the
    preloadEntriesOnStart A/B despite 310 fewer files. The spike is a fixed cost of
    the board route, not a per-file one. Claim withdrawn.
  - Orphaned zlib streams. Next documents this leak class and the fix is absent from
    this build, and a peak snapshot showed 15 Gzip objects still present at idle -
    which read as accumulation. getActiveResourcesInfo() reports Zlib=0 both at
    baseline and after 300 deliberately aborted gzip requests, RSS flat. Snapshot
    objects are not live native handles, and nginx buffers responses so Next never
    sees the mid-response disconnect that orphans a stream.

Adds a /handles endpoint to the probe, which is what settled the second one.
2026-08-10 09:37:10 +02:00
Thomas Camlong e687c1dd22 docs(findings): full peak breakdown, the preloading correction, and jitless rejected
Records the Phase 2 peak work:

  - Corrects the earlier claim that Next preloading all 74 route graphs is
    'framework behaviour, not something this codebase sets'. Next documents
    experimental.preloadEntriesOnStart for exactly this, found by reading its
    memory-usage guide rather than by inference. Includes the deterministic
    per-stage table: 310 files and 6.3 MiB of source stay unloaded even after a
    full multi-tab session, so the cost is not deferred, it is never paid.
  - Rejects --jitless with evidence. Code space really does go 11.6 -> 0.0 MiB and
    peak RSS looked 24% better, but the flag was applied to the server while the
    browser's listener count fell 80% and board load got faster, which is
    impossible. The page under-rendered; the win was an app doing less work. The
    tell generalises: a server-side change moving a client-side metric means the
    workload changed and the comparison is void.
  - Notes that --optimize-for-size, --no-opt and --lite-mode are rejected outright
    by Node in NODE_OPTIONS, and that the rest of Next's memory guide covers
    build-time memory under Webpack, which does not apply to this Turbopack build.
2026-08-10 09:18:55 +02:00
Thomas Camlong bcea4733b9 perf(nextjs): stop preloading every route's module graph at startup
Next loads the module graph for all 74 app routes when the server starts. Measured
here: 1,114 files and 24 MiB of source resident before the first request, including
one shared 16 MiB subtree behind the API routes that reaches every integration
client. A dashboard serves a handful of routes per session.

`experimental.preloadEntriesOnStart: false` is Next's documented switch for this.
An earlier round of this work recorded route preloading as "framework behaviour, not
something this codebase sets" - that was wrong, and the findings doc is corrected.

Deterministic per-stage measurements, identical across both runs on each side:

  stage            source loaded      files        heapUsed
  boot idle       24.0 -> 10.6 MiB   1114 -> 706   70.7 -> 44.8  (-37%)
  post-login      27.9 -> 19.3 MiB   1215 -> 884   123.8 -> 109.8
  board loaded    29.0 -> 21.7 MiB   1323 -> 1006  (run-to-run noise)
  multi-tab       29.2 -> 22.9 MiB   1325 -> 1015  139.7 -> 127.8
  post-soak       29.2 -> 22.9 MiB   1325 -> 1015  142.5 -> 127.4

The important part is the last two rows: 310 files and 6.3 MiB of source stay
unloaded even after a full multi-tab session, so this is not merely deferred cost -
routes the session never visits are never paid for at all. Live heap is lower at
every stage, by 26 MiB at idle and 12-15 MiB under load and at rest.

Peak node RSS does not move (350.3 -> 346.1, spreads overlap). V8 sizes its heap
against available headroom, so a smaller live set does not shrink peak RSS; what
improves is idle footprint and how much room is left before the heap cap. Median
board load is +3% with overlapping spreads, so the first-hit cost this trades for
did not show at this workload.

Full E2E suite passes 8/8 against the built image.
2026-08-10 09:07:16 +02:00
Thomas Camlong 8594cc884f perf(docker): stop loading the gRPC stack dockerode only needs for image builds
dockerode/lib/docker.js required './session' at module scope, which pulls in
@grpc/grpc-js and @grpc/proto-loader - 112 files. Its single call site is inside
Docker.prototype.buildImage, the BuildKit path. Homarr lists, starts and stops
containers and never builds images, so every install with a Docker widget paid for
a gRPC stack it cannot reach.

Measured with a forced GC, in context rather than in a bare process: requiring
dockerode goes from +26.6 MiB RSS / 5.2 MiB heap to +20.6 MiB / 2.7 MiB, with grpc
and protobufjs files going 63 and 49 to zero.

The bare-process figure was +24.1 MiB, and quoting that would have overstated the
win by 4x - most of it overlaps with what dockerode loads anyway. The patch comment
records the in-context number.
2026-08-10 08:49:02 +02:00
Thomas Camlong 1de51a9afd docs(findings): record the DB-driver chop as measured, attempted, and backed out
mysql2 (77 files) and pg (13 files) load on every SQLite install because
db/drivers/index.ts imports all three wrappers statically. Re-measured with a
forced GC between readings, the prize is 21.4 MiB RSS - not the ~30 MiB the naive
sum implied, and drizzle-orm's 124.3 MiB in the require-cost sweep was almost
entirely that same no-GC artefact.

Three attempts, each caught by booting the real image rather than by typecheck:
createRequire(import.meta.url) is undefined in the CommonJS migrate.cjs bundle and
broke startup for every dialect; a dynamic require resolves to a .cjs build Next
never traces; and leaving drizzle's dialect static defeats the purpose because
drizzle-orm/mysql2 itself imports mysql2.

The two remaining routes are worse than the problem - a second copy of drizzle's
core alongside the ESM one, or hardcoding drizzle's internal file layout so an
upgrade breaks startup for MySQL and Postgres. Backed out; the structural blocker
is that createDb must stay synchronous.
2026-08-10 08:43:45 +02:00
Thomas Camlong 4a7da0bdab fix(benchmarks): split the allocation-site key explicitly instead of with replace
CodeQL flagged `key.replace("|", ...)` as js/incomplete-sanitization (high): it
replaces only the first occurrence. Here that was intentional - the key is
`functionName|url:line` and only the first separator matters - but written as a
replace it reads as a broken sanitiser, and it would mangle any URL containing a
pipe. Splitting on indexOf says what it means. Path trimming now uses replaceAll,
which is what was meant there too.
2026-08-09 22:24:46 +02:00
Thomas Camlong 3cc6ff33df perf(docker): stop loading ssh2, which allocates a 16 MiB WASM heap nobody uses
Profiling at the memory high-water mark - not at idle, which is what every earlier
round measured - found the largest single retainer in every snapshot was one V8
Context holding a 16.00 MiB ArrayBuffer. Expanding it through the dominator tree
named it: ssh2's crypto module, whose POLY1305_WASM_MODULE instantiates a
WebAssembly memory for ChaCha20-Poly1305 as soon as it is required.

Homarr has no SSH feature and does not depend on ssh2. The chain is
dockerode -> docker-modem -> ssh2, and docker-modem/lib/modem.js requires './ssh'
at module scope while using it in exactly one place, already guarded by
`protocol === 'ssh'`. A pnpm patch moves the require inside that branch. SSH-based
Docker hosts still work - require() is cached, so they pay it once - and every
socket-based install, which is everyone else, never loads it.

Measured over four alternated runs on a restored real backup, external memory:

                     before        after
  board loaded    25.4, 25.1    8.3, 11.2
  nav-01          28.5, 25.4   10.1,  8.6
  multi-tab       31.1, 35.8   13.0, 10.0
  post-soak       21.4, 21.5    5.5,  7.2

Consistently -15 to -18 MiB, ranges non-overlapping, and 30 fewer files loaded
(1470 -> 1440).

Peak RSS does NOT improve, and that is worth stating plainly: V8 sizes its heap
against available headroom, so freeing native memory lets it grow further before
collecting. What changes is the composition - a fixed 16 MiB of unusable WASM
buffer becomes headroom the app can actually use, which matters under a container
memory limit but does not move the peak number.

Also adds the instruments this needed. A heap snapshot can never explain a peak,
because V8 runs a full GC before serialising one and reports only survivors:

  - memory-probe.cjs gains in-process allocation sampling, on-demand snapshots via
    v8.writeHeapSnapshot, an smaps endpoint, and require-graph recording.
  - analyze-allocations.mjs diffs two stages to isolate what one stage allocated.
  - analyze-smaps-stages.mjs splits each stage's RSS into JS heap, JIT, native and
    file-backed pages.
  - analyze-heap-dominators.mjs can now expand the largest retainer, which is what
    identified ssh2. A plain graph walk escapes through __proto__ and is useless.

One trap recorded in the findings: taking the peak snapshot over CDP delivers it as
JS string chunks and added ~134 MiB of ArrayBuffers to the process being measured,
producing a fake 693 MiB reading. v8.writeHeapSnapshot streams from C++ instead, and
the probe now reports its own observerCostMiB so the cost of looking is visible.
2026-08-09 22:17:25 +02:00
Thomas Camlong 3bc25509cf docs(findings): record what the real Umami instance revealed about the page ceiling
The live site has >100k events for one event name, so the paged path logs that its
counts are a lower bound. The previous code had the identical 100k ceiling - the
46.3 MiB body was that ceiling being hit - but truncated in silence. The ceiling is
unchanged; it is now visible.
2026-08-09 21:42:13 +02:00
Thomas Camlong 4766653979 chore(benchmarks): record the server require graph, and use it to attribute boot-time code
A flat module cache says 24 MiB of source is resident before the first request, but not
what pulled any of it in. Recording parent -> child as the CommonJS loader resolves it
answers that, and the answer redirects the remaining 'ship less server JS' work:

  - Next loads all 74 app route bundles at startup, not lazily.
  - One 16.10 MiB / 460-file subtree is reached by the tRPC/API routes AND by
    instrumentation, so instrumentation's exclusive cost is 0 bytes - deferring it would
    free nothing, because Next preloads /api/[...trpc] anyway.
  - Only 1.10 MiB is common to every route, so this is the API surface pulling in every
    integration, not a universal barrel.
2026-08-09 21:39:28 +02:00
ajnart 3c56eab58f style: auto-fix lint and format 2026-08-09 19:36:22 +00:00
Thomas Camlong cb3a2a360e perf(umami): stop buffering 100k raw events per widget refresh
Profiling the container end to end found one allocation site responsible for
most of the peak: a single 46.3 MiB JSON body, assembled from 3,946 socket
chunks, inside undici's fullyReadBody. That function holds the chunk list and
the joined copy at the same instant, so a body of size N costs about 2N.

The source was two Umami calls asking for pageSize=100000, with
getMultiEventTimeSeriesAsync running them for every tracked event name
concurrently via Promise.all. getEventNamesAsync was downloading every raw
event of the last 30 days purely to collect a handful of distinct name strings.

  - getEventNamesAsync now reads the aggregated /metrics?type=event endpoint.
  - getWebsiteEventTimeSeriesAsync pages 5,000 records at a time, folding each
    page into the bucket map before requesting the next.
  - getMultiEventTimeSeriesAsync runs series sequentially.

Instances that return aggregated {x,y} buckets still answer in one request, an
instance that accepts pageSize but ignores page is detected rather than
double-counted, and hitting the page ceiling logs instead of silently
undercounting.

Alternated runs, 2 per side, on a restored real backup:

  peak container       498.9 -> 398.5 MiB  (-20%)
  peak node RSS        452.4 -> 353.3 MiB  (-22%)
  peak during 4 tabs   498.5 -> 356.3 MiB  (-29%)
  peak arrayBuffers     95.8 ->   6.6 MiB  (-93%)
  largest allocation    46.4 ->   2.4 MiB  (-95%)

Costs +4% CPU from the extra round trips; board load latency unchanged.
Post-soak steady state and boot idle are unchanged within run-to-run spread.

Adds the attribution tooling this needed: attribute-memory.mts reconciles
cgroup -> processes -> address-space mappings -> V8 spaces -> loaded code,
memory-probe.cjs is an injectable diagnostics preload (never shipped) that
tracks buffer and fetch allocation sites, analyze-heap-dominators.mjs computes
real retained sizes, and compare-stress-runs.mjs reports alternated runs with
their spread.

Also corrects an earlier claim in PHASE2-FINDINGS.md: the reported "7.86 MiB of
duplicate chunk sources" was an artefact of V8 truncating string contents to
~1 KiB in heap snapshots. There are zero byte-identical duplicates among all
3,055 server JS files. The real doubling is character width - one codepoint
above U+00FF forces a whole source string to two bytes per character, which
costs 9.45 MiB across 80 loaded files.

Adds 6 tests; this integration had none.
2026-08-09 21:35:00 +02:00
Thomas Camlong f707f64333 perf: externalise @ioredis/commands, and identify what actually holds the memory
Tested the hypothesis that cached base64 images were bloating TanStack Query and
Redis. They are not, measured on a loaded 4-tab session:

  base64 data-URLs, server heap   0.01 MiB (2 strings)
  base64 blobs, browser heap      0.01 MiB (7 strings)
  Redis total                     2.17 MB
  image bytes in Redis            none - image-proxy stores URL + headers only

What the server heap is actually made of (141 MiB used): 41.4 MiB is bundled
JavaScript source retained as strings, ~29% of the heap. The memory is the code, not
cached data — which is why every cache-tuning idea in the fix catalog measured at
zero, and why serverExternalPackages was the only lever that moved idle memory.

7.86 MiB of that is literal duplicate copies: the same module inlined into several
Turbopack chunks and retained once per chunk. @ioredis/commands' command table was in
10 chunks and resident 6 times (2.07 MiB wasted), so it is now external — ioredis is
already external and owns it as a direct dependency, so it stays traced.

Recorded as bundle hygiene, NOT a memory win: idle 89.7 -> 89.4 MiB is inside noise,
and board-loaded/peak deltas were smaller than the run-to-run spread (one "before" run
read 220.6 MiB against 169-173 for identical code).

got and @octokit/request-error duplicate just as badly but were deliberately left
bundled: they are only transitive dependencies, and a check inside the image showed
got is not traced into node_modules, so externalising it would have turned a bundled
module into a require that resolves to nothing — a possible MODULE_NOT_FOUND for zero
measured gain.

analyze-heap-strings.mjs buckets a snapshot's strings by what they are, which is what
made "37 MiB of strings" actionable instead of just alarming.

Verified: 28/28 widgets render, auth control passes, zero module-not-found in the
container log, typecheck clean. pnpm test 1794/1795 with two different Testcontainers
flakes across runs (pi-hole, sabnzbd); both pass 18/18 in isolation.
2026-08-09 16:07:26 +02:00
Thomas Camlong 933a54ead9 docs(perf): consolidated before/after, plus what is still unverified
All previous numbers were spread across rounds with different instruments and
different workloads. This is one instrument, one workload, alternated, 2 runs per
side, merge-base vs HEAD:

  server idle        141.9 -> 89.7 MiB   (-37%)
  server loaded      257.3 -> 173.0 MiB  (-33%)
  server peak        705.2 -> 568.4 MiB  (-19%)
  after idle soak    332.0 -> 232.5 MiB  (-30%)
  JS event listeners 10,126 -> 7,774     (-23%)
  React fibers       5,224 -> 3,588      (-31%)
  DOM nodes          9,175 -> 8,284      (-10%)
  browser JS heap    55.2 -> 51.1 MiB    (-7%)
  image              410 -> 398 MB       (-3%)
  container CPU      22.8 -> 22.8 s      unchanged

Recorded prominently rather than buried: under *sustained* 4-tab load the two builds
converge, 368 vs 364 MiB. The wins are idle, steady state and peak. That is why
hammering the app with five tabs did not feel 30% lighter — at saturation the
footprint is live widget data and render churn, which this PR does not change.

Also documents the gaps, since "tested" without them would be misleading: no
multi-hour soak (the #5301 reports are about days), single user, SQLite only, the
DevTools Profiler tab confirmed at bundle level rather than in the extension, and
the App Router re-rendering ~1x/second still unfixed.

verify-context-menu.mts now also covers touch long-press, and no longer asserts on a
fiber total — that counter swung 7,454 to 16,149 between runs of the same build, so
it was noise. probe-fiber-tree.mts is the stable counter and is what the -31% above
comes from.
2026-08-09 15:39:41 +02:00
Thomas Camlong 63e65bbc6c perf(common): stop useTimeAgo re-rendering every second, add re-render profiler
measure-rerenders.mts counts React commits on an untouched board and attributes each
to the shallowest component that re-rendered, i.e. where the update entered the tree.

What it found: an idle board commits 73-96 times per minute, re-rendering
@mantine/core/Box over 30,000 times in 60s. That is the churn behind the 650 MB
browser peak — transient allocation rather than retention. About 80% of those commits
enter at the same anonymous depth-2 provider, props [value, children], value {appDir}:
Next.js's internal HeadManagerContext, whose value is a fresh object literal on every
App Router render, so each render re-renders the whole app beneath it. The rest enter
at __next_metadata_boundary__ / <title>, pointing the same way.

The trigger is inside Next's router/metadata handling — application code has no
polling router.refresh() — so nothing here fixes it. It is the next thing to chase and
it dominates everything downstream of it.

useTimeAgo called setState on a 1s interval even when the label was unchanged ("3
minutes ago" holds for a full minute), re-rendering its subtree ~60x/minute per
consumer for an identical string. It now compares before setting. Recorded as a
correctness fix and NOT a measured win: idle commits stayed inside their 73-96 range
afterwards, because the app-root churn dominates.

pnpm test 1794/1795 (pre-existing pi-hole Testcontainers flake); typecheck clean
across 37 packages; no new lint errors.
2026-08-09 15:18:38 +02:00
ajnart 9bebc28102 style: auto-fix lint and format 2026-08-09 11:40:56 +00:00
Thomas Camlong 605431bc3f fix(perf): actually produce a React profiling build
DevTools still reported "Profiling not supported" because the build was never a
profiling one, despite three settings appearing to enable it. Next applies the React
swap through a webpack alias — getReactProfilingInProduction, mapping
'react-dom/client$' to 'react-dom/profiling' — that Turbopack never sees. A Turbopack
resolveAlias does not catch it either: the vendored entry requires
./cjs/react-dom-client.production.js relatively, not through a bare specifier.

The Dockerfile now performs that same swap on the vendored entry when
HOMARR_PROFILING=true, guarded so it fails the build loudly if React's file layout
changes rather than silently producing a normal build again.

Added `pnpm docker:profiling`, which builds and then verifies, because
`docker build . -t homarr:performance` alone yields a non-profiling image and that is
almost certainly what was being run.

verify-react-profiling.mts checks the bundle rather than the runtime: 2 client chunks
carry React's profiling renderer with the flag, 0 without. It replaces a Playwright
probe that could not tell the two apart — React sends a reduced inject payload to a
synthetic devtools hook, byte-identical for both builds, so that probe reported a
false negative for profiling and production alike. The bundle marker discriminates;
confirm the Profiler tab itself with the real extension.
2026-08-09 13:39:43 +02:00
ajnart 1d09172887 style: auto-fix lint and format 2026-08-09 09:06:26 +00:00
Thomas Camlong b4be884dfc perf(board): one shared context menu instead of one per widget
Mantine's Menu mounts Popover + PopoverDropdown + several context providers even
while closed, so a menu per widget was ~13 fibers each for something almost never
opened. WidgetContextMenuProvider now mounts a single menu for the board, anchored
to the cursor; items keep only a contextmenu/long-press listener.

The dropdown takes the section's gridstack ref as a prop rather than reading
SectionContext — the shared menu lives outside any section, and reading it from
context there crashed the board with "useSectionContext must be used within a
SectionContext".

Verified against both builds with scripts/benchmarks/verify-context-menu.mts
(opens on right-click, shows actions, closes on Escape, re-targets to a second
widget):

  idle fibers          18,670 -> 16,278  (-12.8%)
  fibers after use     +4,191 -> +4
  JS event listeners   10,128 -> 7,774   (-23%)
  DOM nodes             9,189 -> 8,287   (-10%)
  browser JS heap       54.1  -> 52.7 MiB
  median board load      520  -> 479 ms
  container CPU         20.4  -> 20.0 s

Two runs per side, four tabs across two boards. Server memory is unchanged, as
expected for a client-only change. The listener reduction is the point: the 650 MB
session had ~88k listeners and this removes a per-widget source of them.

The gate is validated by passing against unmodified code first. An earlier version
used Playwright's click({ button: "right" }), which emits no contextmenu event
here and so failed shipped code identically — it wrongly condemned a previous
attempt at this optimisation. A gate never run against the un-optimised build
proves only that something changed, not that it still works.

pnpm test 1794/1795 (pre-existing pi-hole Testcontainers flake, 9/9 in isolation);
typecheck clean across 37 packages; 0 lint errors.
2026-08-09 11:05:03 +02:00
Thomas Camlong fd7bcb2231 test(perf): fiber-tree probe, and the client-side diagnosis it produced
probe-fiber-tree.mts walks the live React tree and reports which components are
mounted, what DOM they render and their props. Heap snapshots only give minified
names like `y`, and source maps do not help because the snapshot records runtime
names — props shape plus rendered DOM identifies a component reliably. It injects
a DevTools hook stub before load so React registers every root; climbing from one
DOM node's fiber missed most of the tree (2,093 fibers vs 3,918).

What it found, on a 28-widget board:

- The most-mounted component is @mantine/core/Box (513). It is the primitive every
  Mantine component renders through, so it is a symptom of how much is mounted
  rather than a bug. Proportions match the 650 MB session snapshot (13% vs 17%),
  so the minified `y` there is almost certainly Box.
- 71 Menu -> Popover -> PopoverDropdown triplets are mounted for context menus that
  are almost never opened. Isolated with the existing enableRightClickOnWidgets
  flag: 3,918 fibers with them, 3,554 without — 364 fibers, 9%, scaling with item
  count. The dropdown contents are already lazy; the wrappers are not.

A fix that deferred the Menu until first right-click was reverted: it broke
right-click and inflated the tree to 20,107 fibers, because the replayed
contextmenu event re-entered the arming handler. Doing it properly needs one
board-level menu instead of one per item. The Playwright check that caught this is
in the findings — a fiber count alone would have read as a win.

Also records that the demo account had completed_board_tour = 0, so earlier rounds
measured the first-time-user path; completing it moves the tree by 38 fibers, so
the published numbers stand.
2026-08-09 10:37:33 +02:00
Thomas Camlong 8f18b17207 test(perf): profiling build, container introspection, client-side measurement
The container-only harness was measuring the wrong thing. Real usage — several
tabs, several boards, clicking into widgets — reaches ~880 MB server and a
650 MB *browser* heap, and the browser was never measured at all.

Profiling build: `docker build --build-arg HOMARR_PROFILING=true . -t
homarr:performance`. React's production bundle strips the hooks DevTools needs,
hence "Profiling not supported". Note reactProductionProfiling in next.config is
read only by the webpack path; under Turbopack it comes from `next build
--profile`, and turbo strips any env var not declared in the task's `env`, so all
three had to line up before the flag actually took effect — verified by markers
present only in React's profiling bundle, and static chunks going 20 -> 84 MB
with source maps.

inspect-container.mts attaches to an already-running container (SIGUSR1 opens
Node's inspector — no rebuild, no restart), reports cgroup/per-process/V8 memory
and can pull a heap snapshot out over CDP. The inspected context has no require
and no dynamic import, so it reads globals and uses CDP domains instead.

analyze-heapsnapshot / analyze-react-fibers / analyze-heap-retainers summarise a
snapshot too large for DevTools to open.

Harness now opens STRESS_BOARDS across STRESS_TABS_PER_BOARD tabs, clicks into
widgets, and records browser heap, DOM nodes and listener count via CDP.
Performance.getMetrics silently returns nothing unless the domain is enabled,
which is why the first run reported a 0 MiB client heap.
2026-08-09 08:21:57 +02:00
Thomas Camlong a532f9d377 docs(perf): repetition-backed headline, round-3 result, unpursued levers
Headline moves from single-sample -36% to repetition-backed -34% idle /
-35% loaded (2 before, 3 after, groups non-overlapping).

Round 3 (API-docs SPA lazy-loaded, MCP SDK external) is recorded as NOT a
memory win: every figure lands inside the previous build's spread, so it is
credited only with the deterministic -3 MB image.

Also records the measured cost of the levers not pursued: in-image Redis is
8.1 MiB RSS (the catalog guessed 10-40 MB) and nginx 9.2 MiB RSS / 3.1 PSS,
so neither justifies its deployment risk. next-server is 225.8 of ~236 MiB
at board-loaded, so anything further has to come out of that heap.
2026-08-09 03:21:21 +02:00
Thomas Camlong 61d9faffc5 perf: keep the API-docs SPA and MCP SDK out of the server graph
@scalar/api-reference-react is a documentation SPA reachable only from one
admin tab, and it renders nothing usable on the server, yet it was compiled
into the server graph for an SSR pass whose output is discarded. It is a
client component already, so next/dynamic with ssr: false is allowed in
place and needs no restructuring. @modelcontextprotocol/sdk is only reachable
through /api/mcp/[transport], so it moves to serverExternalPackages.

Honest accounting: this is an image-size and architecture change, not a
measured memory win. Idle landed at 89.4 MiB against 90.3/91.1/100.5 for the
previous build — the better end of that spread, but inside it, so on one
sample it cannot be claimed as a reduction. What is deterministic is image
size, 401 -> 398 MB. Kept because a docs SPA should not be server-rendered
regardless of what the memory number does.

pnpm typecheck and lint clean; pnpm test 1794/1795 with the known
Testcontainers pi-hole flake, which passes 9/9 in isolation.
2026-08-09 03:20:28 +02:00
Thomas Camlong 1965c75356 docs(perf): record round-2 findings and the auth proof
Adds the GC-tuning and wider-unbundling numbers, states the CPU and image-size
costs alongside the memory wins, and documents the three assertions that now
prove the measurement runs behind a real session rather than a public page.
2026-08-09 01:44:19 +02:00
Thomas Camlong 79ea987761 perf: bound V8's young generation and unbundle server-only deps
Two further wins, both measured on a restored 28-widget board behind a real
demo session (scripts/benchmarks/stress-restore.mts).

--max-semi-space-size=4 in run.sh. --max-old-space-size only bounds the old
generation; V8 also sizes the young generation generously (16 MB per
semi-space) and that slack is pure resident memory. 4 MB cut idle anonymous
memory 120.0 -> 97.2 MiB and peak 710.6 -> 560.2 MiB. It costs CPU, as any
GC-space reduction does — 15.8 -> 17.1 CPU-seconds over the run, ~8% — with
no board-load latency regression (median 594 -> 570 ms). Overridable via
HOMARR_MAX_SEMI_SPACE_SIZE.

serverExternalPackages for the remaining server-only dependencies. Bundling
a package inlines its transitive graph into a chunk that is compiled at boot
even when the code path never runs; leaving it external makes it a lazy
require. Adding winston, drizzle-orm, ical.js, jszip, ldapts, node-unifi,
@kubernetes/client-node and linkedom took idle to 90.3 MiB and board-loaded
209.6 -> 164.6 MiB, with slightly lower CPU (16.3s) and latency (549 ms).
The trade-off is image size: 388 -> 401 MB, because those packages now ship
in node_modules instead of being tree-shaken into chunks.

Cumulative vs the merge-base: idle 141.7 -> 90.3 MiB (-36%), board loaded
257.8 -> 164.6 MiB (-36%), under load 392.7 -> 302.5 MiB (-23%).

The harness now proves the session rather than inferring it: it asserts an
anonymous request to the private board is denied, requires the Auth.js
session cookie after sign-in, and reports whether it had to modify the
restored data at all (now "none"). It also records cgroup CPU seconds and
board-load latency, so a memory win bought with CPU cannot hide.

pnpm test 144/144 files, 1795 tests; typecheck and lint clean.
2026-08-09 01:43:06 +02:00
Thomas Camlong 9826000722 docs(perf): record post-trim verification
Removing the three zero-effect changes cost nothing: idle 120.0 MiB,
board loaded 229.5, after stress 357.0 — all within noise of the
untrimmed branch, with the lowest peak of any config measured.
2026-08-09 01:03:59 +02:00
Thomas Camlong b3f58cd8aa perf: drop the cache layers that measured at zero
Ablation (PHASE2-FINDINGS.md) attributed the memory win to the
serverExternalPackages line and the integrations barrel diet. These three
measured at exactly zero and were most of the file churn, so they go:

next-cache-handler: orphaned. cacheHandlers and cacheComponents are both
already gone from next.config.ts and nothing imported the package — it
was reachable only through a stale dependency entry.

cache-tags / cache-invalidation: there is no "use cache", cacheTag,
cacheLife or unstable_cache anywhere in app source, so revalidateTag was
firing on tags nothing registers, with no handler to receive them, inside
a catch that swallowed the resulting out-of-scope error. Removing the
calls also removed the SELECTs that existed only to supply a board name
or member list to them, so board.ts, group.ts, user.ts, serverSettings.ts,
section-router.ts and change-search-preferences.ts return to the
merge-base exactly. The live in-process integration data cache is
untouched.

Redis 6 -> 2 client consolidation: no measurable effect at idle (139.3 vs
139.2 MiB) or under load. Reverted, but the pubSub:last:* TTL is kept —
that one is an unbounded-growth fix, not a consolidation.

23 files, -883 lines. Diff vs merge-base: 100 files/3336 insertions ->
82 files/2484. pnpm test 144/144 files, 1795 tests, 0 failures;
typecheck and lint clean; app and Docker image build.
2026-08-09 00:59:41 +02:00
Thomas Camlong 84dcbd8847 docs(perf): attribute the Phase 1 memory win to specific changes
Measured on a populated instance (real backup restored through the
onboarding UI, 28-widget board open) rather than an empty container, and
compared on anonymous memory because memory.current swung 192-261 MiB
for the same image once page cache was counted.

Branch vs merge-base: -24% idle, -11% board loaded, -8% under load.

Cumulative ablation from the baseline image shows where that comes from:
adding mysql2/pg to serverExternalPackages — one line — accounts for the
entire idle win (-21 MiB), because the driver barrel statically imports
all three drivers and bundling inlined mysql2's whole graph into the
boot-loaded server chunks (108 MB of chunks vs 95 MB, and mysql2 absent
from node_modules because it had been inlined). The integrations barrel
diet carries the under-load win but moves idle by 0. Redis 6->2 clients
and the V8 heap cap both move idle by 0 — the cap is a drift ceiling,
not a reduction, since removing it entirely (cap4096) matched the branch
at every stage.

The cache-invalidation layer and next-cache-handler package measure at
exactly zero: no "use cache", cacheTag, cacheLife or unstable_cache
anywhere in app source, and cacheHandlers/cacheComponents are already
gone from next.config.ts, so 66 revalidateTag call sites across ~30
routers are no-ops. Keep/drop decision left open.
2026-08-09 00:44:50 +02:00
Thomas Camlong fb80b3882d test(benchmarks): compare on anon memory, retry pre-hydration login clicks
Two fixes found by running the harness against a real pair of images:

Login submitted before the form hydrated, so the click was a no-op that
looked identical to bad credentials. Load the page at networkidle and
retry the submit, surfacing any page alert if all attempts fail — the
same race already handled for the onboarding restore toggle.

memory.current counts reclaimable page cache, which moved the "same"
baseline between 192 and 261 MiB across runs. Anonymous memory is now
the headline metric (idle was 144 MiB anon vs 77 MiB of page cache), so
an A/B is not decided by unrelated disk I/O.
2026-08-09 00:44:50 +02:00
Thomas Camlong 6363d96a1b test(benchmarks): fix post-login wait, allow V8 flag injection
Sign-in redirects client-side, so page.waitForURL never reached its
implicit "load" state and every run failed after the restore step even
though authentication had succeeded. Poll window.location instead.

STRESS_NODE_OPTIONS injects NODE_OPTIONS directly, which is what makes
the V8 heap cap attributable: the merge-base run.sh does not read
HOMARR_MAX_OLD_SPACE_SIZE, so the cap can only be A/B'd against that
image by passing the flag in.
2026-08-09 00:44:50 +02:00
Thomas Camlong 0d19968131 test(benchmarks): add populated-dashboard memory stress benchmark
Container memory was only ever measured on an empty install, which hides
the cost that actually matters: a real board with real widgets and
integrations loaded.

stress-restore.mts boots a fresh container, restores a supplied backup
through the onboarding restore UI (including the "I understand"
confirmation), signs in, opens the busiest board, waits for every widget
to mount, then hammers navigation and soaks idle while sampling cgroup
v2 memory at each stage.

The backup is passed by path via STRESS_BACKUP_ZIP and never enters the
repo — it carries real integration secrets and an encryption key.
.gitignore now also refuses homarr-backup*.zip and loose *.sqlite.

Baseline reading (merge-base image, 28-item board, all widgets ready):
223 MiB idle -> 448 MiB board loaded -> 688 MiB peak.
2026-08-09 00:44:50 +02:00
ajnart 9187af8882 style: auto-fix lint and format 2026-08-08 11:02:04 +00:00
Thomas Camlong abec79abe8 fix: address CodeRabbit review — typed mocks, robust benchmark script
- Replace `as any` casts in integration-test-connection.spec.ts with
  Pick<Integration, "testConnectionAsync"> + mockResolvedValue
- build-compare.sh: split local declarations (SC2318), clean .next
  between builds with --force, add EXIT trap for safe stash/cleanup
2026-08-08 13:00:42 +02:00
Thomas Camlong b1713a9820 fix: revert Suspense layout wrappers causing E2E hydration errors
Revert layout.tsx, _layout-creator.tsx, manage/layout.tsx,
_theme.tsx, _creator.tsx, mantine.tsx, and login/page.tsx
to origin/dev — the Suspense provider splits caused
"EditMode is required", stream close, and React #419 errors.

Also remove cacheComponents/cacheHandlers from next.config.ts
(incompatible with auth/i18n/dynamic rendering) and fix
Boolean() truthy bug in Redis connection.
2026-08-08 12:40:48 +02:00
Thomas Camlong 3db87d781f fix: address CodeRabbit review issues
- redis-handler: add TLS_CA and USERNAME env support matching core client
- connection.ts: fix Boolean() truthy bug (CI="false" was disabling Redis)
- channel.ts: add in-memory lock for single-instance fallback mode
- channel-subscription-tracker: lazy-init subscriber client
- board.ts: invalidate user cache on board delete for home-board users
- .env.example: document REDIS_CACHE_DB vs DATABASE_INDEX collision risk
- CLI docs: fix docker exec command (remove /bin/bash prefix)
- Docker docs: fix docker rm -f and docker ps -a for stopped containers
2026-08-08 09:17:39 +02:00
Thomas Camlong 51467ded12 docs: remove obsolete X-Frame-Options ALLOW-FROM examples
ALLOW-FROM is not supported by any modern browser. The CSP
frame-ancestors directive is the correct replacement and was
already present in both examples.
2026-08-08 09:09:53 +02:00
Thomas Camlong 38fc9bfd01 chore: remove full-screen loading spinners from boards and manage 2026-08-08 09:09:19 +02:00
Thomas Camlong 9bc1fc397c chore: remove .cursor/goal files from git 2026-08-08 09:08:48 +02:00
Thomas Camlong 9596a83f60 fix: use any cast for mock integration in test-connection spec
The barrel no longer exports PiHoleIntegrationV6 after the dynamic
import refactor. The mock only needs testConnectionAsync, so a type
cast is sufficient.
2026-08-08 09:03:18 +02:00
Thomas Camlong e036cd86d8 perf: move dependencies to build-time static JSON, add build benchmark
- Move getDependenciesAsync filesystem scan to scripts/dump-dependencies.mts
  (runs at build time, outputs static-data/dependencies.json)
- About page now imports dependencies.json like other static data
  (eliminates runtime glob of all package.json files)
- Delete dead getDependenciesAsync + parseDependencyCatalogsAsync from
  package-reader.ts (only getPackageVersion remains)
- Add scripts/benchmarks/build-compare.sh for dev vs branch comparison
2026-08-08 08:58:54 +02:00
Thomas Camlong 6692acbd33 chore: strip formatting noise, delete dead routes, fix CodeRabbit issues
- Remove 179 format-only file changes (docs, migrations, plex, patchmon, CSS)
- Delete .cursor/goal files from git tracking
- Delete 4 dead /api/about routes (unused, data loaded from static JSON)
- Fix resolveAlias to use absolute paths (CodeRabbit)
- Fix Tailwind classes in Mantine app layout fallback (CodeRabbit)
- Return stale data during cache revalidation (CodeRabbit)
2026-08-08 08:45:16 +02:00
Thomas Camlong 95a7d56989 fix: pi-hole test import from barrel to direct path
The barrel no longer exports PiHoleIntegrationV5/V6 after the
dynamic integration import refactor.
2026-08-08 00:41:50 +02:00
Thomas Camlong 7bd5991076 ci: trigger CI run 2026-08-08 00:29:48 +02:00
Thomas Camlong 345a4be4b0 fix: CI build errors + simplify cache handler and data cache
- Remove `export const dynamic` from 4 routes (incompatible with cacheComponents)
- Static import for redis-handler (Turbopack can't resolve relative dynamic imports)
- Simplify redis-handler: Buffer.concat, NonNullable, single connect promise
- Inline single-use helpers in integration-data-cache
- Non-greedy SVG regex to prevent ReDoS
- Inline getDevelopmentServiceAliases in next.config
- Remove restating comments
2026-08-08 00:19:27 +02:00
Thomas Camlong 6f563640ac fix: remove as const from svgSanitizeOptions to fix DOMPurify type compat 2026-08-07 22:12:09 +02:00
Thomas Camlong 4270f2f2f1 fix: address CodeRabbit review — stream tee, cache guard, base64, layout fallback
- BoardLayoutFallback: use HomarrLogoWithTitle (no board context required)
- integration-data-cache: guard cache.set with placeholder identity to
  prevent stale write-back after invalidation + test
- next-cache-handler: memoize the promise (not resolved handler) to
  prevent concurrent Redis connections
- redis-handler: tee ReadableStream before consuming in set(), hand fresh
  branch to each get() caller
- redis-handler: base64-encode body instead of number[] (3-4x smaller)
2026-08-07 22:09:11 +02:00
ajnart 00855d4825 style: auto-fix lint and format 2026-08-07 19:44:19 +00:00
Thomas Camlong 39334569d6 fix: remove notifications from refetch-intervals to match widget definitions
The notifications widget has no refetchInterval in its definition,
so the static polling policy entry was causing a manifest test failure.
2026-08-07 21:42:08 +02:00
Thomas Camlong ffed73ba8f merge: incorporate latest dev changes into phase1-perf branch
Merge origin/dev to pick up service isolation (concurrently dev script,
turbopack aliases, instrumentation simplification), PostHog analytics,
color scheme, and other dev improvements while preserving all Phase 1
performance work (Suspense boundaries, cache components, Redis handler,
integration barrel diet, cache invalidation).
2026-08-07 21:36:48 +02:00
Thomas Camlong 90d9a50227 fix: correct Content-Type for content-detected SVG responses
When looksLikeSvg detects SVG content under a non-SVG MIME type,
set Content-Type to image/svg+xml so browsers render correctly
with X-Content-Type-Options: nosniff.
2026-08-07 21:25:07 +02:00
Thomas Camlong e836d07d4c fix: content-based SVG detection, guard tests, document REDIS_CACHE_DB
- Detect SVG by content (magic bytes) in addition to MIME type to prevent
  sanitization bypass via mislabeled uploads
- Add guard test verifying DOMPurify actually strips dirty payloads
- Add looksLikeSvg detection test
- Document REDIS_CACHE_DB in .env.example
2026-08-07 21:22:25 +02:00
Thomas Camlong 33293ba1cf refactor: simplify Phase 1 infrastructure code
- cache-invalidation: extract isExpectedRevalidateError helper
- integration-data-cache: flatten early-return chain, extract eviction
- redis-handler: extract StoredEntry type, readStreamBody, ensureConnected
- svg-purify: single getOwnPropertyDescriptor lookup per prop
- session-scoped-providers: simplify auth flow
- retryable-loader: use function declarations
- translation/request: replace spread-in-reduce with for loop
2026-08-07 21:04:42 +02:00
Thomas Camlong b3b2d5a022 fix: SVG regex prefers </svg> terminator, add self-closing children test
Use greedy <svg>...</svg> match preferring closing tag over inner />
to prevent truncation at first self-closing child element.
Self-closing root <svg .../> handled as separate alternative.
Added test with <path/> + <circle/> children asserting well-formed output.
2026-08-07 21:00:55 +02:00
Thomas Camlong 67ac2bbc7c fix: Redis expire guard, transferOwnership prev-owner, SVG regex
- Redis get() rejects legacy expire=0 entries and cleans them up
- transferOwnership invalidates previous owner in addition to new
- SVG regex uses non-greedy match and handles self-closing <svg />
- SVG fallback returns empty string instead of HTML wrapper
- Memory channel comment: FIFO eviction, not timestamp-based
2026-08-07 20:57:07 +02:00
Thomas Camlong fbb2eddd87 fix: concurrent stale race, Redis expire leak, missing invalidation, UI Suspense
- Fix integration data cache: set pending synchronously before creating
  fetch to prevent concurrent stale-revalidation from spawning duplicate
  upstream calls
- Redis cache handler: skip persisting entries with expire=0 (dynamic
  entries should not be stored in Redis)
- Add missing invalidation: user.changePassword, group.updateGroup,
  group.transferOwnership
- Move SearchEngineOptimization inside Suspense in root layout
- Add Suspense fallback loader to root layout
- Move searchParams resolution inside Suspense in login page
2026-08-07 20:53:36 +02:00
Thomas Camlong 38ac8f3075 fix: concurrent cache miss, SVG output, narrowed error handling, format
- Fix integration data cache: check pending promise before TTL to
  prevent returning undefined on concurrent cold-cache access
- Add concurrent-miss test (two parallel callers, one fetch)
- SVG sanitizer: extract <svg> from DOMPurify output to strip
  residual HTML wrapper
- Narrow safeRevalidateTag catch: log unexpected errors, silence only
  known "outside request scope" messages
- Run oxfmt across all packages (206 files formatted)
2026-08-07 20:48:17 +02:00
Thomas Camlong b81b90875f fix: revert DB driver to static imports, fix test regressions
- Revert createRequire() for DB drivers — require() can't resolve .ts
  files in Vitest, breaking 49 test suites
- Make cache invalidation fail-safe via try-catch around revalidateTag
  (throws outside Next.js context: tests, tasks service, websocket)
- Fix 6 integration test imports: barrel no longer exports concrete
  classes, point tests at direct source paths
- Fix Redis cache handler lint errors: remove non-null assertions,
  suppress __NEXT_BUILD_ID underscore warning
2026-08-07 20:43:07 +02:00
Thomas Camlong 67fcd69eb8 fix: inverted tag expiration in Redis handler + saveItemOptions board name
- Fix critical bug: tag expiration comparison was inverted (< should be >)
  in Redis cache handler, which would prevent revalidateTag from clearing
  stale entries
- Pass board name to invalidateBoardCache in saveItemOptions
- Add clarifying comment about two-tier invalidation helpers
2026-08-07 19:30:58 +02:00
Thomas Camlong aeabfd3521 fix: requestMedia dual-layer cache invalidation
Mirror answerRequest pattern: invalidate both module-level integration
data cache and request-handler caches for media request list/stats.
2026-08-07 19:27:41 +02:00
Thomas Camlong 37930e44cc perf: clean integration barrel — remove 46 concrete class re-exports
- Remove all concrete integration class re-exports from barrel
- Replace instanceof check with kind check in health-monitoring
- Document cache invalidation as preparatory (cacheTag follow-up)
2026-08-07 19:22:32 +02:00
Thomas Camlong 451dc3ce39 fix: address review #4 — board name invalidation, SVG tests, test gaps
- Pass board.name to all invalidateBoardCache calls (permission mutations)
- Align integration create invalidation with update/delete
- Add initial-fetch-failure test for integration-data-cache
- Expand SVG XSS test corpus (javascript:, xlink:href, @import, onbegin)
- Forbid <style> tags in SVG sanitizer to prevent CSS injection
- Fix duplicate export in packages/common/src/index.ts
2026-08-07 19:18:18 +02:00
Thomas Camlong 4361c8dbe2 fix: address Bugbot review #3 — mutation invalidation gaps
- Dual-layer cache invalidation on interact mutations
  (dns-hole, media-requests, smart-home, indexer-manager, anchor-notes)
- importOldmarrConfig: invalidate created boards
- createBoard: invalidateUserCache when auto-setting homeBoardId
- group.savePositions: invalidate user caches for affected members
- integration.requestMedia: invalidate integration data cache
- user.delete: invalidate user cache
2026-08-07 19:14:02 +02:00
Thomas Camlong 0f8362c3da fix: address Bugbot review #2 findings
- Invalidate integration data cache from widget interact mutations
  (downloads, dns-hole, media-requests, indexer-manager, smart-home, anchor-notes)
- Redis handler getExpiration: fetch missing tags from Redis (multi-instance coherence)
- saveItemOptions: invalidate board cache after saving
- Remove unused cacheTags.app
- Fix integration data cache comment accuracy
2026-08-07 19:08:57 +02:00
Thomas Camlong 6b45aca5d5 fix: address Bugbot review findings
- Skip module-level integration cache for multi-instance (REDIS_IS_EXTERNAL)
- Add invalidateBoardCache to permission mutations (prevents future leak)
- Remove unused cacheTags.integrationData
- Replace KEYS with SCAN in Redis cache handler refreshTags()
- Document REDIS_CACHE_DB env var
2026-08-07 19:04:53 +02:00
Thomas Camlong 018f98e318 perf: implement Phase 1 homepage diet via Next.js Cache Components
P1-0: V8 heap cap (HOMARR_MAX_OLD_SPACE_SIZE, default 512MB)
P1-1: Module graph cuts
  - Dynamic integration imports (55 static → lazy loaders)
  - Conditional DB driver loading (only configured driver loaded)
  - Replace jsdom with linkedom for SVG sanitization
  - Delete eager widget barrel (55 import * as → 0)
  - Extract createRetryableLoader to @homarr/common
P1-2: Enable cacheComponents + restructure root layout
  - Suspense boundaries around session-dependent providers
  - ColorSchemeScript replaces server-side cookie reads
  - Board/manage/auth layouts wrapped in Suspense
P1-3: Cache read paths
  - Cache tag registry + invalidation helpers
  - Cache invalidation wired into all mutation routers
  - Translation deep-merge memoized per locale
P1-4: Prerendered shells + loading.tsx files
P1-5: Shared integration data cache (60s TTL, stale-while-revalidate)
P1-6: Redis-backed Next.js cache handler (@homarr/next-cache-handler)
P1-7: Redis optional (memory channel fallback), TTL on pubSub:last:*,
      Redis clients consolidated from 6 to 2
2026-08-07 18:59:47 +02:00
Thomas Camlong 366ec6bdbc fix custom widget e2e login race 2026-08-03 15:52:08 +02:00
Thomas Camlong 9f4b147f0e fix legacy media server option mapping 2026-08-03 15:29:27 +02:00
Thomas Camlong 08af704c8a Merge origin/dev into feat/build-performance 2026-08-03 15:21:23 +02:00
Thomas Camlong 3f9fb215c6 fix: avoid ambiguous benchmark regex 2026-08-03 14:27:21 +02:00
Thomas Camlong de801f3f00 perf: optimize dashboard loading and builds 2026-08-02 15:58:16 +02:00
ajnart d90e762e4b fix: align Navidrome media sessions 2026-07-31 13:52:39 +02:00
ajnart 81a13d3b52 Merge remote-tracking branch 'origin/dev' into feat/build-performance 2026-07-31 13:38:16 +02:00
ajnart 388a74086e fix: address Copilot cache review 2026-07-31 13:32:48 +02:00
ajnart cd6c83a7b0 test: guard LDAP user lookup 2026-07-30 00:50:02 +02:00
ajnart 9963ba0274 fix: harden runtime isolation and regression tests 2026-07-30 00:38:34 +02:00
ajnart 5d9f05e083 test(e2e): open spotlight with hotkey 2026-07-29 07:53:35 +02:00
ajnart d48f299585 test(e2e): wait for authenticated board 2026-07-29 07:42:57 +02:00
ajnart c80e9878de fix: handle lazy startup failures 2026-07-29 07:23:09 +02:00
ajnart 84f87ce24e fix(ci): bound affected task concurrency 2026-07-29 07:11:26 +02:00
ajnart fbb57c8f40 fix(docker): build runtime workspaces only 2026-07-29 06:57:06 +02:00
ajnart 0b9c4d6375 fix: address performance review feedback 2026-07-29 06:50:13 +02:00
ajnart 7266e15356 style: auto-fix lint and format 2026-07-29 06:47:07 +02:00
ajnart c22c733463 build(docker): simplify manifest copy layers 2026-07-29 06:47:07 +02:00
ajnart 440626ba00 test(performance): add authenticated local benchmarks 2026-07-29 06:47:07 +02:00
ajnart f8d8a09c56 perf(proxy): isolate settings database reads 2026-07-29 06:47:07 +02:00
ajnart add6ee56ba fix(updates): enforce a persisted daily check 2026-07-29 06:47:07 +02:00
ajnart 6b74078766 perf(widgets): lazy load the application graph 2026-07-29 06:47:07 +02:00
ajnart 4ca0b747bb chore(next): upgrade preview and isolate dev services 2026-07-29 06:47:07 +02:00
824 changed files with 34000 additions and 72061 deletions

No files matched your search

@@ -1,28 +0,0 @@
---
name: homarr-custom-widget
description: Author safe Homarr Custom JSX v2 widgets from API documentation.
---
# Homarr Custom Widget
Author one widget at a time. When connected to Homarr, retrieve `homarr://custom-widgets/schema` and only the component resources needed for the design. The live resources are authoritative for the installed release.
1. Read the API documentation.
2. Create one credential-free widget with keyed `sources`, keyed `requests`, optional keyed `options`, and safe JSX `template`.
3. Validate, preview, test queries and simulated actions, visually inspect, and iterate.
4. Configure deployment-specific source URLs and request credentials through Homarr; never repeat plaintext.
5. Save only after narrow/wide, light/dark, loading, empty, error, and success states work.
Use `{option:name}` or `$option` for saved options. Use `{param:name}` or `$param` for values supplied by `SubFetch`, `ActionButton`, or `ToggleSwitch`. Load queries cannot use invocation parameters. Templates read `data`, `status`, `options`, and temporary `inputs`.
`SubFetch` with `trigger="manual"` renders its own load button. Pass a card or image through `triggerContent` with `triggerAriaLabel` when that content should launch the request. Its callback is `(result, meta)`; never author `onClick` or a fetch callback.
Use standard Mantine compound names and deliberate visual hierarchy, spacing, restrained semantic color, and responsive layouts. Avoid excessive nested cards.
Do not use imports, hooks, refs, raw HTML, raw event callbacks, browser requests, arbitrary functions, eval, bigint, npm packages, authored statement blocks, IIFEs, or recursion. Do not pretend MCP tools exist in an offline chat session.
To install an existing widget, use `customWidget_workshopSearch`, inspect it with `customWidget_workshopGet`, then use `customWidget_workshopInstall`. Configure each self-hosted source afterward with `customWidget_sourceConfigure`, or call `customWidget_configurationRequestUser` so the user can enter the server URL and credentials directly in Homarr.
Preview-scoped configuration expires with the preview. To persist user-entered values for a new widget, create the widget first, request configuration for its definition ID, then create a final preview with that definition ID so Homarr loads the stored credentials.
When this skill is installed from the repository, optional offline details are in `references/schema.md`, `references/runtime.md`, and `references/security.md`. MCP-only clients should use the live Homarr resources instead.
@@ -1,16 +0,0 @@
# Runtime
Templates read `data.requestId`, `status.requestId`, `options.name`, and temporary `inputs.name`. Status contains `loading`, `ok`, `status`, `statusText`, and `error`.
`bind="search"` creates an in-memory input. It is never persisted. Supply invocation values only through `params`, for example:
```jsx
<TextInput bind="search" label="Search" />
<SubFetch requestId="search" params={{ query: inputs.search }}>
{(items) => <Stack>{(items ?? []).map(item => <Text key={item.id}>{item.name}</Text>)}</Stack>}
</SubFetch>
```
`SubFetch` with `trigger="manual"` renders its own load button. To make a card or image launch the request, pass that node through `triggerContent` and provide `triggerAriaLabel`; Homarr supplies the click and keyboard behavior. Its optional second callback argument contains `{ ok, status, statusText, loading: false }`; loading and request failures are rendered by `SubFetch` before the callback runs. Do not author `onClick` or a fetch callback.
Use expression callbacks for supported collection methods and trusted slots. Do not use callback blocks, IIFEs, authored recursion, or raw events. Bounded regex literals work only with safe string matching/replacement operations.
@@ -1,22 +0,0 @@
# Schema
```ts
interface HomarrCustomWidgetV2 {
$schema: "homarr-custom-widget-v2";
name: string;
description?: string;
iconUrl?: string;
sources: Record<string, CustomWidgetSource>;
requests: Record<string, CustomWidgetRequest>;
options?: Record<string, CustomWidgetOption>;
template: string;
}
```
Sources require `default`. A request defaults to the default source, query, GET, load, inherited auth, and view permission. An action defaults to manual and modify permission. DELETE uses full permission and confirmation.
Use stable real URLs for public APIs and clear suggested URLs for self-hosted services. Homarr collects the installer's server URL, network scope, and credentials as source setup; credentials remain outside the manifest.
Paths use `{option:name}` and `{param:name}`. Query/body data uses `{ "$option": "name" }` and `{ "$param": "name" }`. Load queries cannot use params. Runtime parameter names and primitive values are inferred from references.
Every option has `label`, `control`, and `default`. Optional fields are `description`, `choices`, `choicesFrom`, `min`, `max`, `step`, `advanced`, and `group`.
@@ -1,7 +0,0 @@
# Security
All requests use Homarr's protected server executor. Source origin, network scope, DNS, redirects, SSRF, rate limits, permissions, size limits, timeouts, and encrypted credential injection remain enforced.
The JSX interpreter blocks imports, hooks, refs, raw event callbacks, browser requests, eval, arbitrary functions, prototype access, unsafe URLs, global CSS escape, arbitrary portals, bigint, statement blocks, IIFEs, and recursion. Regex literals must be bounded and reject backreferences, lookbehind, nested quantifiers, excessive length, and unsupported flags.
Credentials are stored separately and never exported or returned to an agent. A published self-hosted source URL is only a suggestion: installers must confirm or replace private and loopback URLs for their own Homarr deployment. Source origins cannot be controlled through widget options.
+5 -22
View File
@@ -55,36 +55,19 @@ TURBO_TELEMETRY_DISABLED=1
# Example: DOCKER_SOCKET_PATHS=/var/run/docker.sock,/run/user/1000/podman/podman.sock
# DOCKER_SOCKET_PATHS=
# Redis DB index for the Next.js cache handler (default: 1).
# Must differ from REDIS_DATABASE_INDEX (default: 0) to avoid cache ops affecting app data.
# REDIS_CACHE_DB=1
# Enable kubernetes tool
# ENABLE_KUBERNETES=true
# Enable mock integration
UNSAFE_ENABLE_MOCK_INTEGRATION=true
# Public URL contract. Values must be absolute HTTP(S) URLs without credentials,
# query strings, or fragments. Trailing slashes are normalized at startup.
HOMARR_WEBSITE_URL=https://homarr.dev
# PocketBase API used by Homarr and by the standalone documentation site.
# Defaults to HOMARR_WEBSITE_URL.
# WORKSHOP_API_URL=https://homarr.dev
# Public Workshop page opened from Homarr. Defaults to HOMARR_WEBSITE_URL/workshop.
# WORKSHOP_WEB_URL=https://homarr.dev/workshop
# Documentation and PocketBase deployment settings.
# WORKSHOP_PUBLIC_ORIGIN=https://homarr.dev
# PB_ALLOWED_ORIGINS=*
# Local Workshop stack:
# HOMARR_WEBSITE_URL=http://127.0.0.1:3003
# WORKSHOP_API_URL=http://127.0.0.1:8090
# WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
# WORKSHOP_PUBLIC_ORIGIN=http://127.0.0.1:3003
# Optional: GITHUB_CLIENT_ID=
# Optional: GITHUB_CLIENT_SECRET=
# Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration)
# DEMO_MODE=true
# Whether demo mode is read-only. Defaults to true (the public demo blocks all mutations).
# Set to false for writable preview deployments so the demo user can modify the dashboard.
# DEMO_READ_ONLY=false
# DEMO_READ_ONLY=false
+2 -2
View File
@@ -16,6 +16,6 @@
**When contributing a new integration or widget:**
- [ ] I confirmed manually that the integration or widget is working with a real system and performed basic smoke
tests (e.g. what happens if it is offline)
tests (e.g. what happens if it is offline)
- [ ] Optional but recommended: I confirm that I am willing to be added to the `CODEOWNERS` of this feature and will
provide support and maintain the integration (e.g. in case of breaking or failure)
provide support and maintain the integration (e.g. in case of breaking or failure)
+21 -9
View File
@@ -1,4 +1,4 @@
name: Formatting
name: Auto-fix lint and format
on:
pull_request:
@@ -6,30 +6,42 @@ on:
branches: ["*"]
permissions:
contents: read
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
format:
name: Check formatting
autofix:
name: Auto-fix
if: github.event.pull_request.head.repo.id == github.repository_id
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
ref: ${{ github.head_ref }}
- name: Setup
uses: ./tooling/github/setup
- name: Check changed files
- name: Get changed files
id: changed
env:
BASE_REF: ${{ github.base_ref }}
run: |
git diff --name-only --diff-filter=ACMR -z "origin/${BASE_REF}...HEAD" -- \
'*.css' '*.js' '*.json' '*.jsonc' '*.jsx' '*.md' '*.mdx' '*.scss' '*.ts' '*.tsx' '*.yaml' '*.yml' \
| xargs -0 -r pnpm exec oxfmt --check --
git diff --name-only --diff-filter=ACMR "origin/${BASE_REF}...HEAD" -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files
echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT
- name: Auto-fix lint + format
if: steps.changed.outputs.count != '0'
run: |
xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true
xargs -r -a /tmp/changed-files pnpm exec oxfmt --write
- name: Commit fixes
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "style: auto-fix lint and format"
+18 -161
View File
@@ -8,7 +8,7 @@ on:
types: [closed]
branches: ["*"]
push:
branches: [main, dev, beta, next, v2]
branches: [main, dev, beta, next]
merge_group:
workflow_dispatch:
inputs:
@@ -34,10 +34,8 @@ permissions:
contents: read
concurrency:
# Every event for a PR shares one group so a close event cancels stale image
# publication and a newer synchronize run cannot be overwritten by an older run.
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' || github.ref != 'refs/heads/main' }}
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
env:
FORCE_COLOR: 3
@@ -89,17 +87,6 @@ jobs:
- name: Validate workspace configuration
run: pnpm lint:ws
- name: Validate Custom Widget architecture
run: pnpm check:custom-widgets
- name: Validate checked-in OpenAPI schema
run: pnpm check:openapi
- name: Validate Custom Widget bundle budget
run: |
pnpm --filter nextjs build
pnpm check:custom-widget-bundle
- name: Run unit tests
run: pnpm test
@@ -115,8 +102,6 @@ jobs:
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
@@ -199,166 +184,38 @@ jobs:
path: e2e/test-results/
retention-days: 7
- name: Log in to GHCR for preview publication
- name: Determine preview tag
id: preview
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
run: echo "tag=pr-${PR_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Log in to GHCR
if: steps.preview.outputs.tag != ''
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push amd64 preview image by digest
id: preview-build
if: |
!env.ACT &&
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
uses: docker/build-push-action@v7
with:
context: .
network: host
platforms: linux/amd64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-amd64
secrets: |
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 }}
- name: Push preview image
if: steps.preview.outputs.tag != ''
env:
PREVIEW_TAG: ${{ steps.preview.outputs.tag }}
run: |
docker tag homarr-ci "${PREVIEW_IMAGE}:${PREVIEW_TAG}"
docker push "${PREVIEW_IMAGE}:${PREVIEW_TAG}"
- name: Show Act publication result
if: env.ACT
run: echo "Act validated the publication path; external GHCR writes are disabled locally."
preview-arm64:
name: Build arm64 preview image
needs: fast-gate
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.preview-build.outputs.digest }}
steps:
- uses: actions/checkout@v7
if: "!env.ACT"
with:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
- name: Log in to GHCR
if: "!env.ACT"
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Build and push arm64 preview image by digest
id: preview-build
if: "!env.ACT"
uses: docker/build-push-action@v7
with:
context: .
network: host
platforms: linux/arm64
outputs: type=image,"name=${{ env.PREVIEW_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=homarr-linux-arm64
cache-to: type=gha,scope=homarr-linux-arm64,mode=max
secrets: |
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 }}
publish-preview:
name: Publish multi-platform preview image
needs: [container, preview-arm64]
if: |
(
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' && inputs.operation == 'publish')
)
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
packages: write
pull-requests: read
env:
PREVIEW_TAG: pr-${{ github.event.pull_request.number || inputs.pr_number }}
steps:
- name: Log in to GHCR
if: "!env.ACT"
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: "!env.ACT"
uses: docker/setup-buildx-action@v4
- name: Verify PR head before publication
if: github.event_name == 'pull_request'
uses: actions/github-script@v8
env:
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
with:
script: |
const { data: pull } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
if (pull.state !== "open") {
core.setFailed(`PR #${pull.number} is ${pull.state}; refusing to publish its preview.`);
return;
}
if (pull.head.sha !== process.env.EXPECTED_HEAD_SHA) {
core.setFailed(`PR #${pull.number} moved from ${process.env.EXPECTED_HEAD_SHA} to ${pull.head.sha}.`);
}
- name: Publish preview manifest
if: "!env.ACT"
env:
AMD64_DIGEST: ${{ needs.container.outputs.digest }}
ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }}
run: |
docker buildx imagetools create -t "${PREVIEW_IMAGE}:${PREVIEW_TAG}" \
"${PREVIEW_IMAGE}@${AMD64_DIGEST}" \
"${PREVIEW_IMAGE}@${ARM64_DIGEST}"
- name: Verify preview manifest platforms
if: "!env.ACT"
run: |
image="${PREVIEW_IMAGE}:${PREVIEW_TAG}"
manifest="$(docker buildx imagetools inspect "$image" --raw)"
for architecture in amd64 arm64; do
jq --exit-status --arg architecture "$architecture" \
'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \
<<<"$manifest" >/dev/null
done
docker buildx imagetools inspect "$image"
cleanup:
name: Clean up preview image
if: github.event_name == 'pull_request_target' || (github.event_name == 'workflow_dispatch' && inputs.operation == 'cleanup')
-63
View File
@@ -1,63 +0,0 @@
name: Workshop
on:
pull_request:
push:
branches: [dev, v2, main]
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24.18.0
- uses: pnpm/action-setup@v6
with:
run_install: false
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @homarr/workshop typecheck
- run: pnpm --filter @homarr/docs typecheck
- run: docker compose --env-file apps/workshop/.env.staging.example -f apps/workshop/docker-compose.staging.yml config --quiet
- run: pnpm test:workshop
- run: pnpm test:workshop-image
publish:
if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'main')
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v6
id: metadata
with:
images: ghcr.io/homarr-labs/workshop
tags: |
type=sha,prefix=sha-,format=long
type=raw,value=v2,enable=${{ github.ref_name == 'v2' }}
type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
- uses: docker/build-push-action@v7
with:
context: .
file: apps/workshop/Dockerfile
target: production
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
+9 -6
View File
@@ -16,7 +16,6 @@ out/
next-env.d.ts
# artifacts
.ai-evaluations/
packages/db/migrations/*/migrate.cjs
# nitro
@@ -72,11 +71,7 @@ apps/nextjs/public/images/background.png
# personal agent config
.opencode
.agents/*
!.agents/skills/
.agents/skills/*
!.agents/skills/homarr-custom-widget/
!.agents/skills/homarr-custom-widget/**
.agents/
skills-lock.json
# docusaurus
@@ -84,3 +79,11 @@ apps/docs/.docusaurus
apps/docs/build
.pnpm-store
benchmark-results
.bench/
# Homarr backups used by scripts/benchmarks/stress-restore.mts. These contain
# real integration secrets and an encryption key — never commit one.
homarr-backup*.zip
*.sqlite
*.sqlite-wal
*.sqlite-shm
@@ -1,116 +0,0 @@
---
target: Workshop store in the docs
total_score: 20
p0_count: 1
p1_count: 3
timestamp: 2026-07-21T11-32-50Z
slug: apps-docs-src-components-workshop
---
# Workshop Store Critique
## Design Health Score
| # | Heuristic | Score | Key issue |
| --------- | ----------------------------------- | --------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of system status | 1/4 | Fetch failure can show beside an empty-store message, so the system communicates two incompatible states. |
| 2 | Match between system and real world | 3/4 | Widget, CSS, voting, revisions, and comments use familiar language. |
| 3 | User control and freedom | 2/4 | Filters are reversible, but the crashed route and failed fetch state offer no useful recovery. |
| 4 | Consistency and standards | 2/4 | The submission editor uses CodeMirror, while revision editing falls back to a textarea; several tiny custom controls do not follow the shared button vocabulary. |
| 5 | Error prevention | 2/4 | Submission validation exists, but disabled progression does not explain what is missing and destructive actions use a brief inline confirmation. |
| 6 | Recognition rather than recall | 3/4 | Labels, type choices, and the submission stepper are understandable without documentation. |
| 7 | Flexibility and efficiency | 2/4 | Search covers titles only, signed-out users can select Yours, and installation is reduced to ambiguous Copy and Download actions. |
| 8 | Aesthetic and minimalist design | 3/4 | The restrained visual system is clean, but the browse toolbar and card action rows become dense. |
| 9 | Help users recover from errors | 0/4 | The production route crashes with React error 130; the fetch error has no retry action or useful context. |
| 10 | Help and documentation | 2/4 | The editor explains its fields, but the store does not teach the full inspect, trust, download, and install journey. |
| **Total** | | **20/40** | **Needs work before release** |
## Anti-Patterns Verdict
The surface does not look like obvious AI-generated decoration. Its visual restraint, familiar controls, and practical editor are credible product UI. The weakness is product behavior rather than styling: contradictory states, tiny action controls, an overloaded responsive toolbar, and a primary installation journey that is not explicit.
The deterministic Impeccable scan returned zero findings for `apps/docs/src/components/workshop`. This is useful but incomplete. The detector did not catch runtime failures, semantic state conflicts, touch-target sizing, or responsive grouping. Browser evidence found a full route crash, React error 130, and an earlier state that rendered both `Something went wrong.` and `No submissions yet`.
No visual overlay was injected because the available browser evaluation surface is read-only. Screenshots, DOM snapshots, console logs, and source inspection were used instead.
## Overall Impression
The submission modal is now one of the stronger parts of the Workshop. The store around it is not release-ready. The single biggest opportunity is to make discovery and installation a dependable, explicit path: load reliably, show one truthful state, help users evaluate a submission, and give them one obvious next action.
## What Is Working
1. The visual system is restrained and consistent with the Homarr documentation. Typography, color, radii, and spacing do not compete with the content.
2. The submission flow chunks a complex task into Type, Details, and Media. The shared JSON/CSS editor, drag-and-drop import, metadata autofill, and capped media previews reduce creator effort.
3. The detail design exposes revision, author, reports, changelog, source content, and comments. This supports inspection rather than treating community code as a blind install.
## Priority Issues
### P0: The Workshop route crashes in the production docs build
**Why it matters:** No visual polish matters if the store becomes a Docusaurus error page. A fresh independent tab consistently reached `This page crashed` with React error 130. The console attributed the thrown error to the Kapa widget bundle.
**Fix:** Isolate or disable the Kapa script on Workshop routes, prevent third-party widget failures from reaching the Docusaurus route boundary, and add a route-level Workshop boundary that preserves navigation and retry. Reproduce against the built Docker docs image, not only the development server.
**Suggested command:** `$impeccable harden`
### P1: Failure and empty states contradict each other
**Why it matters:** Showing `Something went wrong.` followed by `No submissions yet` makes users believe the store is both broken and empty. The generic message gives no cause and no recovery.
**Fix:** Make loading, failure, empty, and results mutually exclusive. The failure state should say that listings could not be loaded, preserve the current filters, and provide `Try loading again`. Do not render filters or an empty store as authoritative data after the initial request fails.
**Suggested command:** `$impeccable harden`
### P1: Mobile discovery controls are too dense and do not form a responsive hierarchy
**Why it matters:** The toolbar's inner row contains four type choices, sorting, and outdated visibility without wrapping. Most controls are 24 to 28 pixels tall. At narrow widths this is difficult to scan, easy to clip, and below a comfortable touch target.
**Fix:** Put a full-width search first on mobile, keep All, Widgets, and CSS as the primary segmented choice, move sorting and outdated visibility into a compact secondary row or popover, and hide Yours when signed out or make it a sign-in affordance. Use at least 40 to 44 pixel touch targets. Add `aria-pressed` or proper tab semantics to the type filter.
**Suggested command:** `$impeccable adapt`
### P1: The store does not make the install journey obvious
**Why it matters:** A first-time user sees Copy and Download without knowing which is recommended, what will be copied, how to inspect safety, or where the file goes in Homarr. This weakens the store's main purpose.
**Fix:** Make the detail page the decision surface. Use one primary action named for its result, such as `Download widget JSON` or `Copy CSS`, with a short `Install in Homarr` explanation linked to the correct in-app flow. Cards should prioritize title, preview, compatibility, score, and `View details`; secondary utilities should not compete with evaluation.
**Suggested command:** `$impeccable clarify`
### P2: Card and moderation actions are too small and too numerous
**Why it matters:** Vote, copy, download, comment, delete, and report controls compete in a compact footer. Low-contrast icon buttons are hard to discover and hard to use by touch. CSS columns also create a top-to-bottom visual reading order that can differ from the expected row order.
**Fix:** Use a regular responsive grid, remove hover scaling, keep one or two visible card actions, and move owner/moderation actions to a menu. Keep voting visible but increase its target size. Correct the report copy: reports and reporter identities are moderation data, not public social proof.
**Suggested command:** `$impeccable distill`
## Persona Red Flags
### Jordan, first-time self-hoster
Jordan opens Workshop to install a widget. The route can crash before any listing appears. In the partial failure state, Jordan sees both an error and an empty store. If listings load, Copy and Download do not explain the Homarr destination or recommended path. Jordan is likely to leave before installing anything.
### Alex, experienced Homarr administrator
Alex can understand the JSON and appreciates the source viewer, but title-only search and limited filtering make a larger catalog inefficient. Compatibility and required secret information are not prominent at browse time. Tiny moderation and owner controls slow repeated management tasks.
### Morgan, mobile user
Morgan encounters a toolbar with more controls than fit comfortably in one decision group, 24 to 28 pixel targets, and a signed-out Yours filter that leads to an unhelpful no-results state. Submission cards add another dense row of small actions.
## Minor Observations
- Future submission steps are focusable buttons even when clicking them does nothing. Disable them or render non-interactive progress markers.
- The report textarea relies on placeholder text and needs a persistent accessible label.
- The revision editor should reuse the same JSON/CSS CodeMirror component as creation.
- Search should include title, description, and author at minimum.
- The timed inline `Confirm?` deletion pattern is easy to miss and weak for keyboard and touch users.
- The detail-page changelog uses an uppercase tracked label, which is unnecessary in this otherwise restrained interface.
- Loading comments should use the same skeleton vocabulary as listings rather than centered status text.
## Questions to Consider
1. Is Workshop primarily a catalog for discovery, or a technical exchange for copying source? The primary card and detail actions should commit to one answer.
2. Should users evaluate compatibility and required secrets before opening a listing, or only on the detail page?
3. Does Workshop need every moderation and ownership action visible inline, or should those actions live behind a menu?
@@ -1,64 +0,0 @@
---
target: Workshop store in the docs
total_score: 38
p0_count: 0
p1_count: 0
timestamp: 2026-07-21T12-29-41Z
slug: apps-docs-src-components-workshop
---
# Workshop Store Critique — Post-Polish
## Design Health Score
| # | Heuristic | Score | Evidence |
| --------- | ----------------------------------- | --------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of system status | 4/4 | Loading, initial failure, stale refresh failure, empty, and results states are mutually exclusive; recovery actions are visible. |
| 2 | Match between system and real world | 4/4 | Actions describe their result: Download widget JSON, Copy CSS, View details, and Install in Homarr. |
| 3 | User control and freedom | 4/4 | Filters can be cleared, errors retried, dialogs cancelled, and destructive submission deletion requires explicit confirmation. |
| 4 | Consistency and standards | 4/4 | Creation and revision editing share CodeMirror, buttons follow the shared system, and cards use a predictable responsive grid. |
| 5 | Error prevention | 3/4 | Validation, disabled progression, pending states, RLS, and confirmations are strong; disabled form progression could explain unmet requirements more explicitly. |
| 6 | Recognition rather than recall | 4/4 | Persistent labels, type descriptions, step status, source previews, and installation instructions minimize recall. |
| 7 | Flexibility and efficiency | 4/4 | Search spans title, description, and author; signed-in filtering, sorting, responsive controls, and direct detail actions support repeat use. |
| 8 | Aesthetic and minimalist design | 3/4 | The catalog is restrained and scannable; long source documents and the complete creator form necessarily remain dense on small screens. |
| 9 | Help users recover from errors | 4/4 | Route, listing, edit, report, copy, OAuth, and detail failures remain local and provide a clear next action. |
| 10 | Help and documentation | 4/4 | The detail page explains the exact Homarr destination and links to updated Workshop documentation. |
| **Total** | | **38/40** | **Release-quality; minor polish remains** |
## Anti-Patterns Verdict
The surface is product-led rather than decorative. The redesign removes the overloaded card footer, hover scaling, tiny touch controls, public moderation details, contradictory states, and ambiguous install actions. The deterministic Impeccable detector reports zero findings.
## What Improved
1. The production route no longer collapses into the Docusaurus crash screen without recovery. A route-local boundary protects navigation, and the fresh-volume production path renders without console errors.
2. Loading, failure, empty, stale-results, and populated states are truthful and mutually exclusive.
3. Search and filters form a clear responsive hierarchy. Mobile targets are 40–44 pixels, signed-out users do not see Yours, and cards no longer overflow at 390 pixels.
4. Cards prioritize evaluation: title, author, type, score, description, comments, and View details. Copy, download, reporting, editing, and deletion moved to the detail page.
5. The detail page now teaches the installation journey and names its primary action precisely.
6. Reports and reporter identities are admin-only through PocketBase rules. Public catalog UI no longer turns reports into social proof.
7. The creator and revision dialogs share the CodeMirror editor, use bounded internal scrolling, expose pending/error states, and avoid step-selection layout shifts.
## Remaining Minor Opportunities
### P2: Explain disabled progression inline
The Type and Details steps are understandable, but a disabled Next button could state the missing requirement for keyboard and first-time users. This is a refinement, not a blocker, because required fields and validation messages are already present.
### P2: Consider a compact source summary on very small screens
The read-only source viewer is intentionally complete and horizontally scrollable. A future collapse/expand summary could reduce page length for nontechnical visitors without hiding inspection entirely.
## Verification Evidence
- Fresh Docker Compose PocketBase volume and production docs image.
- Populated catalog with widget, CSS, and outdated examples.
- Desktop and 390px mobile layouts in light and dark themes.
- Widget detail, installation instructions, CodeMirror source view, and comments.
- Signed-in submission wizard through Type and Details, including the shared editor.
- Zero browser console errors after the final fixes.
- Impeccable deterministic detector: zero findings.
## Compatibility Note
An existing local development volume created from an earlier unreleased version of the reset migration still has a stale Workshop view. It is intentionally not migrated or deleted. A fresh volume created from the current unreleased migration is the supported validation target and works correctly.
@@ -1,106 +0,0 @@
---
target: Homarr Workshop docs UI
total_score: 27
p0_count: 0
p1_count: 2
timestamp: 2026-07-22T15-58-15Z
slug: apps-docs-src-components-workshop
---
## Design Health Score
| # | Heuristic | Score | Key Issue |
| --------- | ------------------------------- | --------: | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | Visibility of System Status | 3 | Strong loading, retry, stale-data, pending, and copy states; successful report/edit actions still end silently. |
| 2 | Match System / Real World | 3 | Core Workshop language is clear, but `networkScope`, query/action, and schema terminology assumes technical fluency. |
| 3 | User Control and Freedom | 3 | Back, cancel, clear filters, vote toggling, and destructive confirmations are present; destructive actions have no undo. |
| 4 | Consistency and Standards | 3 | Shared primitives are coherent, though Docusaurus chrome and the denser app-store vocabulary still feel slightly stitched together. |
| 5 | Error Prevention | 2 | Validation and confirmations are good, but install safety and credential context appears after download/copy actions on small screens. |
| 6 | Recognition Rather Than Recall | 3 | Author, revision, source, filters, and capabilities are visible; installation still asks users to remember a separate Homarr navigation path. |
| 7 | Flexibility and Efficiency | 2 | Search, filters, sort, drag-and-drop, autofill, and comment shortcuts help; the install handoff remains manual. |
| 8 | Aesthetic and Minimalist Design | 2 | Hierarchy is clear, but repeated panels, badges, uppercase metadata labels, and overlapping install actions add noise. |
| 9 | Error Recognition and Recovery | 3 | Errors are plain-language and usually preserve work; the global action error can be detached from the failed control. |
| 10 | Help and Documentation | 3 | The installation guide and source explanation are useful; security terminology lacks inline definitions. |
| **Total** | | **27/40** | **Acceptable, significant improvements needed** |
## Anti-Patterns Verdict
**LLM assessment:** Moderate AI-assisted feel, not outright slop. Domain-specific copy, extensive state coverage, and transparent source/capability information make the feature credible. The generic card grid, repeated rounded panels, icon tiles, uppercase tracked labels, and decorative changelog stripe still expose a component-recipe grammar. It looks competent but not fully edited into a distinctive Homarr store.
**Deterministic scan:** `detect.mjs --json apps/docs/src/components/workshop` exited 0 with exact output `[]`: zero findings, zero triggered rules, and no false positives. The scan found none of Impeccable's deterministic anti-patterns.
**Visual overlays:** No reliable user-visible overlay is available. The independent detector agent had no browser backend. The parent fallback opened a fresh browser tab and visually inspected the listing, but the browser API did not expose a mutable evaluation method for the required preflight, so injection and `[Human]` visibility were correctly skipped. Browser/source evidence was used instead.
## Overall Impression
The Workshop is already substantially better than a generic gallery: it communicates authorship, source code, API capabilities, secrets, revision history, reports, and discussion. The biggest opportunity is to turn that information into a deliberate trust sequence. Today the interface lets the user act first and understand safety later, especially on mobile.
## What's Working
- **State coverage is unusually strong.** Listing failures, stale data, loading skeletons, empty results, filter recovery, comment errors, pending actions, and destructive confirmations have explicit UI.
- **Pre-install transparency is excellent.** API hosts, methods, configurable options, network scope, and credential requirements are visible instead of hidden behind a download.
- **The new primitive layer is coherent.** Search, filters, menus, switches, dialogs, and code presentation share one restrained interaction vocabulary across light and dark themes.
## Priority Issues
### [P1] Safety context arrives after the install action on mobile
**Why it matters:** Download and copy actions appear in the header, while source capabilities, credential requirements, and installation guidance live in an aside that drops below the long source and discussion sections under the desktop breakpoint. A first-time mobile user can commit before understanding what the widget contacts or what secrets it needs.
**Fix:** Put a compact “Before you install” summary immediately below the header on small screens: API hosts, network scopes, credential count, and action/query counts. Keep the full sticky details panel on desktop.
**Suggested command:** `$impeccable layout`
### [P1] Primary button and own-comment contrast misses AA
**Why it matters:** The requested theme's primary red with white foreground is about 3.4:1, below 4.5:1 for small text. The same pairing affects primary buttons and the author's comment bubbles.
**Fix:** Darken the primary red or use a dark foreground in light mode, then verify default buttons and comment bubbles in both themes.
**Suggested command:** `$impeccable colorize`
### [P2] The listing's server-rendered fallback is blank
**Why it matters:** Before hydration, the listing renders an empty block instead of preserving the page's hierarchy and indicating progress. This creates a visible dead zone on slower self-hosted deployments.
**Fix:** Render the listing heading, toolbar silhouette, and shared skeleton cards as the `BrowserOnly` fallback, matching the detail page's loading treatment.
**Suggested command:** `$impeccable harden`
### [P2] Download, copy, source, and install compete for the same task
**Why it matters:** “Download widget,” “Copy JSON,” a full source viewer, and a separate installation panel all appear as parallel paths. Users must infer the recommended workflow.
**Fix:** Make “Download widget JSON” the single primary header action. Keep copy beside the source viewer and place the next import step directly beside or immediately after download.
**Suggested command:** `$impeccable clarify`
### [P2] Repeated metadata scaffolding creates visual noise
**Why it matters:** Card shells, badges, icon tiles, uppercase tracked labels, and the changelog stripe make the detail page feel assembled from recipes and slow scanning.
**Fix:** Flatten the widget details into a compact definition list, reserve badges for status, and remove the decorative changelog stripe.
**Suggested command:** `$impeccable distill`
## Persona Red Flags
**Jordan, first-timer:** “Network scope,” queries, actions, options, and the manifest schema appear without inline explanation. Download versus copy has no stated recommendation. On mobile, credential and installation context arrives after source and discussion.
**Sam, accessibility-dependent user:** Primary controls and author comment bubbles miss small-text contrast. Copy, optimistic votes, filter result changes, and successful report/edit completion do not expose explicit live-region behavior. Gallery controls and dots are below comfortable touch-target sizes.
**Casey, distracted mobile user:** The most important safety panel falls to the bottom, while primary actions stay at the top. Long source and discussion content separate the decision from its explanation. The submit type selector remains dense at narrow widths.
## Minor Observations
- The internal installation guide uses an external-link icon.
- The final publication action says “Submit” rather than “Publish submission.”
- Dark-theme tokens are declared in two blocks, increasing drift risk.
- Screenshot dots rely on opacity alone and lack `aria-current`.
- Successful report and edit actions deserve a small confirmation instead of silent dismissal.
## Questions to Consider
- If source review is important enough to say “Review exactly what will be installed,” why can users download before seeing the capability and credential summary?
- Is Workshop primarily a code catalog or a safety-conscious app store? The current action hierarchy tries to be both.
- Should the successful end state be a downloaded file, or a confident handoff into Homarr with the next step already visible?
+1 -11
View File
@@ -3,15 +3,5 @@
"printWidth": 120,
"endOfLine": "lf",
"sortPackageJson": true,
"ignorePatterns": [
"**/*.cjs",
"**/lang/**",
"static-data/**",
"CHANGELOG.md",
"apps/docs/.docusaurus/**",
"apps/docs/build/**",
"apps/docs/blog/**",
"packages/custom-widgets/src/core/component-catalog.generated.json",
"apps/docs/static/custom-widgets/component-catalog-v1.json"
]
"ignorePatterns": ["**/*.cjs", "**/lang/**", "static-data/**", "CHANGELOG.md", "apps/docs/.docusaurus/**", "apps/docs/build/**", "apps/docs/blog/**"]
}
+1 -9
View File
@@ -30,15 +30,7 @@
"options": {
"reportUnusedDisableDirectives": "off"
},
"ignorePatterns": [
"**/*.config.js",
"**/*.config.mjs",
"**/*.cjs",
"**/lang/*.d.json.ts",
".next/**",
"apps/docs/.docusaurus/**",
"apps/docs/build/**"
],
"ignorePatterns": ["**/*.config.js", "**/*.config.mjs", "**/*.cjs", "**/lang/*.d.json.ts", ".next/**", "apps/docs/.docusaurus/**", "apps/docs/build/**"],
"settings": {
"react": { "version": "19" }
}
+21 -2
View File
@@ -22,7 +22,7 @@ COPY --parents ./packages/definitions/src ./
# isolated linker, patches apply inside the virtual store (node_modules/.pnpm/...)
# which IS created by pnpm fetch. The original hoisted linker is restored
# before the install step so the final node_modules layout stays flat.
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
pnpm config set store-dir /pnpm/store && \
sed -i 's/nodeLinker: hoisted/nodeLinker: isolated/' pnpm-workspace.yaml && \
pnpm fetch --ignore-scripts && \
@@ -31,7 +31,7 @@ RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
# Install only from the fetched, committed lockfile so local and Docker builds
# resolve the same dependency graph. Serial lifecycle builds avoid esbuild's
# atomic binary replacement racing across the hoisted workspace (pnpm/pnpm#8200).
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store,sharing=locked \
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
npm_config_nodedir=/usr/local pnpm install --recursive --offline --frozen-lockfile --child-concurrency=1
COPY . .
@@ -40,6 +40,25 @@ ARG SKIP_ENV_VALIDATION='true'
ARG CI='true'
ARG DISABLE_REDIS_LOGS='true'
ARG TARGETPLATFORM
# Opt-in React profiling build + browser source maps, for React DevTools' Profiler
# and for readable names in heap snapshots. See next.config.ts.
ARG HOMARR_PROFILING='false'
ENV HOMARR_PROFILING=${HOMARR_PROFILING}
# React ships a separate profiling renderer; the production one omits the hooks
# DevTools needs, which is what makes it report "Profiling not supported". Next only
# swaps it via a webpack alias (create-compiler-aliases.js), so under Turbopack both
# `next build --profile` and `reactProductionProfiling` silently produce a normal
# build. Point the vendored client entry at the profiling bundle instead — the same
# swap webpack's alias performs, done where Turbopack cannot miss it.
RUN if [ "$HOMARR_PROFILING" = "true" ]; then \
set -e; \
entry=node_modules/next/dist/compiled/react-dom/client.js; \
grep -q "react-dom-client.production.js" "$entry" || { echo "profiling patch: unexpected $entry"; exit 1; }; \
sed -i "s#./cjs/react-dom-client.production.js#./cjs/react-dom-profiling.profiling.js#" "$entry"; \
grep -q "react-dom-profiling.profiling.js" "$entry" || { echo "profiling patch failed"; exit 1; }; \
echo "profiling patch applied to $entry"; \
fi
RUN --mount=type=secret,id=TURBO_API,env=TURBO_API \
--mount=type=secret,id=TURBO_TEAM,env=TURBO_TEAM \
+28
View File
@@ -0,0 +1,28 @@
# Performance findings
This file grew past 1,200 lines covering server memory, client rendering, methodology and a long
list of rejected ideas at once. It has been split by subject under [`docs/perf/`](./docs/perf/):
| document | subject |
| --- | --- |
| [docs/perf/README.md](./docs/perf/README.md) | Headline results and the change-by-change ratio table |
| [docs/perf/SERVER-MEMORY.md](./docs/perf/SERVER-MEMORY.md) | What the container's RAM is made of, layer by layer, and what reduced it |
| [docs/perf/CLIENT-PERFORMANCE.md](./docs/perf/CLIENT-PERFORMANCE.md) | Dashboard cost in Chrome: DOM, fibers, images, paint |
| [docs/perf/REJECTED.md](./docs/perf/REJECTED.md) | Ideas implemented, measured, and abandoned — with their numbers |
| [docs/perf/METHODOLOGY.md](./docs/perf/METHODOLOGY.md) | The tools, and the measurement traps that produced wrong answers |
## Result in one table
Server RAM, merge-base `06724cfeb` vs this branch, two alternated runs per side, cgroup **anonymous**
memory — the figure that counts against a container limit:
| stage | before | after | change |
| --- | --- | --- | --- |
| **peak** | 376.6 MiB | **279.3 MiB** | **−26%** |
| boot idle | 130.5 MiB | **62.4 MiB** | **−52%** |
| after sign-in | 247.4 MiB | **142.9 MiB** | **−42%** |
| dashboard loaded | 293.4 MiB | **170.4 MiB** | **−42%** |
| 4 tabs, 2 boards | 366.5 MiB | **258.2 MiB** | **−30%** |
| after 2 min idle soak | 323.7 MiB | **200.8 MiB** | **−38%** |
No before/after range overlaps at any stage. Cost: **+18% CPU**, solid and non-overlapping.
-33
View File
@@ -1,33 +0,0 @@
# Product
## Register
product
## Users
Homarr is used by self-hosters and administrators who assemble dashboards for services running across home labs and private infrastructure. They need dense, reliable controls and status at a glance, with enough guidance to configure advanced features without hiding how they work.
## Product Purpose
Homarr provides one place to organize, observe, and operate self-hosted services. Success means a user can build a useful dashboard quickly, understand its state, recover from configuration errors, and extend it without requiring a bespoke Homarr feature for every service.
## Brand Personality
Direct, capable, and approachable. Copy should be concise and technical when precision matters, while the interface should remain familiar to users who are not developers.
## Anti-references
Avoid decorative admin dashboards, wizard flows that fight ordinary scrolling, hidden configuration magic, inconsistent controls, excessive modal usage, and interfaces that expose implementation complexity before the user needs it.
## Design Principles
1. Keep the user in control by making configuration and consequences visible.
2. Prefer familiar product patterns and progressive disclosure over custom interaction concepts.
3. Make simple tasks short without limiting advanced use cases.
4. Isolate failures so one service or widget cannot disrupt the dashboard.
5. Use the existing Mantine design language consistently across management and board surfaces.
## Accessibility & Inclusion
Target WCAG 2.2 AA. Support keyboard navigation, screen readers, visible focus, sufficient contrast, reduced motion, non-color status cues, responsive layouts, and actionable error messages.
+9 -2
View File
@@ -1,3 +1,10 @@
{
"cSpell.words": ["Homarr", "Lidarr", "Nextcloud", "Radarr", "Readarr", "Sonarr"]
}
"cSpell.words": [
"Homarr",
"Lidarr",
"Nextcloud",
"Radarr",
"Readarr",
"Sonarr"
]
}
+10 -16
View File
@@ -21,14 +21,8 @@ pnpm dev:docs
# Or from within this directory
pnpm dev
# Point the docs at a local PocketBase instance
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
The standalone docs use `WORKSHOP_API_URL` for PocketBase and `HOMARR_WEBSITE_URL` for the public site origin. The old
`WORKSHOP_URL` name is a deprecated one-release alias.
## Build
```bash
@@ -46,7 +40,7 @@ Uses oxlint and oxfmt (same as the rest of the monorepo):
```bash
pnpm lint # oxlint
pnpm format # oxfmt (check)
pnpm format:fix # oxfmt (fix)
pnpm format -- --write # oxfmt (fix)
```
## Typecheck
@@ -77,7 +71,7 @@ Each integration/widget doc follows a consistent pattern:
The docs app has `@homarr/definitions` as a workspace dependency. You can import integration kinds, widget kinds, and other definitions directly:
```typescript
import { IntegrationKind } from "@homarr/definitions";
import { IntegrationKind } from '@homarr/definitions';
```
This enables type-safe references to homarr domain concepts within documentation code.
@@ -96,15 +90,15 @@ pnpm verify:search
## Tech Stack
| Layer | Technology |
| -------------- | ------------------------------------------- |
| Layer | Technology |
|---|---|
| Site generator | Docusaurus 3 (`@docusaurus/preset-classic`) |
| Styling | Tailwind CSS 4 + Docusaurus Infima |
| Icons | `@tabler/icons-react` |
| Diagrams | Mermaid (`@docusaurus/theme-mermaid`) |
| Search | Algolia DocSearch |
| Analytics | PostHog |
| Charts | @nivo/line |
| Styling | Tailwind CSS 3 + Docusaurus Infima |
| Icons | `@tabler/icons-react` |
| Diagrams | Mermaid (`@docusaurus/theme-mermaid`) |
| Search | Algolia DocSearch |
| Analytics | PostHog |
| Charts | @nivo/line |
## Contributing
-24
View File
@@ -1,24 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/css/custom.css",
"baseColor": "mist",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "tabler",
"rtl": false,
"menuColor": "default",
"menuAccent": "subtle"
}
@@ -1,4 +1,4 @@
{
"collapsed": true,
"label": "Command line interface"
}
}
@@ -14,4 +14,4 @@ homarr fix-usernames
This will convert all credential usernames to lowercase and update the database accordingly.
![](./img/screenshot-cli.png)
![](./img/screenshot-cli.png)
@@ -1,7 +1,7 @@
# Command line interface
The Homarr image ships with a built-in recovery tool. This can be useful, if you do not have access to Homarr or if something is not working.
Please note that the CLI is an _emergency tool_, not an API tool.
Please note that the CLI is an *emergency tool*, not an API tool.
:::caution
@@ -12,10 +12,10 @@ You should also create a backup of the database, in case you break something.
## Execute CLI with Docker
1. Execute `docker ps | grep homarr` on the root system with Docker to find the container ID of your Homarr container.
2. Run `docker exec -it <container-id> /bin/bash homarr <your-command-goes-here>`. \
Replace `<container-id>` with your container ID from step 1. \
Replace `<your-command-goes-here>` with the command you want to execute (see sub-pages).
1. Execute ``docker ps | grep homarr`` on the root system with Docker to find the container ID of your Homarr container.
2. Run ``docker exec -it <container-id> homarr <your-command-goes-here>``. \
Replace ``<container-id>`` with your container ID from step 1. \
Replace ``<your-command-goes-here>`` with the command you want to execute (see sub-pages).
## Execute CLI with QNAP
@@ -7,7 +7,6 @@ This guide requires that you understand [how to access the CLI](../).
:::
Using these commands, you can manage the integrations from the CLI:
```bash
Group of commands to manage integrations
@@ -44,7 +43,7 @@ Flags:
## Delete Integration
- Requires a database connection
- Requires one parameter: `id` or `name`
- Requires one parameter: ``id`` or ``name``
- Deletes an integration permanently from the database
```bash
@@ -58,4 +57,4 @@ Flags:
-n, --name string Name of the integration
-h, --help help for integrations delete
-v, --version version for homarr-cli
```
```
@@ -12,8 +12,8 @@ All sessions for that user will be terminated and the user must re-login.
![](./img/screenshot-cli.png)
Using the `--username` parameter, you can specify who you want to reset the password for.
Using the ``--username`` parameter, you can specify who you want to reset the password for.
Executing the command will create a new random password, update the database and display the new password.
All sessions for this user will be terminated and they must re-login.
![](./img/screenshot-cli-result.png)
![](./img/screenshot-cli-result.png)
@@ -7,7 +7,6 @@ This guide requires that you understand [how to access the CLI](../).
:::
Using these commands, you can manage the users from the CLI:
```bash
Group of commands to manage users
@@ -43,7 +42,7 @@ yj52ek5cyb6du4bp4osbkjry admin xxxx@example.com
## Delete User
- Requires a database connection
- Requires one parameter: `id` or `username`
- Requires one parameter: ``id`` or ``username``
```bash
Delete a user by id or username
@@ -77,4 +76,4 @@ Flags:
-p, --password New password
-h, --help help for users update-password
-v, --version version for homarr-cli
```
```
@@ -1,8 +1,9 @@
{
"label": "Developer guides",
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index"
"label": "Developer guides",
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index"
}
}
}
@@ -8,121 +8,74 @@ tags:
sidebar_position: 1
---
## Set up your development environment
## Setting up your development environment
This guide will help you set up your development environment to start contributing to homarr.
### Prerequisites
- **Node.js 24.18 or newer**: You can download it from the [official website](https://nodejs.org/).
- **Corepack**: Run `corepack enable` so the repository selects its pinned pnpm 11 version.
- **Corepack**: Run `corepack enable` so the repository selects its pinned pnpm version.
- **Git**: You can download it from the [official website](https://git-scm.com/).
- **Docker or Docker Desktop**: Required for the Redis service and the developer Docker CLI.
- **Go 1.25+**: Required only for the developer Docker CLI. You can download it from the [official website](https://go.dev/).
- **GitHub CLI**: Required only for pull-request image features. Install it from the [official website](https://cli.github.com/), then authenticate with `gh auth login`.
The versions in the root `package.json` are the supported baseline. Let Corepack select the repository version instead
of installing an unrelated global pnpm:
### Setting up the project
```sh
corepack enable
corepack install
pnpm install --frozen-lockfile
```
1. Fork the repository by clicking on the "Fork" button on the top right corner of the repository page.
### Run the Homarr application
![Fork button](./img/fork.png)
1. Copy `.env.example` to `.env`.
2. Set `DB_URL` to an absolute writable SQLite file path.
3. Keep Redis and the optional local databases running in one terminal.
4. Apply migrations and start Next.js in another terminal.
2. Clone the repository to your local machine with `git clone`. You can find the url to use by clicking on the "Code" button on the top right corner of your forks repository page.
```sh
cp .env.example .env
```
![Clone button](./img/clone.png)
Terminal 1:
3. Change directory to the project folder with `cd homarr`.
```sh
pnpm docker:dev
```
4. Install the dependencies with `pnpm install`.
Terminal 2:
5. Create `.env` from `.env.example`. In a POSIX shell, run `cp .env.example .env`; in PowerShell, run `Copy-Item .env.example .env`. Set `DB_URL` to an absolute path for your SQLite file.
```sh
pnpm db:migration:sqlite:run
pnpm dev
```
6. Start Redis in the background with `pnpm docker:dev:up` if your development workflow needs it.
The infrastructure command remains in the foreground. Open `http://127.0.0.1:3000`; the first user created through
Homarr becomes the instance administrator.
Use `pnpm docker:dev:up` instead when you only need Redis in the background. Running the migration also seeds the
default data; use `pnpm db:seed` to seed it explicitly later.
7. Create or update the database with `pnpm db:migration:sqlite:run`. This also seeds the default data. To run the seed explicitly later, use `pnpm db:seed`.
### Run PocketBase and documentation
8. Start the development server with `pnpm dev` and wait for a message that says `✓ Ready in 10.7s`
Start PocketBase in another terminal:
9. Open your browser and navigate to [http://localhost:3000](http://localhost:3000). (Loading the page might take up to 2 minutes the first time)
```sh
cp apps/workshop/.env.example apps/workshop/.env
docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop
```
### Useful npm scripts
Then start Docusaurus in a third terminal:
#### During development
```sh
WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs
```
- `pnpm dev`: Start the Next.js development server.
- `pnpm dev:benchmark`: Run the local authenticated-board benchmark. Set `DEV_BENCHMARK_BOARD_URL` and
`DEV_BENCHMARK_STORAGE_STATE`; readiness-only measurements require the explicit `--smoke` flag and are not
suitable for performance claims.
- `pnpm dev:cli -- dev`: Browse and run local images and remote pull-request images.
- `pnpm dev:cli -- build <name>`: Build the current checkout as `homarr:<name>`.
- `pnpm dev:cli -- build --pr <number>`: Build a pull request locally from a temporary checkout.
- `pnpm dev:cli -- rebuild <name>`: Rebuild a local image from its recorded checkout or pull request.
- `pnpm dev:cli:install`: Optionally install the developer CLI as a `homarr` binary.
- `pnpm cli`: Run cli commands.
- `pnpm db:migration:sqlite:run`: Run the migrations to create or update the sqlite db file.
- `pnpm db:seed`: Explicitly seed the default database data.
- `pnpm docker:dev:up`: Start the Redis development service in the background.
- `pnpm db:migration:sqlite:generate`: Generate a new migration file for the sqlite db. Also works with `mysql`.
- `pnpm db:studio`: Open a visual studio for the database.
- `pnpm package:new`: Create a new package in the monorepo.
PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume.
#### Checking code quality
Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at
`http://127.0.0.1:3003/workshop`.
- `pnpm lint`: Run the linter to check for code quality issues.
- `pnpm format:fix`: Run the formatter to fix code style issues.
- `pnpm test`: Run the tests. You can also use `pnpm test:ui` to get a UI for the tests.
- `pnpm typecheck`: Run the typescript type checker.
Point Homarr at the local services with:
#### Building the project
```dotenv
HOMARR_WEBSITE_URL=http://127.0.0.1:3003
WORKSHOP_API_URL=http://127.0.0.1:8090
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop
```
Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See
[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth,
Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset.
### URL testing
Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads
`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a
separate API origin before release:
```sh
HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \
WORKSHOP_API_URL=http://127.0.0.1:8090 \
WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \
pnpm dev:docs
```
### Useful commands
- `pnpm dev` — Next.js on port 3000.
- `pnpm dev:benchmark` — authenticated-board benchmark; readiness-only `--smoke` runs are not performance claims.
- `pnpm dev:cli -- dev` — browse and run local images and remote pull-request images.
- `pnpm dev:cli -- build <name>` — build the current checkout as `homarr:<name>`.
- `pnpm dev:cli -- build --pr <number>` — build a pull request from a temporary checkout.
- `pnpm dev:cli -- rebuild <name>` — rebuild an image from its recorded checkout or pull request.
- `pnpm dev:cli:install` — optionally install the developer CLI as a `homarr` binary.
- `pnpm dev:docs` — Docusaurus only on port 3003.
- `pnpm docker:workshop` — local PocketBase through Docker Compose.
- `pnpm docker:dev:up` — Redis development service in the background.
- `pnpm cli` — Homarr operations CLI.
- `pnpm db:migration:sqlite:run` — create or update a SQLite database.
- `pnpm db:seed` — seed the default database data explicitly.
- `pnpm db:studio` — inspect the database.
- `pnpm format` / `pnpm format:fix` — check or apply oxfmt.
- `pnpm lint` / `pnpm lint:fix` — check or apply oxlint.
- `pnpm typecheck` — typecheck all workspaces.
- `pnpm test` — unit and contract suites.
- `pnpm test:workshop` — disposable Workshop integration suite.
- `pnpm build` — production monorepo build.
- `docker build -t homarr .` — build a production Docker image.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>` — run it.
- `pnpm build`: Build the project for production.
- `pnpm dev:cli -- build <name>`: Build a `homarr:<name>` image and record its checkout for later rebuilds.
- `docker build -t homarr .`: Build a docker image for the project.
- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:<name>`: Run the created docker image.
@@ -132,4 +132,4 @@ This command should return CPU and memory usage statistics for each node.
## Conclusion
You have successfully set up a multi-node Kubernetes cluster using kind and installed the Metrics Server for monitoring resource usage. This setup is useful for local development and testing Kubernetes workloads before deploying them to a production environment.
You have successfully set up a multi-node Kubernetes cluster using kind and installed the Metrics Server for monitoring resource usage. This setup is useful for local development and testing Kubernetes workloads before deploying them to a production environment.
@@ -25,17 +25,14 @@ This is especially useful for [docker secrets](https://docs.docker.com/engine/sw
Using the `PUID` and `PGID` will require you to set the correct permissions on the mounted volumes and if used the docker socket. See more in the [Running as a different user](/docs/advanced/running-as-different-user) documentation.
:::
| Environment Variable | Description | Possible values | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------- |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` |
| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` |
| `WORKSHOP_WEB_URL` | Public Workshop page opened from Homarr. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` |
| Environment Variable | Description | Possible values | Default |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- |
| `PUID` | User ID to run the container as | Any valid user ID | `0` |
| `PGID` | Group ID to run the container as | Any valid group ID | `0` |
| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` |
| `NO_EXTERNAL_CONNECTION` | Disables Homarr's internet-backed requests, including update checks, weather, and RSS feeds | `true` or `false` | `false` |
| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` |
| `HOMARR_DISABLE_IPV6` | Disables the IPv6 listener of the internal nginx proxy, even when the host supports IPv6. IPv6 is normally disabled automatically when the host has no IPv6 configured. See [#4596](https://github.com/homarr-labs/homarr/issues/4596) | `true` or `false` | `false` |
## Authentication
@@ -109,12 +106,6 @@ External redis is currently not supported with Proxmox Community Scripts
The advanced deployments environment variables should only be used if you know what you are doing.
:::
| Environment Variable | Description |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. |
| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production and must match the externally reachable Workshop origin. |
| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. Use `*` for the central public community so arbitrary self-hosted Homarr instances can use the API and OAuth; private staging may use a comma-separated origin allowlist. |
All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize
trailing slashes when Homarr or Docusaurus starts. The Docusaurus-only `WORKSHOP_URL` name is accepted as a deprecated alias for
`WORKSHOP_API_URL` for one release. `WORKSHOP_API_URL` takes precedence.
| Environment Variable | Description |
| ------------------------ | -------------------------------------------------- |
| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts |
@@ -6,11 +6,11 @@ tags:
Homarr has the following keyboard shortcuts:
| Shortcut | Description |
| ----------- | -------------------- |
| Shortcut | Description |
| -------- | ----------- |
| `MOD` + `K` | Opens the search bar |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
| `MOD` + `J` | Toggles Dark Mode |
| `MOD` + `E` | Toggles Edit Mode |
:::tip
+8 -12
View File
@@ -9,17 +9,16 @@ tags:
- Security
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
## Allowing self-signed certificates
Some users may come across a barrier, where they're unable to receive a 200 response from the Ping widget for some apps, while using self-signed certificates or a local certificate authory.
:::note What's going on?
Homarr is trying to communicate to your apps via the integrations.
It usually doesn't matter if Homarr is running on `http` or `https`.
It usually doesn't matter if Homarr is running on ``http`` or ``https``.
Your apps have a self-signed certificate - Homarr will recognize that the certificate was signed by an unknown authority and requests will be blocked.
:::
@@ -29,9 +28,8 @@ To allow self-signed certificates, you can configure them on the certificates ma
## Securing Homarr with Traefik
Copying the configuration straight from the docker-compose file won't work if you are running Homarr behind Traefik, such as a Portainer setup, or docker-swarm. In that case, you should use the following slightly modified configuration:
```yaml
version: "3"
version: '3'
services:
homarr:
container_name: homarr
@@ -55,12 +53,11 @@ networks:
external: true
```
<br />
<br/>
A sample Traefik docker-compose.yml using Cloudflare for certificate generation that works with the configuration above would be:
```yaml
version: "3"
version: '3'
apps:
traefik:
@@ -106,6 +103,7 @@ networks:
external: true
```
:::tip
Of particular note here is that both configurations explicitly define which network they are using, in this case "proxy", but it can be named anything. It just has to be the same across all apps for which Traefik is serving as a proxy. These are marked as external because the proxy network was manually created by running: `docker network create proxy` but this might be unnecessary depending on HOW exactly you are running Traefik. For example, if running [Traefik with Portainer](https://docs.portainer.io/advanced/reverse-proxy/traefik#deploying-in-a-docker-standalone-scenario), you can follow their official docs on how to set up Traefik and Portainer together, and you can just focus on the Homarr docker labels instead.
:::
@@ -116,6 +114,7 @@ If you are using Caddy as the reverse proxy for your setup, your `docker-compose
```yaml
services:
# <-- [Homarr installation]
caddy:
@@ -141,17 +140,14 @@ networks:
proxy:
external: true
```
:::important
Homarr needs to be on the same network so you will need to add the following property to your homarr install:
```yaml
homarr:
...
networks:
- proxy
```
:::
Next, create a file named `Caddyfile` at the same level as your `docker-compose.yml` file with the following content:
@@ -8,7 +8,7 @@ tags:
- PUID
---
By default the container is running with user `root` and group `root`.
By default the container is running with user `root` and group `root`.
You can change the user and group by using the `PUID` and `PGID` environment variables.
Important to note is, that you'll also need to change the permissions for the mounted directories accordingly.
@@ -32,11 +32,11 @@ services:
volumes:
- ./homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=1000
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=1000
ports:
- "7575:7575"
- '7575:7575'
```
After that you'll need to create the appdata directory and set the correct permissions:
@@ -76,11 +76,11 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=999
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
- PUID=1000
- PGID=999
ports:
- "7575:7575"
- '7575:7575'
```
And set the permissions for the mounted directories:
@@ -90,4 +90,4 @@ mkdir -p ./homarr/appdata
chown -R 1000:999 ./homarr/appdata
```
Important here is that the group ID is set to `999` as we created the `docker` group with this ID.
Important here is that the group ID is set to `999` as we created the `docker` group with this ID.
@@ -10,21 +10,20 @@ tags:
- Active Directory
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Single Sign On
Homarr supports multiple authentication options, from internal userbase (credentials), to LDAP (with Active directory support), and OIDC.
### Auth configuration common variables
| Environment Variable | Description | Default Value |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| AUTH_PROVIDERS | Select Which provider to use between _credentials_, _ldap_ and _oidc_. <br /> Multiple providers can be enabled with by separating them with `,`, (ex. `AUTH_PROVIDERS=credentials,oidc`, it is highly recommended to just enable one provider). | credentials |
| AUTH_LOGOUT_REDIRECT_URL | URL to redirect to after clicking logging out. | --- |
| AUTH_SESSION_EXPIRY_TIME | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | "30d" |
| AUTH_COOKIE_PREFIX | Prefix used for all authentication cookies (session, csrf, callback, pkce, state, nonce). Change this if you run another Auth.js/NextAuth app on the same hostname as Homarr (browsers scope cookies by host, not by port) to avoid cookie name collisions. Only letters, numbers, hyphens and underscores are allowed. | `homarr` |
| Environment Variable | Description | Default Value |
|---------------------------|-------------|---------------|
| AUTH_PROVIDERS | Select Which provider to use between *credentials*, *ldap* and *oidc*. <br /> Multiple providers can be enabled with by separating them with `,`, (ex. `AUTH_PROVIDERS=credentials,oidc`, it is highly recommended to just enable one provider). | credentials |
| AUTH_LOGOUT_REDIRECT_URL | URL to redirect to after clicking logging out. | --- |
| AUTH_SESSION_EXPIRY_TIME | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") | "30d" |
| AUTH_COOKIE_PREFIX | Prefix used for all authentication cookies (session, csrf, callback, pkce, state, nonce). Change this if you run another Auth.js/NextAuth app on the same hostname as Homarr (browsers scope cookies by host, not by port) to avoid cookie name collisions. Only letters, numbers, hyphens and underscores are allowed. | `homarr` |
<Tabs>
<TabItem value="credentials" label="Credentials Provider" default>
+11 -19
View File
@@ -19,14 +19,12 @@ Homarr uses https://mantine.dev/ under the hood. You can change the look & feel
using custom CSS, that extends or overwrites the styles from Mantine.
## Getting into CSS
If you don't have any experience with CSS,
we strongly recommend you to go through the courses here: https://www.w3schools.com/css/.
Due to the infinite scenarios and setups possible with custom CSS,
our support team will not provide any support with your own written CSS.
## Limitations and dangers
When using custom CSS, you can break the page or certain functionalities.
Although CSS can improve the design & feel of the page, it **<ins>cannot do any of the following</ins>**:
@@ -35,23 +33,21 @@ Although CSS can improve the design & feel of the page, it **<ins>cannot do any
- Completely revamp the design of the page, only smaller changes are possible
Here are some dangers of using custom CSS:
- Updates may contain breaking changes in the structure, built-in styling or how styling is processed. Therefore,
custom CSS may break unexpectedly without any warning
custom CSS may break unexpectedly without any warning
- Custom CSS may make certain elements invisible or not possible to click / interact with
- Custom CSS could make readability and accessibility worse for impaired people
## How to inspect the page
When styling the page, you can use the `Inspector` to inspect elements on the page and try out styles.
When styling the page, you can use the ``Inspector`` to inspect elements on the page and try out styles.
This may vary depending on your browser and operating system, but the workflow usually goes like this:
- Open the Homarr page
- Press `Ctrl` + `Shift` + `I` at the same time.
Alternatively, right-click on the page and choose `Inspect` or `Inspect element`.
- Open the tab `Elements` if it isn't open yet.
- Press ``Ctrl`` + ``Shift`` + ``I`` at the same time.
Alternatively, right-click on the page and choose ``Inspect`` or ``Inspect element``.
- Open the tab ``Elements`` if it isn't open yet.
- Search the element you want to change in the DOM tree or use the element picker (usually at the top left)
to select it by clicking on the page.
to select it by clicking on the page.
- Experiment by changing the CSS code on the right side
When writing custom CSS, it is important to also remember or create a selector, which tells the browser
@@ -59,19 +55,17 @@ which element to apply the styles to. If you accidentally broke something or wan
simply reload the page.
## Using Homarr's classes
Homarr's elements will already have a lot of classes and styles pre-applied.
You can reuse those classes to apply additional styles or overwrite them.
When overwriting, remember to add the `!important` flag to your CSS property to tell the browser to use our instruction instead.
When overwriting, remember to add the ``!important`` flag to your CSS property to tell the browser to use our instruction instead.
Classes with random number suffixes, such as `mantine-Card-x2fske`, should be avoided, since those are dynamic
Classes with random number suffixes, such as ``mantine-Card-x2fske``, should be avoided, since those are dynamic
CSS classes that will change with every build. This means, every time you update Homarr,
your styles will no longer apply to them
## Defining custom classes for widgets
If you want to apply the styles only for one widget, you can define custom class names only for that widget.
To do that, enter edit mode on your board, click the three dots of your widget, click `Edit item` and click `Advanced options`.
To do that, enter edit mode on your board, click the three dots of your widget, click ``Edit item`` and click ``Advanced options``.
Then, you can enter as many custom classes as you'd like:
![](./img/widget-custom-classes.png)
@@ -79,7 +73,6 @@ Then, you can enter as many custom classes as you'd like:
To save them, click `Save changes` on both modals and exit edit mode.
## Where to enter custom CSS
After you wrote your custom CSS, it is time to permanently save it, so you don't need to use the inspector anymore.
To do this, enter the board settings:
@@ -91,12 +84,11 @@ Here you can enter your CSS code, which will be applied on the respective board
![](./img/custom-css-setting.png)
## Using variables
You can also change the values of the theme variables, which are located here: https://mantine.dev/styles/css-variables-list/.
To do this, see the following example CSS:
```css
:root {
--mantine-primary-color-filled: #ffbb00;
--mantine-primary-color-filled: #ffbb00;
}
```
```
+1 -5
View File
@@ -7,15 +7,12 @@ tags:
---
# Donate
## Why to donate
Our team of 4 developers is working on Homarr in their free time.
Homarr has become a big part of our daily things we work on - and therefore we invest a lot of time into maintaining Homarr,
providing support and implementing new features.
We also incur monetary costs while developing Homarr for the tools that we need.
A few examples include:
- Renting for a Cloud VPS for our cloud services (status page, monitoring, email, analytics, [demo instance](https://demo.homarr.dev/), ...)
- Paying for (IDE-) licenses
@@ -23,7 +20,6 @@ Using a donation, you help us to cover these expenses and enable the development
Homarr is completely free to use. We'd highly appreciate if you consider donating, especially if you use Homarr in a commercial environment.
## Where to donate
We accept donations on our official [OpenCollective](https://opencollective.com/homarr).
You may also sponsor us directly by providing a useful service / product to us other means.
You can get into contact with us [here](/docs/community/get-in-touch).
You can get into contact with us [here](/docs/community/get-in-touch).
+14 -34
View File
@@ -5,57 +5,43 @@ tags:
- Help
---
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
### Can I install Homarr on a Raspberry Pi?
Yes, Homarr is compatible with the architecture of the Raspberry Pi. Have a look in our <a href="/docs/category/installation-1">installation guide</a> on how to do so.
### I have a suggestion / idea for improvement or want to report a bug
You can report issues, suggestions, or ideas for improvements <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
### Can I use my own custom icons for apps?
Yes, check out our guide on <a href="/docs/advanced/customizations/icons">how to add your custom icons</a>.
### Some of my online icons are not working (anymore)
It seems the `cdn.jsdelivr.net` database of icon sometimes appear in the lists for DNS-Holes (PiHole, AdGuard Home...). <br />
So make sure to manually whitelist this address.
### Can I add my own widgets? Do I need programming skills?
We have decided, not to support custom widgets, until Homarr is in a stable and highly polished state.
Enabling users to write their own widgets is easy at the first glance, but there are some major issues with it:
Yes. A [Custom Widget](/docs/management/custom-widgets) can read a public or private HTTP API.
- There are mainly two options for adding plugins or custom widgets to Homarr. The first option is, to allow and inject custom code in Homarr. Apps like Nextcloud can do this easily, because they use the Runtime to render all components of the application (In reality, the process is still a bit more complicated). In contrast, Homarr is using Next.js - a framework that needs to be compiled in production. That means, that each time you would modify a custom widget or plugin, the app would have to be restarted. There are options to avoid this, but they are quite flakey and bring disadvantages in our experience. The second option is, to write widgets declaratively instead of imperatively. This way, we would be able to render and modify widgets without any restarting at all. However, we'd have to write our own interpreter for said declarative schema. This is totally possible, but requires a lot of planning and architecture design to function properly. The required work to implement either option is therefore quite substantial.
- Custom widgets would need to be able to request data, so they work properly. We use tRPC for this. Many APIs use different formats and we'd have to convert and read those.
- Using custom widgets, one could easily create harmful widgets, leak your personal data (potentially including your tokens) and trick your users. A proper implementation (eg. official plugin store, where we can ban harmful widgets from) would resolve this problem to a certain extent, but again requires additional work and time to be put in.
It can show responsive Mantine JSX. It can also use widget options and actions with board permission checks.
Custom JSX v2 is the only Custom Widget format. Write the JSON and JSX in the workbench.
You can also use the AI copy prompt. An MCP agent can validate and preview a definition before it saves the definition.
The workbench reads a pasted AI response. It shows request capabilities and redacted diagnostics.
A template cannot import packages or run arbitrary JavaScript. It cannot call browser `fetch`.
Named requests use the Homarr server. The server applies board permissions and the network policy.
Before installation, examine the origins, methods, permissions, and actions. Use the same review before publication.
If you still want to add a widget, please consider to contribute it directly to Homarr. For that, basic programming skills are required. Our [Developer Guides](/docs/advanced/development/getting-started) explain this even more detailed.
### Is it secure to expose Homarr to the web?
Yes, since v0.14.0, we are confident that Homarr is secure enough to be exposed to the web.
Yes, since v0.14.0, we are confident that Homarr is secure enough to be exposed to the web.
Make sure to enable the "allow anonymous access" [option in the settings](/docs/management/boards#access-control), if you want to allow people without an account to be able to see your dashboard (not edit)
### How do I open the console / log?
There are two different sources of information:
- console: This is your client (browser). It can tell us, if there is an issue on the client (crash, bug, ...).
- log: This is your server. It will tell us, if there are issues with connections for the integrations, crashes and errors.
Both variants _can_ contain personal data. It's recommended, that you don't post them publicly or remove personal data from it.
Both variants *can* contain personal data. It's recommended, that you don't post them publicly or remove personal data from it.
<Tabs>
<TabItem value="apple" label="Read console" default>
@@ -63,45 +49,39 @@ Both variants _can_ contain personal data. It's recommended, that you don't post
> Note: This will not work on most mobile browsers.
Open your browser and ensure, that Homarr is your current tab.
Press `CTRL` + `SHIFT` + `I` on your keyboard. If this shortcut doesn't work, please use one of the alternatives: https://developer.chrome.com/docs/devtools/open/.
Click on `Console` in the new window, that opened. There, you should see white, orange and sometimes red messages. You can use the Windows Snip & Sketch tool (or your respective tool on Linux / MacOS) to screenshot those messages.
Press ``CTRL`` + ``SHIFT`` + ``I`` on your keyboard. If this shortcut doesn't work, please use one of the alternatives: https://developer.chrome.com/docs/devtools/open/.
Click on ``Console`` in the new window, that opened. There, you should see white, orange and sometimes red messages. You can use the Windows Snip & Sketch tool (or your respective tool on Linux / MacOS) to screenshot those messages.
> Please never execute any commands in the console. Scammers are using the console to let you execute harmful commands. If you want to resolve an issue in Homarr, **you'll never have to execute any commands here**
</TabItem>
<TabItem value="orange" label="Read log">
This depends on your installation method. If you use Unraid, you can simply click on the App and on `Log`. If you have a bare Docker install, use `docker container logs homarr`. If you used a different name than `homarr` for your container, you need to replace said argument. Here is some additional information about the command: https://geekflare.com/check-docker-logs/.
This depends on your installation method. If you use Unraid, you can simply click on the App and on ``Log``. If you have a bare Docker install, use ``docker container logs homarr``. If you used a different name than ``homarr`` for your container, you need to replace said argument. Here is some additional information about the command: https://geekflare.com/check-docker-logs/.
</TabItem>
</Tabs>
### How do I post logs?
Please check [the section above](#how-do-i-open-the-console--log), on how to obtain logs. Logs should always be posted to https://pastebin.com/ or https://rentry.co/.
### Can I use Homarr at my company?
Yes, you can! But please consider a donation, if our work has been useful to you.
### I think I found a bug. What can I do?
If you think, that you found a bug, please create an issue <a href="https://github.com/homarr-labs/homarr/issues/new/choose">here</a>.
### Why can't I use my custom icons?
You need to restart the Homarr Docker container after adding icons.
### Why can't homarr see/ping containers running locally behind GluetunVPN?
This is because GluetunVPN, and many other docker VPN services, secure their VPN connections with strict firewalls. These firewalls often block access to local containers, even on the same docker network. The workaround for <a href="https://github.com/qdm12/gluetun-wiki/blob/main/setup/options/firewall.md">GluetunVPN</a> is to add "- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24" into the environmental variables of the docker compose file. If you are using a firewall on your system, you will need to add a rule that allows requests from ip's in the range of 192.168.xxx.xxx . If you are using UFW the command would be "sudo ufw allow from 192.168.0.0/16".
Tip: If you are running your torrent client behind a VPN, and running Sonarr (or something similar) normally, make sure to open your torrent client's port in your firewall. If you don't, Sonarr will not be able to see it. Furthermore, use your machines IP (192.168.x.x) rather then 127.0.0.1 or localhost.
### Can I get in touch with you?
Yes, go <a href="/docs/community/get-in-touch">here</a> to get with us in touch.
<hr />
<hr/>
:::tip
+2 -2
View File
@@ -9,16 +9,16 @@ tags:
We're happy to talk with you! Shoot us a message at the following places:
## E-Mail
We are open to discuss sponsorships, security vulnerabilities and other private requests through our email.
Please do not report any bugs / feature requests here - they will be ignored and deleted.
[homarr-labs@proton.me](mailto:homarr-labs@proton.me)
## Discord
We have a Discord Server, where we announce new releases, discuss features and bugs and show off our dashboards.
There are over 4'000 members and we're happy to answer your questions or concerns.
You're also open to discuss new / upcoming features and any offtopic selfhosting content.
[_![](https://discordapp.com/api/guilds/972958686051962910/widget.png?style=banner2)_](https://discord.gg/aCsmEV5RgA)
+2 -1
View File
@@ -7,7 +7,6 @@ tags:
Homarr is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).
## TL;DR
| Action | Allowed |
| -------------- | ------- |
| Copy | ✅ |
@@ -25,6 +24,8 @@ Please consider [making a donation](/docs/community/donate) to Homarr, if this p
:::
## Full License
```txt
+12 -14
View File
@@ -5,31 +5,30 @@ tags:
- Translations
---
We want to make Homarr accessible for everyone.
But to do that, we need your help.
We are running a community program that enables everyone to translate Homarr into their language – no programming required.
We are using [Crowdin](https://crowdin.com/) to manage our translations.
We want to make Homarr accessible for everyone.
But to do that, we need your help.
We are running a community program that enables everyone to translate Homarr into their language – no programming required.
We are using [Crowdin](https://crowdin.com/) to manage our translations.
You can find the translations [here](https://translate.homarr.dev/).
## How to translate
### Using crowdin's online platform
Go to the [Crowdin translations](https://translate.homarr.dev/) and select a language you want to translate.
You can then enter the translation mode and start translating.
After you've submitted your translations, a Proofreader will review your changes.
After a translation has been approved, it will be merged to the language development branch.
We merge this one periodically to the development branch.
### Using crowdin's online platform
Go to the [Crowdin translations](https://translate.homarr.dev/) and select a language you want to translate.
You can then enter the translation mode and start translating.
After you've submitted your translations, a Proofreader will review your changes.
After a translation has been approved, it will be merged to the language development branch.
We merge this one periodically to the development branch.
After the next update of Homarr, all changes from the development branch will be merged, including the changes in the translations.
### Using live translate
You can also help translate homarr directly from the app!
For this you can simply change your language to "Crowdin (live translations)"
![](./img/crowdin-language-select.png)
Then you'll be prompted to connect to your Crowdin account and choose the language in which you want to transalte.
Then you'll be prompted to connect to your Crowdin account and choose the language in which you want to transalte.
You can then start translating directly from the app by selecting some text on the app.
If some text cannot be selected to be edited with the live translation tool, you can still translate it inside of the In-Context popup at the Home tab, it will be red if it's not translated yet.
@@ -48,8 +47,7 @@ Do you want to become a proofreader? [Let us know on our Discord](/docs/communit
:::
## Guidelines
1. Translate with context: Some translations can't be translated 1:1 - they need context. Ensure that your translations make sense in the context.
2. Keep things simple and original - no additional characters are needed.
3. Don't translate the same thing twice - if you see a translation that is already translated, don't translate it again unless you have a better translation.
4. Keep your translations as close to the original as possible.
5. Keep your translations as close to the original as possible.
@@ -54,7 +54,11 @@ You must choose a username and password (both can later be changed).
We enforce strict password requirements when you enter your password to protect your instance if you accidentally
expose it to the internet.
<img src={imageStartFromScratchStepAdminUser} alt={"Screenshot form to create admin user"} width={300} />
<img
src={imageStartFromScratchStepAdminUser}
alt={"Screenshot form to create admin user"}
width={300}
/>
If you selected external providers (such as LDAP or OIDC), you'll be prompted to create an admin group.
This group will be used to synchronize users from your provider to Homarr.
@@ -112,14 +116,22 @@ The onboarding can no longer be started again.
From here you can click on one of the provided links to continue your journey.
We recommend you to continue with the section [creating your first board on this page](#creating-your-first-board).
<img src={imageStartFromScratchStepFinish} alt={"Screenshot of the finish onboarding process with links"} width={300} />
<img
src={imageStartFromScratchStepFinish}
alt={"Screenshot of the finish onboarding process with links"}
width={300}
/>
### Importing a ZIP from version before 1.0.0
This is recommended when you used Homarr before and want to re-import your data.
It is important that you first migrate correctly using [the list of breaking changes for 1.0](/blog/2024/09/23/version-1.0).
<img src={manageExportOldHomarr} alt="Export from old Homarr" style={{ maxWidth: 500 }} />
<img
src={manageExportOldHomarr}
alt="Export from old Homarr"
style={{ maxWidth: 500 }}
/>
**How to export and import data**
@@ -185,23 +197,38 @@ Here you have the following options in the header:
When entering the edit mode, you'll get some additional buttons to work with:
<img src={manageBoardHeaderEditMode} alt={"Screenshot of buttons in the edit mode"} width={200} />
<img
src={manageBoardHeaderEditMode}
alt={"Screenshot of buttons in the edit mode"}
width={200}
/>
- **Add item**: Click this button to add new elements to your page.
- **Exit edit mode**: Click this btuton to save your changes and exit edit mode.
To continue, click the option "New Item":
<img src={manageBoardHeaderAddItem} alt={"Screenshot of the add item menu"} width={200} />
<img
src={manageBoardHeaderAddItem}
alt={"Screenshot of the add item menu"}
width={200}
/>
Next, you can choose what item you'd like to add.
We will discuss the other options in the following sections - for now choose "App" to continue.
<img src={manageBoardHeaderChooseItem} alt={"Screenshot of choosing the app item in the modal"} width={300} />
<img
src={manageBoardHeaderChooseItem}
alt={"Screenshot of choosing the app item in the modal"}
width={300}
/>
As soon as you chose the app, the modal will close and you'll get the following item on your board:
<img src={manageBoardNoApp} alt="Screenshot of the No app element on the board" />
<img
src={manageBoardNoApp}
alt="Screenshot of the No app element on the board"
/>
This is expected, because you haven't told Homarr what app it should show in this item.
To configure this, click on the small 3 dots at the top of the item in the edit mode and choose "Edit item" in the menu.
+1 -2
View File
@@ -1,9 +1,8 @@
# Glossary
In the glossary, you'll learn the different components and their names of Homarr.
| Term | Definition |
| --------------- | -------------------------------------------------------------------------------------------------------------- |
|-----------------|----------------------------------------------------------------------------------------------------------------|
| Apps | An piece of software that you or a third party hosts (eg. Google) |
| Integration | A connection to a service outside of Homarr |
| Dynamic section | Resizeable area where items on your board can be placed and grouped |
+6 -7
View File
@@ -8,14 +8,13 @@ tags:
- Unraid
---
import Link from "@docusaurus/Link";
import { IconArrowRight } from "@tabler/icons-react";
import Link from '@docusaurus/Link';
import { IconArrowRight } from '@tabler/icons-react';
Before you try to install Homarr, please review if your device meets the requirements.
Installing on an unsupported device may lead to unstable behaviour and no support will be provided.
## Hardware
- At least 500MB RAM
- At least 600MB of free disk space for the Docker image. SSD is preferred.
- Single or dual core CPU
@@ -25,8 +24,8 @@ Installing on an unsupported device may lead to unstable behaviour and no suppor
We build Homarr for a bunch of different architectures, including ARM. Please ensure that your device architecture is supported by Homarr. We won't be able to provide support for unusual or outdated architectures.
| Architecture | Supported |
| ------------ | ---------------- |
| Architecture | Supported |
|--------------|-----------------|
| linux/amd64 | ✅ Supported |
| linux/arm64 | ✅ Supported |
| linux/arm/v7 | ❌ Not supported |
@@ -35,10 +34,10 @@ We build Homarr for a bunch of different architectures, including ARM. Please en
<Link
data-attr="Go to installation"
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
className={'button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800'}
to="/docs/category/installation-1"
>
<div className={"flex items-center gap-3"}>
<div className={'flex items-center gap-3'}>
<span className={"dark:text-gray-200"}>Go to Installation</span>
<IconArrowRight className={"dark:text-gray-200"} size={20} />
</div>
@@ -1,23 +1,21 @@
import { DockerInstallSnippet } from "@site/src/components/pages/installation/docker/dynamic-docker-install";
import { DockerInstallSnippet } from '@site/src/components/pages/installation/docker/dynamic-docker-install'
# Docker
Docker is our recommended installation method for beginners and professionals.
It is easy to use, ships with all dependencies inside a single container and is easy to use and maintain.
It requires at least some basic knowledge of [Docker](https://www.docker.com/).
We recommend [NetworkChuck's Docker Containers 101](https://www.youtube.com/watch?v=eGz9DS-aIeY) as a good start for complete beginners.
## Docker compose
### Prerequisites
- A Linux or Windows based system that meets the [hardware prerequisites](/docs/getting-started/#hardware) and [architecture prerequisites](/docs/getting-started/#architecture).
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/) - _preinstalled on many systems nowadays_
- [Docker Compose](https://docs.docker.com/compose/install/) - *preinstalled on many systems nowadays*
### Installation
<DockerInstallSnippet data-visual-test="blackout" />
<DockerInstallSnippet data-visual-test="blackout"/>
Finally, run ``docker compose up -d`` in the same directory. This will start the Homarr container in the background.
:::caution
@@ -28,26 +26,27 @@ Docker compose creates a network by default. Some integrations may not work (spe
### Updating
To update, navigate to the directory with the `docker-compose.yaml` located.
To update, navigate to the directory with the ``docker-compose.yaml`` located.
- Stop Homarr using `docker compose down`
- Pull the newest image of Homarr using `docker compose pull`
- Start Homarr again using `docker compose up -d` (`-d` for detached mode - start in background)
- Delete the old image using `docker image prune` (_Warning: this also removes you other unused images - not just Homarr_)
- Stop Homarr using ``docker compose down``
- Pull the newest image of Homarr using ``docker compose pull``
- Start Homarr again using ``docker compose up -d`` (``-d`` for detached mode - start in background)
- Delete the old image using ``docker image prune`` (*Warning: this also removes you other unused images - not just Homarr*)
You can automate this process using [Watchtower](https://containrrr.dev/watchtower/).
### Uninstalling
To uninstall, navigate to the directory with the `docker-compose.yaml` located.
To uninstall, navigate to the directory with the ``docker-compose.yaml`` located.
- Stop Homarr using `docker compose down`
- Delete the created directories & files on your root file system (check `docker-compose.yaml` file for your specific locations)
- Delete compose file using `rm docker-compose.yaml`
- Prune unused Docker images using `docker image prune` (_Warning: this also removes you other unused images - not just Homarr_)
- Stop Homarr using ``docker compose down``
- Delete the created directories & files on your root file system (check ``docker-compose.yaml`` file for your specific locations)
- Delete compose file using ``rm docker-compose.yaml``
- Prune unused Docker images using ``docker image prune`` (*Warning: this also removes you other unused images - not just Homarr*)
- (Optional): Prune any network or volumes if you use any.
- (Optional): Remove Homarr from your reverse proxy, VPNs or tunnels if you use any.
## Docker standalone
:::caution
@@ -78,16 +77,15 @@ docker run \
:::note
Docker will mount the configuration files and icons to your host machine. Please make sure to replace `<your-path>` from the docker run command with your desired storage location. The path must be absolute.
Docker will mount the configuration files and icons to your host machine. Please make sure to replace ``<your-path>`` from the docker run command with your desired storage location. The path must be absolute.
:::
### Updating
To update Homarr, you must remove your container first. Make sure that you've mounted your data and that you have access to it, so your configuration doesn't get lost.
1. Run `docker rm homarr` to remove the container.
2. Pull the latest Homarr image `docker pull ghcr.io/homarr-labs/homarr:latest`.
1. Run ``docker rm -f homarr`` to stop and remove the container.
2. Pull the latest Homarr image ``docker pull ghcr.io/homarr-labs/homarr:latest``.
3. Re-run the command you used to install Homarr.
This process can get tideous, if you update frequently. Thus, we recommend [using docker-compose](#docker-compose)
@@ -100,8 +98,8 @@ Want to update all your containers automatically? Checkout [Watchtower](https://
### Uninstalling
1. Obtain the container ID of your Homarr container using `docker ps | grep homarr`.
2. Delete the container using `docker rm <your-container-id>`
3. Prune unused Docker images using `docker image prune` (_Warning: this also removes you other unused images - not just Homarr_)
1. Obtain the container ID of your Homarr container using ``docker ps -a | grep homarr``.
2. Delete the container using ``docker rm -f <your-container-id>``
3. Prune unused Docker images using ``docker image prune`` (*Warning: this also removes you other unused images - not just Homarr*)
4. (Optional): Prune any network or volumes if you use any.
5. (Optional): Remove Homarr from your reverse proxy, VPNs or tunnels if you use any.
@@ -12,7 +12,7 @@ A Helm chart to deploy homarr for Kubernetes
## Source Code
- <https://github.com/homarr-labs/homarr>
* <https://github.com/homarr-labs/homarr>
## Requirements
@@ -76,38 +76,38 @@ To avoid including sensitive information in plain text within your version contr
Below is an exhaustive list of all secrets:
| FEATURE | SECRET NAME | SECRET KEYS | Required |
| -------- | ---------------- | ------------------------------------ | -------- |
| OIDC | auth-oidc-secret | oidc-client-id<br>oidc-client-secret | No |
| LDAP | auth-ldap-secret | bind-password | No |
| DATABASE | db-secret | db-url | No |
| DATABASE | db-encryption | db-encryption-key | yes |
| FEATURE | SECRET NAME | SECRET KEYS | Required |
|-----------|-------------------------|-----------------------------------------|----------|
| OIDC | auth-oidc-secret | oidc-client-id<br>oidc-client-secret | No |
| LDAP | auth-ldap-secret | bind-password | No |
| DATABASE | db-secret | db-url | No |
| DATABASE | db-encryption | db-encryption-key | yes |
### Database
You have multiple options for configuring the database:
| DRIVER TYPE | Persistence mode |
| -------------- | ---------------------------- |
| better-sqlite3 | Pod disk |
| better-sqlite3 | homarr-database PVC |
| mysql2 | External MySql database |
| node-postgres | External Postgresql database |
| DRIVER TYPE | Persistence mode |
|----------------|---------------------------------|
| better-sqlite3 | Pod disk |
| better-sqlite3 | homarr-database PVC |
| mysql2 | External MySql database |
| node-postgres | External Postgresql database |
#### Pod disk
No additional configuration is required. However, keep in mind that if the pod restarts, all data will be lost. This setup is not _recommended_ for production use.
No additional configuration is required. However, keep in mind that if the pod restarts, all data will be lost. This setup is not *recommended* for production use.
To create the necessary database secret, execute the following command:
<details>
<summary>Required Secrets</summary>
```yaml
````yaml
kubectl create secret generic db-encryption \
--from-literal=db-encryption-key='<SECRET_ENCRYPTION_KEY_SECRET_TO_CHANGE>' \
--namespace homarr
```
````
</details>
@@ -120,11 +120,11 @@ Associated secret to create :
<details>
<summary>DB Required Secrets</summary>
```yaml
````yaml
kubectl create secret generic db-encryption \
--from-literal=db-encryption-key='<SECRET_ENCRYPTION_KEY_SECRET_TO_CHANGE>' \
--namespace homarr
```
````
</details>
@@ -133,13 +133,13 @@ Bellow an example of the override value file :
<details>
<summary>values.yaml</summary>
```yaml
````yaml
persistence:
homarrDatabase:
enabled: true
storageClassName: "default"
size: "1Gi"
```
````
</details>
@@ -150,18 +150,17 @@ To create the necessary database secrets, execute the following command:
<details>
<summary>Required Secrets</summary>
```yaml
````yaml
kubectl create secret generic db-encryption \
--from-literal=db-encryption-key='<SECRET_ENCRYPTION_KEY_SECRET_TO_CHANGE>' \
--namespace homarr
```
````
```yaml
````yaml
kubectl create secret generic db-secret \
--from-literal=db-url='mysql://user:password@host:port/homarrdb' \
--namespace homarr
```
````
</details>
Below is an example of the override values file:
@@ -169,11 +168,10 @@ Below is an example of the override values file:
<details>
<summary>values.yaml</summary>
```yaml
````yaml
database:
type: mysql
```
````
</details>
#### External Postgresql database
@@ -183,17 +181,17 @@ To create the necessary database secrets, execute the following command:
<details>
<summary>Required Secrets</summary>
```yaml
````yaml
kubectl create secret generic db-encryption \
--from-literal=db-encryption-key='<SECRET_ENCRYPTION_KEY_SECRET_TO_CHANGE>' \
--namespace homarr
```
````
```yaml
````yaml
kubectl create secret generic db-secret \
--from-literal=db-url='postgresql://user:password@host:port/homarrdb' \
--namespace homarr
```
````
</details>
@@ -202,11 +200,10 @@ Below is an example of the override values file:
<details>
<summary>values.yaml</summary>
```yaml
````yaml
database:
type: postgresql
```
````
</details>
### Ingress
@@ -216,7 +213,7 @@ The ingress section in the values.yaml file allows you to configure how external
<details>
<summary>values.yaml</summary>
```yaml
````yaml
service:
enabled: true # Ensure the service is enabled for Ingress to route traffic
ingress:
@@ -234,8 +231,7 @@ ingress:
- "homarr.homelab.dev"
- "www.homarr.homelab.dev"
secretName: homelab-tls
```
````
</details>
### HTTPRoute (Gateway API)
@@ -246,9 +242,9 @@ This provides a more expressive and future-proof alternative to Ingress, with su
<details>
<summary>values.yaml</summary>
```yaml
````yaml
service:
enabled: true # must be enabled for HTTPRoute to forward traffic
enabled: true # must be enabled for HTTPRoute to forward traffic
httproute:
enabled: true
parentRefs:
@@ -264,15 +260,14 @@ httproute:
backendRefs:
- name: homarr
port: 7575
```
````
</details>
#### Notes:
- parentRefs: the Gateway(s) this route attaches to. Gateways must already exist.
- hostnames: domain names this route applies to.
- rules: each rule can define matches (paths, headers, queries), optional filters, and backendRefs to services
- rules: each rule can define matches (paths, headers, queries), optional filters, and backendRefs to services
- TLS must be configured on the Gateway (not on HTTPRoute). For example, the Gateway can have a TLS listener for homarr.homelab.dev, and this route will automatically apply once matched.
### Certificates
@@ -289,7 +284,7 @@ Use inline certificates to generate a ConfigMap, mounted as individual files.
<details>
<summary>values.yaml</summary>
```yaml
````yaml
persistence:
homarrTrustedCerts:
enabled: true
@@ -303,8 +298,7 @@ persistence:
-----BEGIN CERTIFICATE-----
MIID...EFGH==
-----END CERTIFICATE-----
```
````
</details>
Behavior:
@@ -319,7 +313,7 @@ Use inline certificates to generate a Kubernetes Secret, mounted as files.
<details>
<summary>values.yaml</summary>
```yaml
````yaml
persistence:
homarrTrustedCerts:
enabled: true
@@ -333,8 +327,7 @@ persistence:
-----BEGIN CERTIFICATE-----
MIIC...XYZ==
-----END CERTIFICATE-----
```
````
</details>
Behavior:
@@ -349,17 +342,16 @@ Use an existing Kubernetes Secret, assuming its keys are filenames.
<details>
<summary>values.yaml</summary>
```yaml
````yaml
persistence:
homarrTrustedCerts:
enabled: true
type: existingSecret
existingSecretName: "existingSecretName"
existingSecretKeys:
- cert3.crt
- cert4.crt
```
- cert3.crt
- cert4.crt
````
</details>
Behavior:
@@ -380,115 +372,114 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
## Values
| Key | Type | Default | Description |
| -------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| additionalObjects | list | `[]` | Additional resources to deploy. These objects are templated. |
| affinity | object | `{}` | Node affinity for pod scheduling |
| autoscaling.enabled | bool | `false` | Enable autoscaling |
| autoscaling.maxReplicas | int | `100` | Maximum replicas |
| autoscaling.minReplicas | int | `1` | Minimum replicas |
| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization for autoscaling |
| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. |
| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). |
| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server |
| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups |
| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups |
| env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user's groups search filter (& based) |
| env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE | string | `"member"` | Attribute used for querying group member |
| env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE | string | `"dn"` | User attribute used for comparing with group member |
| env.AUTH_LDAP_SEARCH_SCOPE | string | `"base"` | LDAP search scope between base, one or sub |
| env.AUTH_LDAP_URI | string | `nil` | URI of your LDAP server |
| env.AUTH_LDAP_USERNAME_ATTRIBUTE | string | `"uid"` | Attribute used for username |
| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) |
| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field |
| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. |
| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login |
| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) |
| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim |
| env.AUTH_OIDC_ISSUER | string | `nil` | Issuer URI of OIDC provider without trailing slash (/) |
| env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE | string | `nil` | Overwrite name attribute. By default, it will use preferred_username if it does not contain a @ and otherwise name. |
| env.AUTH_OIDC_SCOPE_OVERWRITE | string | `"openid email profile groups"` | Override the OIDC scopes |
| env.AUTH_PROVIDERS | string | `"credentials"` | Enabled authentication methods. Multiple providers can be enabled with by separating them with , (ex. AUTH_PROVIDERS=credentials,oidc, it is highly recommended to just enable one provider). |
| env.AUTH_SESSION_EXPIRY_TIME | string | `"30d"` | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") |
| env.ENABLE_DNS_CACHING | string | `"false"` | Enables dns caching. This is not yet working for all users. See #4006 |
| env.LOG_LEVEL | string | `"info"` | Log level to use. Possible values: debug/info/warn/error |
| env.NO_EXTERNAL_CONNECTION | string | `"false"` | Disables some requests that need internet connection |
| env.TZ | string | `"Europe/Paris"` | Your local time zone |
| envSecrets.authLdapCredentials.existingSecret | string | `"auth-ldap-secret"` | Name of existing secret containing LDAP credentials |
| envSecrets.authLdapCredentials.ldapBindingPassword | string | `"bind-password"` | Password for bind user secret key |
| envSecrets.authOidcCredentials.existingSecret | string | `"auth-oidc-secret"` | Name of existing secret containing OIDC credentials |
| envSecrets.authOidcCredentials.oidcClientId | string | `"oidc-client-id"` | ID of OIDC client (application) secret key |
| envSecrets.authOidcCredentials.oidcClientSecret | string | `"oidc-client-secret"` | Secret of OIDC client (application) secret key |
| envSecrets.dbCredentials.dbUrlKey | string | `"db-url"` | Secret key for DB_URL Example for external database: `mysql://username:password@host:port/homarrdb` or `postgresql://username:password@host:port/homarrdb` |
| envSecrets.dbCredentials.existingSecret | string | `"db-secret"` | Name of existing secret containing DB credentials |
| envSecrets.dbEncryption.existingSecret | string | `"db-encryption"` | Name of existing secret containing DB encryption |
| envSecrets.dbEncryption.key | string | `"db-encryption-key"` | Secret key for SECRET_ENCRYPTION_KEY can be generated with `openssl rand -hex 32` |
| fullnameOverride | string | `""` | Overrides chart's fullname |
| hostAliases | list | `[]` | Add static entries to /etc/hosts in the Pod. This is useful in the following cases: - You are running in a dual-stack cluster (IPv4 + IPv6) and want to force usage of IPv4 for specific hostnames - Your application is having DNS resolution issues or IPv6 preference issues - You need to override or simulate DNS entries without changing global DNS - You are running in an air-gapped or isolated environment without external DNS Example: hostAliases: - ip: "192.168.1.10" hostnames: - "example.com" - "example.internal" |
| httproute | object | `{"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"my-gateway","namespace":"default"}],"rules":[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]}` | Gateway API HTTPRoute configuration |
| httproute.enabled | bool | `false` | Enable HTTPRoute |
| httproute.hostnames | list | `["chart-example.local"]` | Hostnames this route matches (similar to ingress.hosts.host) |
| httproute.parentRefs | list | `[{"name":"my-gateway","namespace":"default"}]` | References to the parent Gateway(s) this route attaches to. Each item must include at least a `name`, and optionally a `namespace`. |
| httproute.rules | list | `[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | List of routing rules. Each rule can include: - matches: path/header/query matching - filters: optional transformations (redirects, header modifications, etc.) - backendRefs: one or more Kubernetes Services to forward traffic to |
| httproute.rules[0].filters | list | `[]` | Optional filters for this rule (default: empty) |
| httproute.rules[0].matches[0].path.type | string | `"PathPrefix"` | Path match type. One of: Exact, PathPrefix, RegularExpression |
| httproute.rules[0].matches[0].path.value | string | `"/"` | Path value to match |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository |
| image.tag | string | `"v1.74.0"` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Secrets for Docker registry |
| ingress.annotations | object | `{}` | Ingress annotations |
| ingress.enabled | bool | `false` | Enable ingress |
| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/"}]}]` | Ingress hosts configuration |
| ingress.ingressClassName | string | `""` | Ingress class name |
| ingress.tls | list | `[]` | Ingress TLS configuration |
| livenessProbe.failureThreshold | int | `3` | Failure threshold for liveness probe - number of consecutive failures before pod is restarted |
| livenessProbe.httpGet.path | string | `"/api/health/live"` | This is the liveness check endpoint used by Kubernetes to determine if the application is still running. |
| livenessProbe.httpGet.port | int | `7575` | The port on which the liveness check will be performed. This must be the same as the container port exposed by the application. |
| livenessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the liveness probe starts |
| livenessProbe.periodSeconds | int | `10` | Period in seconds between liveness probe checks |
| livenessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each liveness probe check |
| nameOverride | string | `""` | Overrides chart's name |
| nodeSelector | object | `{}` | Node selectors for pod scheduling |
| persistence.homarrDatabase.accessMode | string | `"ReadWriteOnce"` | homarr-database access mode |
| persistence.homarrDatabase.enabled | bool | `false` | Enable homarr-database persistent storage |
| persistence.homarrDatabase.mountPath | string | `"/appdata"` | homarr-database mount path inside the pod |
| persistence.homarrDatabase.name | string | `"homarr-database"` | homarr-database persistent storage name |
| persistence.homarrDatabase.size | string | `"50Mi"` | homarr-database storage size |
| persistence.homarrDatabase.storageClassName | string | `"local-path"` | homarr-database storage class name |
| persistence.homarrDatabase.volumeClaimName | string | `""` | homarr-database optional volumeClaimName to target specific PV |
| persistence.homarrTrustedCerts.certificates | string | `nil` | homarr-trusted-certificates certificates, each entry will become a new trusted certificate as a dedicated file (works only for "configmap" and "secret" mode) |
| persistence.homarrTrustedCerts.enabled | bool | `false` | Enable trusted certificates persistence |
| persistence.homarrTrustedCerts.existingSecretKeys | string | `nil` | List of keys (filenames) to mount from the existing secret (used only when type is "existingSecret") |
| persistence.homarrTrustedCerts.existingSecretName | string | `""` | Name of the existing Kubernetes Secret to mount (required if type is "existingSecret") |
| persistence.homarrTrustedCerts.mountPath | string | `"/appdata/trusted-certificates"` | homarr-trusted-certificates mount path inside the pod |
| persistence.homarrTrustedCerts.type | string | `"configmap"` | Persistence mode can be : configmap (declarative), secret (declarative) or existingSecret (mount an existing Kubernetes Secret by name and specify which keys to mount as files) |
| podAnnotations | object | `{}` | Pod annotations |
| podLabels | object | `{}` | Pod labels |
| podSecurityContext | object | `{}` | Pod security context |
| rbac | object | `{"enabled":false}` | Enable RBAC resources for Kubernetes integration Creates Role, ClusterRole, and associated bindings for Homarr's Kubernetes features |
| rbac.enabled | bool | `false` | Enable to create RBAC resources and activate Kubernetes integration |
| readinessProbe.failureThreshold | int | `3` | Failure threshold for readiness probe - number of consecutive failures before pod is considered unready |
| readinessProbe.httpGet.path | string | `"/api/health/ready"` | This is the readiness check endpoint used by Kubernetes to determine if the application is ready to handle traffic. |
| readinessProbe.httpGet.port | int | `7575` | The port on which the readiness check will be performed. This must match the container's exposed port. |
| readinessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the readiness probe starts. increase this value if the pod is slow to fully start. |
| readinessProbe.periodSeconds | int | `10` | Period in seconds between readiness probe checks |
| readinessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each readiness probe check |
| replicaCount | int | `1` | Number of replicas |
| resources | object | `{}` | Resource configuration |
| securityContext | object | `{}` | Security context |
| service.enabled | bool | `true` | Enable service |
| service.ipFamilies | list | `[]` | List of IP families to use for the service. Examples: - ["IPv4"] - ["IPv6"] - ["IPv4", "IPv6"] for dual-stack Leave empty to use cluster default behavior |
| service.ipFamilyPolicy | string | `"SingleStack"` | Defines how the service assigns IP families (IPv4/IPv6) Possible values: - SingleStack (default): Only one IP family, usually IPv4 - PreferDualStack: Use dual-stack if the cluster supports it, fallback to single - RequireDualStack: Fail if dual-stack cannot be assigned |
| service.ports.app.port | int | `7575` | Service port |
| service.ports.app.protocol | string | `"TCP"` | Service protocol |
| service.ports.app.targetPort | string | `"http"` | Service target port |
| service.type | string | `"ClusterIP"` | Service type |
| strategyType | string | `"RollingUpdate"` | `strategyType` specifies the strategy used to replace old Pods by new ones. `strategyType` can be `"Recreate"` or `"RollingUpdate"`. `"RollingUpdate"` is the default value and updates Pods in a rolling update fashion. `"Recreate"` will kill all existing Pods before new ones are created. The `"Recreate"` strategy is necessary when persistent volume's `accessMode` is set to `"ReadWriteOnce"` when using `helm upgrade`, as pod volume attachments to an existing PersistentVolumeClaim need to be cleared before a new pod can attach to it. |
| tolerations | list | `[]` | Node tolerations for pod scheduling |
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| additionalObjects | list | `[]` | Additional resources to deploy. These objects are templated. |
| affinity | object | `{}` | Node affinity for pod scheduling |
| autoscaling.enabled | bool | `false` | Enable autoscaling |
| autoscaling.maxReplicas | int | `100` | Maximum replicas |
| autoscaling.minReplicas | int | `1` | Minimum replicas |
| autoscaling.targetCPUUtilizationPercentage | int | `80` | Target CPU utilization for autoscaling |
| containerPorts | object | `{"http":{"port":7575,"protocol":"TCP"}}` | containerPorts defines the ports to open on the container. It is a map where each entry specifies: - `port` (int) (required): The port number to expose inside the container. - `protocol` (string) (required): The network protocol (TCP or UDP) used for the port. - `disabled` (bool) : Optional flag to disable this port (defaults to false). Can be overridden via Helm values. By default, this configuration exposes TCP port 7575 with the name `http`. |
| database.migrationEnabled | bool | `true` | Database migration configuration. DB_MIGRATIONS_DISABLED Set to `true` to disable database migrations. Migrations are enabled by default (`false`). |
| database.type | string | `"sqlite"` | Database type: sqlite, mysql or postgresql |
| env.AUTH_COOKIE_PREFIX | string | `"homarr"` | Prefix used for all authentication cookies. Change if you run another Auth.js/NextAuth app on the same hostname to avoid cookie name collisions. Only letters, numbers, hyphens and underscores. |
| env.AUTH_LDAP_BASE | string | `nil` | Base dn of your LDAP server |
| env.AUTH_LDAP_BIND_DN | string | `nil` | User used for finding users and groups |
| env.AUTH_LDAP_GROUP_CLASS | string | `"groupOfUniqueNames"` | Class used for querying groups |
| env.AUTH_LDAP_GROUP_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user's groups search filter (& based) |
| env.AUTH_LDAP_GROUP_MEMBER_ATTRIBUTE | string | `"member"` | Attribute used for querying group member |
| env.AUTH_LDAP_GROUP_MEMBER_USER_ATTRIBUTE | string | `"dn"` | User attribute used for comparing with group member |
| env.AUTH_LDAP_SEARCH_SCOPE | string | `"base"` | LDAP search scope between base, one or sub |
| env.AUTH_LDAP_URI | string | `nil` | URI of your LDAP server |
| env.AUTH_LDAP_USERNAME_ATTRIBUTE | string | `"uid"` | Attribute used for username |
| env.AUTH_LDAP_USERNAME_FILTER_EXTRA_ARG | string | `nil` | Extra arguments for user search filter (& based) |
| env.AUTH_LDAP_USER_MAIL_ATTRIBUTE | string | `"mail"` | Attribute used for mail field |
| env.AUTH_LOGOUT_REDIRECT_URL | string | `nil` | URL to redirect to after clicking logging out. |
| env.AUTH_OIDC_AUTO_LOGIN | string | `"false"` | Automatically redirect to OIDC login |
| env.AUTH_OIDC_CLIENT_NAME | string | `"OIDC"` | Display name of provider (in login screen) |
| env.AUTH_OIDC_GROUPS_ATTRIBUTE | string | `"groups"` | Attribute used for groups (roles) claim |
| env.AUTH_OIDC_ISSUER | string | `nil` | Issuer URI of OIDC provider without trailing slash (/) |
| env.AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE | string | `nil` | Overwrite name attribute. By default, it will use preferred_username if it does not contain a @ and otherwise name. |
| env.AUTH_OIDC_SCOPE_OVERWRITE | string | `"openid email profile groups"` | Override the OIDC scopes |
| env.AUTH_PROVIDERS | string | `"credentials"` | Enabled authentication methods. Multiple providers can be enabled with by separating them with , (ex. AUTH_PROVIDERS=credentials,oidc, it is highly recommended to just enable one provider). |
| env.AUTH_SESSION_EXPIRY_TIME | string | `"30d"` | Time for the session to time out. Can be set as pure number, which will automatically be used in seconds, or followed by s, m, h or d for seconds, minutes, hours or days. (ex: "30m") |
| env.ENABLE_DNS_CACHING | string | `"false"` | Enables dns caching. This is not yet working for all users. See #4006 |
| env.LOG_LEVEL | string | `"info"` | Log level to use. Possible values: debug/info/warn/error |
| env.NO_EXTERNAL_CONNECTION | string | `"false"` | Disables some requests that need internet connection |
| env.TZ | string | `"Europe/Paris"` | Your local time zone |
| envSecrets.authLdapCredentials.existingSecret | string | `"auth-ldap-secret"` | Name of existing secret containing LDAP credentials |
| envSecrets.authLdapCredentials.ldapBindingPassword | string | `"bind-password"` | Password for bind user secret key |
| envSecrets.authOidcCredentials.existingSecret | string | `"auth-oidc-secret"` | Name of existing secret containing OIDC credentials |
| envSecrets.authOidcCredentials.oidcClientId | string | `"oidc-client-id"` | ID of OIDC client (application) secret key |
| envSecrets.authOidcCredentials.oidcClientSecret | string | `"oidc-client-secret"` | Secret of OIDC client (application) secret key |
| envSecrets.dbCredentials.dbUrlKey | string | `"db-url"` | Secret key for DB_URL Example for external database: `mysql://username:password@host:port/homarrdb` or `postgresql://username:password@host:port/homarrdb` |
| envSecrets.dbCredentials.existingSecret | string | `"db-secret"` | Name of existing secret containing DB credentials |
| envSecrets.dbEncryption.existingSecret | string | `"db-encryption"` | Name of existing secret containing DB encryption |
| envSecrets.dbEncryption.key | string | `"db-encryption-key"` | Secret key for SECRET_ENCRYPTION_KEY can be generated with `openssl rand -hex 32` |
| fullnameOverride | string | `""` | Overrides chart's fullname |
| hostAliases | list | `[]` | Add static entries to /etc/hosts in the Pod. This is useful in the following cases: - You are running in a dual-stack cluster (IPv4 + IPv6) and want to force usage of IPv4 for specific hostnames - Your application is having DNS resolution issues or IPv6 preference issues - You need to override or simulate DNS entries without changing global DNS - You are running in an air-gapped or isolated environment without external DNS Example: hostAliases: - ip: "192.168.1.10" hostnames: - "example.com" - "example.internal" |
| httproute | object | `{"enabled":false,"hostnames":["chart-example.local"],"parentRefs":[{"name":"my-gateway","namespace":"default"}],"rules":[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]}` | Gateway API HTTPRoute configuration |
| httproute.enabled | bool | `false` | Enable HTTPRoute |
| httproute.hostnames | list | `["chart-example.local"]` | Hostnames this route matches (similar to ingress.hosts.host) |
| httproute.parentRefs | list | `[{"name":"my-gateway","namespace":"default"}]` | References to the parent Gateway(s) this route attaches to. Each item must include at least a `name`, and optionally a `namespace`. |
| httproute.rules | list | `[{"backendRefs":[{"name":"homarr","port":7575}],"filters":[],"matches":[{"path":{"type":"PathPrefix","value":"/"}}]}]` | List of routing rules. Each rule can include: - matches: path/header/query matching - filters: optional transformations (redirects, header modifications, etc.) - backendRefs: one or more Kubernetes Services to forward traffic to |
| httproute.rules[0].filters | list | `[]` | Optional filters for this rule (default: empty) |
| httproute.rules[0].matches[0].path.type | string | `"PathPrefix"` | Path match type. One of: Exact, PathPrefix, RegularExpression |
| httproute.rules[0].matches[0].path.value | string | `"/"` | Path value to match |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.repository | string | `"ghcr.io/homarr-labs/homarr"` | Image repository |
| image.tag | string | `"v1.74.0"` | Overrides the image tag whose default is the chart appVersion |
| imagePullSecrets | list | `[]` | Secrets for Docker registry |
| ingress.annotations | object | `{}` | Ingress annotations |
| ingress.enabled | bool | `false` | Enable ingress |
| ingress.hosts | list | `[{"host":"chart-example.local","paths":[{"path":"/"}]}]` | Ingress hosts configuration |
| ingress.ingressClassName | string | `""` | Ingress class name |
| ingress.tls | list | `[]` | Ingress TLS configuration |
| livenessProbe.failureThreshold | int | `3` | Failure threshold for liveness probe - number of consecutive failures before pod is restarted |
| livenessProbe.httpGet.path | string | `"/api/health/live"` | This is the liveness check endpoint used by Kubernetes to determine if the application is still running. |
| livenessProbe.httpGet.port | int | `7575` | The port on which the liveness check will be performed. This must be the same as the container port exposed by the application. |
| livenessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the liveness probe starts |
| livenessProbe.periodSeconds | int | `10` | Period in seconds between liveness probe checks |
| livenessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each liveness probe check |
| nameOverride | string | `""` | Overrides chart's name |
| nodeSelector | object | `{}` | Node selectors for pod scheduling |
| persistence.homarrDatabase.accessMode | string | `"ReadWriteOnce"` | homarr-database access mode |
| persistence.homarrDatabase.enabled | bool | `false` | Enable homarr-database persistent storage |
| persistence.homarrDatabase.mountPath | string | `"/appdata"` | homarr-database mount path inside the pod |
| persistence.homarrDatabase.name | string | `"homarr-database"` | homarr-database persistent storage name |
| persistence.homarrDatabase.size | string | `"50Mi"` | homarr-database storage size |
| persistence.homarrDatabase.storageClassName | string | `"local-path"` | homarr-database storage class name |
| persistence.homarrDatabase.volumeClaimName | string | `""` | homarr-database optional volumeClaimName to target specific PV |
| persistence.homarrTrustedCerts.certificates | string | `nil` | homarr-trusted-certificates certificates, each entry will become a new trusted certificate as a dedicated file (works only for "configmap" and "secret" mode) |
| persistence.homarrTrustedCerts.enabled | bool | `false` | Enable trusted certificates persistence |
| persistence.homarrTrustedCerts.existingSecretKeys | string | `nil` | List of keys (filenames) to mount from the existing secret (used only when type is "existingSecret") |
| persistence.homarrTrustedCerts.existingSecretName | string | `""` | Name of the existing Kubernetes Secret to mount (required if type is "existingSecret") |
| persistence.homarrTrustedCerts.mountPath | string | `"/appdata/trusted-certificates"` | homarr-trusted-certificates mount path inside the pod |
| persistence.homarrTrustedCerts.type | string | `"configmap"` | Persistence mode can be : configmap (declarative), secret (declarative) or existingSecret (mount an existing Kubernetes Secret by name and specify which keys to mount as files) |
| podAnnotations | object | `{}` | Pod annotations |
| podLabels | object | `{}` | Pod labels |
| podSecurityContext | object | `{}` | Pod security context |
| rbac | object | `{"enabled":false}` | Enable RBAC resources for Kubernetes integration Creates Role, ClusterRole, and associated bindings for Homarr's Kubernetes features |
| rbac.enabled | bool | `false` | Enable to create RBAC resources and activate Kubernetes integration |
| readinessProbe.failureThreshold | int | `3` | Failure threshold for readiness probe - number of consecutive failures before pod is considered unready |
| readinessProbe.httpGet.path | string | `"/api/health/ready"` | This is the readiness check endpoint used by Kubernetes to determine if the application is ready to handle traffic. |
| readinessProbe.httpGet.port | int | `7575` | The port on which the readiness check will be performed. This must match the container's exposed port. |
| readinessProbe.initialDelaySeconds | int | `10` | Initial delay in seconds before the readiness probe starts. increase this value if the pod is slow to fully start. |
| readinessProbe.periodSeconds | int | `10` | Period in seconds between readiness probe checks |
| readinessProbe.timeoutSeconds | int | `1` | Timeout in seconds for each readiness probe check |
| replicaCount | int | `1` | Number of replicas |
| resources | object | `{}` | Resource configuration |
| securityContext | object | `{}` | Security context |
| service.enabled | bool | `true` | Enable service |
| service.ipFamilies | list | `[]` | List of IP families to use for the service. Examples: - ["IPv4"] - ["IPv6"] - ["IPv4", "IPv6"] for dual-stack Leave empty to use cluster default behavior |
| service.ipFamilyPolicy | string | `"SingleStack"` | Defines how the service assigns IP families (IPv4/IPv6) Possible values: - SingleStack (default): Only one IP family, usually IPv4 - PreferDualStack: Use dual-stack if the cluster supports it, fallback to single - RequireDualStack: Fail if dual-stack cannot be assigned |
| service.ports.app.port | int | `7575` | Service port |
| service.ports.app.protocol | string | `"TCP"` | Service protocol |
| service.ports.app.targetPort | string | `"http"` | Service target port |
| service.type | string | `"ClusterIP"` | Service type |
| strategyType | string | `"RollingUpdate"` | `strategyType` specifies the strategy used to replace old Pods by new ones. `strategyType` can be `"Recreate"` or `"RollingUpdate"`. `"RollingUpdate"` is the default value and updates Pods in a rolling update fashion. `"Recreate"` will kill all existing Pods before new ones are created. The `"Recreate"` strategy is necessary when persistent volume's `accessMode` is set to `"ReadWriteOnce"` when using `helm upgrade`, as pod volume attachments to an existing PersistentVolumeClaim need to be cleared before a new pod can attach to it. |
| tolerations | list | `[]` | Node tolerations for pod scheduling |
---
Autogenerated from chart metadata using [helm-docs](https://github.com/norwoodj/helm-docs)
@@ -14,7 +14,7 @@ Homarr can be seamlessly integrated into your Home Assistant setup with this add
## Installation
1. **Add Repository**: Navigate to your Home Assistant Supervisor panel, and go to the Add-on Store. Add the following repository URL: `https://github.com/Wiggen94/ha-homarr-v1-server` or click this button
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2FWiggen94%2Fha-homarr-v1-server)
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2FWiggen94%2Fha-homarr-v1-server)
2. **Install the Add-on**: Search for "Homarr Server" in the Add-on Store and select it. Click on "Install" to start the installation process.
@@ -1,7 +1,5 @@
# Portainer
## Portainer Stacks
### Prerequisites
- [Portainer Stacks](https://www.portainer.io/blog/stacks-docker-compose-the-portainer-way)
@@ -25,14 +23,15 @@ services:
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
ports:
- "7575:7575"
- '7575:7575'
```
:::note
Portainer will mount the files to your host machine. Please make sure to replace `<your-path>` from the docker run command with your desired storage location. The path must be absolute.
Portainer will mount the files to your host machine. Please make sure to replace ``<your-path>`` from the docker run command with your desired storage location. The path must be absolute.
:::
Then, click on `Deploy the stack` and wait until the container has started.
Then, click on ``Deploy the stack`` and wait until the container has started.
:::tip
@@ -44,5 +43,5 @@ If you're unsure how to create a stack, checkout the [Portainer Documentation](h
To update Homarr using Docker Compose or Portainer Stacks, simply pull the latest image and restart the container.
1. Run `docker pull ghcr.io/homarr-labs/homarr:latest` to pull the latest image.
2. Re-run `docker-compose up -d` or re-deploy the stack in Portainer.
1. Run ``docker pull ghcr.io/homarr-labs/homarr:latest`` to pull the latest image.
2. Re-run ``docker-compose up -d`` or re-deploy the stack in Portainer.
@@ -31,7 +31,7 @@ After you've installed Homarr, you can find a new LXC "homarr".
# Updating
Updating is as simple as executing
Updating is as simple as executing
```bash
update
@@ -49,19 +49,17 @@ Some Homarr features require setting environment variables. When installing Homa
# Troubleshooting
If something does not work after an update, please first try rebooting the LXC or the homarr Service with
If something does not work after an update, please first try rebooting the LXC or the homarr Service with
```bash
systemctl restart homarr
```
# Getting help
- If you are looking for help with the script, updating the LXC or troubleshooting the installer, please get in contact with the support of the community scripts. They provide support on their [Discord](https://discord.gg/2wvnMDgdnU). If you found vulnerabilities, bugs or have a request for the script, [create a new issue here](https://github.com/community-scripts/ProxmoxVE/issues/new/choose).
- If you are looking to report a bug, vulnerability or request a new feature, use https://github.com/homarr-labs/homarr . You can ask for help on our Discord.
# How to find logs
Homarr runs under a system service called `homarr`, so getting the logs is a simple as using the following command inside the LXC (Container) Shell
```bash
@@ -4,6 +4,6 @@
QNAP installation guide has not updated to 1.0 yet. There guide is still for 0.15.10.
:::
When installing on QNAP, we recommend you to install Homarr using [Container Station](https://www.qnap.com/en/software/container-station) or [this third-party image](https://www.myqnap.org/product/homarr/).
When installing on QNAP, we recommend you to install Homarr using [Container Station]( https://www.qnap.com/en/software/container-station) or [this third-party image](https://www.myqnap.org/product/homarr/).
There is also a guide available, but note that it is written in Chinese: https://post.smzdm.com/p/awzm7op2/
@@ -1,4 +1,4 @@
# Runtipi
# Runtipi
Runtipi is a self-hosted orchestrator that makes it easy to manage and run multiple services on a single server. It is based on Docker and comes with a simple web interface to manage your services. Runtipi is designed to be easy to use, so you don't have to worry about manual configuration or networking.
@@ -11,7 +11,6 @@ https://runtipi.io
# Installation
Installing Homarr is pretty untuitive, the app is available in the default appstore :
1. Search for **"Homarr"** in the App-Store page and select it
2. Click on **"Install"**.
3. Choose the method you want to access Homarr, through any port or domain name then select **"Install"**
@@ -19,15 +18,13 @@ Installing Homarr is pretty untuitive, the app is available in the default appst
# Maintenance
After installation, all you need is available from the WebUI in **"My apps"** > **"Homarr"** :
- Start/Stop
- Update
- Backup
- Logs
- Start/Stop
- Update
- Backup
- Logs
If you like to tinker with your apps, you can also edit your app's docker-compose with a [user-config](https://runtipi.io/docs/guides/customize-app-config).
# Getting help
- If you are looking for help with anything in Runtipi. They provide support on their [Discord](https://discord.gg/Bu9qEPnHsc) and [forums](https://forums.runtipi.io/). If you found vulnerabilities or bugs, [create a new issue here](https://github.com/runtipi/runtipi-appstore/issues).
- If you are looking to report a bug, vulnerability or request a new feature for Homarr, use https://github.com/homarr-labs/homarr . You can ask for help on our Discord.
- If you are looking to report a bug, vulnerability or request a new feature for Homarr, use https://github.com/homarr-labs/homarr . You can ask for help on our Discord.
@@ -5,7 +5,6 @@ Saltbox installation command has not updated to 1.0 yet. There command still run
:::
You can also use this to install on Saltbox:
```bash
sb install sandbox-homarr
```
```
@@ -28,29 +28,15 @@ This method is only useful, when you want to develop on Homarr or extend it with
## Steps
<ol>
<li>
Clone the Repository using <code>git clone https://github.com/homarr-labs/homarr.git</code>
</li>
<li>
Enter the created directory using <code>cd homarr</code>
</li>
<li>
Install all dependencies using <code>pnpm install</code>
</li>
<li>
Copy the `.env.example` file to `.env` and replace `DB_URL` with the full path you want to use for the sqlite
database file. For example `DB_URL='/home/username/homarr/db.sqlite`
</li>
<li>
Replace `AUTH_SECRET` and `SECRET_ENCRYPTION_KEY` both with a secure random string by using `openssl rand -hex 32`
</li>
<li>Clone the Repository using <code>git clone https://github.com/homarr-labs/homarr.git</code></li>
<li>Enter the created directory using <code>cd homarr</code></li>
<li>Install all dependencies using <code>pnpm install</code></li>
<li>Copy the `.env.example` file to `.env` and replace `DB_URL` with the full path you want to use for the sqlite database file. For example `DB_URL='/home/username/homarr/db.sqlite`</li>
<li>Replace `AUTH_SECRET` and `SECRET_ENCRYPTION_KEY` both with a secure random string by using `openssl rand -hex 32`</li>
<li>Replace `CRON_JOB_API_KEY` with a secure random string by using `openssl rand -base64 32`</li>
<li>Run `pnpm run db:migration:sqlite:run` and wait that it completes</li>
<li>Build the source using `pnpm build`</li>
<li>
Copy better-sqlite3.node files with `mkdir build` and `cp
./node_modules/better-sqlite3/build/Release/better_sqlite3.node ./build/better_sqlite3.node`
</li>
<li>Copy better-sqlite3.node files with `mkdir build` and `cp ./node_modules/better-sqlite3/build/Release/better_sqlite3.node ./build/better_sqlite3.node`</li>
<li>Start a redis server using `redis-server`</li>
<li>Run the server using `pnpm start`</li>
<li>Open `http://localhost:3000` in your browser</li>
@@ -1,5 +1,5 @@
import Link from "@docusaurus/Link";
import { IconDownload } from "@tabler/icons-react";
import Link from '@docusaurus/Link';
import { IconDownload } from '@tabler/icons-react';
# Unraid
@@ -14,17 +14,17 @@ You can install Homarr directly from your Unraid Dashboard, no terminal required
<Link
data-attr="Install Unraid Community Apps"
className={"button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800"}
className={'button button--secondary button--lg rounded-3xl dark:border-zinc-600 dark:bg-zinc-800'}
to="https://forums.unraid.net/topic/38582-plug-in-community-applications/"
>
<div className={"flex items-center gap-3"}>
<div className={'flex items-center gap-3'}>
<span className={"dark:text-gray-200"}>Install community applications plugin</span>
<IconDownload className={"dark:text-gray-200"} size={20} />
</div>
</Link>
<br />
<br />
<br/>
<br/>
:::tip
@@ -46,3 +46,4 @@ After you've installed the Community Apps Plugin, you can install Homarr from th
After you've installed Homarr, you can find it under the tab "Docker".
The official support thread is located [here](https://forums.unraid.net/topic/123478-support-smartphonelover-homarr/), but we prefer to communicate over GitHub and Discord instead.
@@ -1,44 +1,37 @@
---
title: "AdGuard Home"
title: 'AdGuard Home'
description: "AdGuard Home is a network-wide software for blocking ads and trackers, providing a safer and faster internet experience."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconAd, IconDeviceGamepad } from "@tabler/icons-react";
import { adguardHomeIntegration } from ".";
import { dnsHoleSummaryWidget } from "@site/docs/widgets/dns-hole-summary";
import { dnsHoleControlsWidget } from "@site/docs/widgets/dns-hole-controls";
import { adguardHomeIntegration } from '.';
import { dnsHoleSummaryWidget } from '@site/docs/widgets/dns-hole-summary';
import { dnsHoleControlsWidget } from '@site/docs/widgets/dns-hole-controls';
<IntegrationHeader integration={adguardHomeIntegration} categories={["DNS Hole"]} />
<IntegrationHeader
integration={adguardHomeIntegration}
categories={['DNS Hole']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: dnsHoleSummaryWidget,
},
{
widget: dnsHoleControlsWidget,
},
]}
items={[{
widget: dnsHoleSummaryWidget,
},{
widget: dnsHoleControlsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: []
}]} />
+23 -28
View File
@@ -1,45 +1,40 @@
---
title: "Anchor"
title: 'Anchor'
description: "Anchor is a self-hosted note-taking application."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { anchorIntegration } from ".";
import { anchorNoteWidget } from "@site/docs/widgets/anchor-note";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { anchorIntegration } from '.';
import { anchorNoteWidget } from '@site/docs/widgets/anchor-note';
<IntegrationHeader integration={anchorIntegration} categories={["Notes"]} />
<IntegrationHeader
integration={anchorIntegration}
categories={['Notes']}
/>
The Anchor integration lets Homarr connect to your Anchor instance so you can display and edit selected notes directly from your dashboard.
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: anchorNoteWidget,
},
]}
items={[{
widget: anchorNoteWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
"Open your Anchor instance.",
"Generate or copy an API key from Anchor settings.",
"Paste that key into the API Key secret field in Homarr.",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
'Open your Anchor instance.',
'Generate or copy an API key from Anchor settings.',
'Paste that key into the API Key secret field in Homarr.'
]
}]} />
+23 -28
View File
@@ -1,43 +1,38 @@
---
title: "Aria2"
title: 'Aria2'
description: "Lightweight multiprotocol & multi-source command-line download utility. It supports HTTP/HTTPS, FTP, SFTP, BitTorrent and Metalink"
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { aria2Integration } from ".";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { aria2Integration } from '.';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={aria2Integration} categories={["Download", "Multiprotocol", "Multi-source"]} />
<IntegrationHeader
integration={aria2Integration}
categories={['Download', 'Multiprotocol', 'Multi-source']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: downloadsWidget,
},
]}
items={[{
widget: downloadsWidget,
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
{
credentials: [],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[
{
credentials: ['apiKey'],
steps: []
},
{
credentials: [],
steps: []
}]} />
@@ -1,40 +1,41 @@
---
title: "Audiobookshelf"
title: 'Audiobookshelf'
description: "Audiobookshelf is a self-hosted audiobook and podcast server that lets you manage and stream your audio library."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { audiobookshelfIntegration } from ".";
import { audioStatsWidget } from "@site/docs/widgets/audio-stats";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { audiobookshelfIntegration } from '.';
import { audioStatsWidget } from '@site/docs/widgets/audio-stats';
<IntegrationHeader integration={audiobookshelfIntegration} categories={["Media"]} />
<IntegrationHeader
integration={audiobookshelfIntegration}
categories={['Media']}
/>
Audiobookshelf provides library statistics including audiobook and podcast counts, listening time, and active sessions.
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: audioStatsWidget }]} />
<IntegrationCapabilites
items={[
{ widget: audioStatsWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
"Open your Audiobookshelf instance",
"Go to Settings → Users",
"Click on your user account",
"Copy the API token from the user details",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
'Open your Audiobookshelf instance',
'Go to Settings → Users',
'Click on your user account',
'Copy the API token from the user details',
]
}]} />
+24 -28
View File
@@ -1,30 +1,31 @@
---
title: "Bazarr"
title: 'Bazarr'
description: "Bazarr is a companion application to Sonarr and Radarr that manages and downloads subtitles."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { bazarrIntegration } from ".";
import { bazarrWidget } from "@site/docs/widgets/bazarr";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { bazarrIntegration } from '.';
import { bazarrWidget } from '@site/docs/widgets/bazarr';
<IntegrationHeader integration={bazarrIntegration} categories={["Subtitles", "Media"]} />
<IntegrationHeader
integration={bazarrIntegration}
categories={['Subtitles', 'Media']}
/>
Bazarr complements Sonarr and Radarr by managing subtitle downloads. The Homarr integration uses your Bazarr API key to fetch missing subtitle counts and health badges.
### Screenshots
import apiKeySettings from "./img/api-key-settings.png";
import widgetOverview from "@site/docs/widgets/bazarr/img/widget-overview.png";
import apiKeySettings from './img/api-key-settings.png';
import widgetOverview from '@site/docs/widgets/bazarr/img/widget-overview.png';
<div className="flex flex-col gap-4">
<div>
<p>
API key location in Bazarr (<strong>Settings → General → Security</strong>):
</p>
<p>API key location in Bazarr (<strong>Settings → General → Security</strong>):</p>
<img src={apiKeySettings} alt="bazarr-api-key-settings" width={128 * 4} />
</div>
<div>
@@ -34,25 +35,20 @@ import widgetOverview from "@site/docs/widgets/bazarr/img/widget-overview.png";
</div>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: bazarrWidget }]} />
<IntegrationCapabilites
items={[{ widget: bazarrWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
You can find your Bazarr API key under **Settings → General → Security**.
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
"Open Bazarr and go to Settings → General → Security.",
"Copy the API key and paste it into Homarr when creating the integration.",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
"Open Bazarr and go to Settings → General → Security.",
"Copy the API key and paste it into Homarr when creating the integration.",
],
}]} />
+28 -32
View File
@@ -1,22 +1,25 @@
---
title: "Beszel"
title: 'Beszel'
description: "Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { beszelIntegration } from ".";
import { beszelSystemStatsWidget } from "@site/docs/widgets/beszel-system-stats";
import { beszelSystemGridWidget } from "@site/docs/widgets/beszel-system-grid";
import { beszelSystemTableWidget } from "@site/docs/widgets/beszel-system-table";
import { beszelAlertsWidget } from "@site/docs/widgets/beszel-alerts";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { beszelIntegration } from '.';
import { beszelSystemStatsWidget } from '@site/docs/widgets/beszel-system-stats';
import { beszelSystemGridWidget } from '@site/docs/widgets/beszel-system-grid';
import { beszelSystemTableWidget } from '@site/docs/widgets/beszel-system-table';
import { beszelAlertsWidget } from '@site/docs/widgets/beszel-alerts';
import beszelDashboard from "./img/beszel-dashboard.png";
import beszelDashboard from './img/beszel-dashboard.png';
<IntegrationHeader integration={beszelIntegration} categories={["System Monitoring"]} />
<IntegrationHeader
integration={beszelIntegration}
categories={['System Monitoring']}
/>
Beszel monitors your servers in real time via lightweight agents, collecting CPU, memory, disk, network, GPU, temperature, and Docker container metrics. It stores historical data and supports configurable alerts.
@@ -25,33 +28,26 @@ Beszel monitors your servers in real time via lightweight agents, collecting CPU
<img src={beszelDashboard} alt="Beszel dashboard overview showing all four widgets" />
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{ widget: beszelSystemStatsWidget },
{ widget: beszelSystemGridWidget },
{ widget: beszelSystemTableWidget },
{ widget: beszelAlertsWidget },
]}
items={[
{ widget: beszelSystemStatsWidget },
{ widget: beszelSystemGridWidget },
{ widget: beszelSystemTableWidget },
{ widget: beszelAlertsWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [
"Open your Beszel hub instance (default port 8090)",
"Use the same credentials you use to log in to the Beszel web UI",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [
'Open your Beszel hub instance (default port 8090)',
'Use the same credentials you use to log in to the Beszel web UI',
]
}]} />
:::tip
Beszel uses PocketBase under the hood. The default admin credentials are set during the initial Beszel setup.
+30 -37
View File
@@ -1,24 +1,29 @@
---
title: "Coolify"
description: "Self-hosted PaaS and deployment platform"
title: 'Coolify'
description: 'Self-hosted PaaS and deployment platform'
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { coolifyIntegration } from ".";
import { coolifyWidget } from "../../widgets/coolify";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { coolifyIntegration } from '.';
import { coolifyWidget } from '../../widgets/coolify';
<IntegrationHeader integration={coolifyIntegration} categories={["System monitoring", "Deployment"]} />
<IntegrationHeader
integration={coolifyIntegration}
categories={['System monitoring', 'Deployment']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: coolifyWidget }]} />
<IntegrationCapabilites
items={[
{ widget: coolifyWidget }
]}
/>
### Adding the integration
<AddingIntegration />
:::info URL configuration
@@ -26,28 +31,16 @@ Use the root URL of your Coolify instance, for example `https://coolify.example.
:::
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
"Open your Coolify instance and make sure you are in the team you want Homarr to monitor.",
<span>
Enable API access under <strong>Settings &gt; Advanced</strong> if it is disabled.
</span>,
<span>
Go to <strong>Security &gt; API Tokens</strong>.
</span>,
'Enter a name, e.g. "Homarr".',
<span>
Select the <code>read</code> permission. The Coolify widget only reads version, server, project, application,
and service data.
</span>,
'Click "Create".',
"Copy the generated token immediately. Coolify only shows it once.",
"Paste the token into the API Key field in Homarr.",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
'Open your Coolify instance and make sure you are in the team you want Homarr to monitor.',
(<span>Enable API access under <strong>Settings &gt; Advanced</strong> if it is disabled.</span>),
(<span>Go to <strong>Security &gt; API Tokens</strong>.</span>),
'Enter a name, e.g. "Homarr".',
(<span>Select the <code>read</code> permission. The Coolify widget only reads version, server, project, application, and service data.</span>),
'Click "Create".',
'Copy the generated token immediately. Coolify only shows it once.',
'Paste the token into the API Key field in Homarr.'
]
}]} />
+19 -20
View File
@@ -1,33 +1,32 @@
---
title: "Dash."
title: 'Dash.'
description: "Dash. is a system performance and resource monitoring tool"
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { dashDotIntegration } from ".";
import { healthMonitoringWidget } from "@site/docs/widgets/health-monitoring";
import { systemResourcesWidget } from "@site/docs/widgets/system-resources";
import { dashDotIntegration } from '.';
import { healthMonitoringWidget } from '@site/docs/widgets/health-monitoring';
import { systemResourcesWidget } from '@site/docs/widgets/system-resources';
<IntegrationHeader integration={dashDotIntegration} categories={["System monitoring"]} />
<IntegrationHeader
integration={dashDotIntegration}
categories={['System monitoring']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: healthMonitoringWidget,
},
{
widget: systemResourcesWidget,
},
]}
items={[{
widget: healthMonitoringWidget
}, {
widget: systemResourcesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
<AddingIntegration />
+20 -25
View File
@@ -1,41 +1,36 @@
---
title: "Deluge"
title: 'Deluge'
description: "Deluge is a lightweight, Free Software, cross-platform BitTorrent client."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IconDownload } from "@tabler/icons-react";
import Alert from "@theme/Admonition";
import { delugeIntegration } from ".";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import Alert from '@theme/Admonition';
import { delugeIntegration } from '.';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={delugeIntegration} categories={["Torrent client"]} />
<IntegrationHeader
integration={delugeIntegration}
categories={['Torrent client']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: downloadsWidget,
},
]}
items={[{
widget: downloadsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['password'],
steps: [],
}]} />
+20 -32
View File
@@ -1,31 +1,32 @@
---
title: "Docker"
title: 'Docker'
description: "Docker is a platform where you can run your applications in containers, allowing for easy deployment and management of applications."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconBrandDocker, IconTool } from "@tabler/icons-react";
import { dockerIntegration } from ".";
import { dockerContainersWidget } from "@site/docs/widgets/docker-containers";
import { dockerIntegration } from '.';
import { dockerContainersWidget } from '@site/docs/widgets/docker-containers';
<IntegrationHeader integration={dockerIntegration} categories={["Containers"]} />
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: dockerContainersWidget,
note: "This widget can only be used with administrator privileges",
},
]}
<IntegrationHeader
integration={dockerIntegration}
categories={['Containers']}
/>
## Enabling the Docker integration
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: dockerContainersWidget,
note: "This widget can only be used with administrator privileges"
}]}
/>
## Enabling the Docker integration
This integration must first be enabled.
If you directly run on the host system and you have docker installed, it will be enabled by default.
Otherwise, you'll need to mount the [docker socket](https://docs.docker.com/engine/security/protect-access/) to the Homarr container,
@@ -48,7 +49,7 @@ services:
environment:
- SECRET_ENCRYPTION_KEY=your_64_character_hex_string # <--- can be generated with `openssl rand -hex 32`
ports:
- "7575:7575"
- '7575:7575'
```
With docker run, add this parameter: `-v /var/run/docker.sock:/var/run/docker.sock`.
@@ -123,10 +124,8 @@ services:
```
### Managing your Docker containers within Homarr
Homarr allows you to interact with Docker containers running on your system.
Each container row has an action menu with the following options:
- **View logs** - opens a dedicated logs page with a live terminal
- **Start** - start a stopped container
- **Stop** - stop a running container
@@ -167,13 +166,11 @@ Currently Homarr supports one label for configuration of the containers:
- **homarr.hide:** If set to any value, the container will be hidden from the Docker Containers widget and tools page.
### Security
Mounting docker sockets can be risky, as they permit full control over your docker service.
As an example, a thread actor could abuse Homarr use the socket to start, stop or delete containers on your system.
Therefore we recommend the usage of a socket proxy, which can prohibit certain actions.
A few examples include:
- https://github.com/linuxserver/docker-socket-proxy
- https://github.com/Tecnativa/docker-socket-proxy
@@ -181,44 +178,36 @@ See documentation of the respective proxies on how to configure them.
Homarr may behave in unexpected ways when you use proxies.
#### Permissions
Homarr needs the following permissions from the Docker API:
- Containers/Start
- Containers/Stop
- Containers/Restart
- Containers/Remove
For socket proxies, you will need these permissions:
- `CONTAINERS=1`
- `POST=1`
**Caution:** `POST` access is security critical as it provides extensive capabilities to modify your docker environment. Please leave it disabled if you're concerned about this.
As a workaround, you can use [LSIO's socket proxy](https://github.com/linuxserver/docker-socket-proxy) and set the following:
- ALLOW_START=1
- ALLOW_STOP=1
- ALLOW_RESTARTS=1
These will work even with `POST=0`.
These will work even with `POST=0`.
You lose the ability to remove containers, but start, stop and restarts should work just fine.
#### Connecting to Docker via Socket Proxies
To connect to Docker via a socket proxy, you'll need to add these two environment variables in the compose file:
- `DOCKER_HOSTNAMES=<name of the socket proxy container>`
- `DOCKER_PORTS=<socket proxy port, usually 2375>`
Refer: https://homarr.dev/docs/advanced/environment-variables/
You will also need to add Homarr to the network of your socket proxy. You can do it like this:
1. Add the network to your compose file (with appropriate changes):
```yaml
networks:
socket-proxy:
@@ -227,7 +216,6 @@ networks:
```
2. Add the network to the homarr service in the compose file:
```yaml
networks:
- socket-proxy
+23 -21
View File
@@ -1,35 +1,37 @@
---
title: "Emby"
title: 'Emby'
description: "Emby is a media server platform that allows you to organize, manage, and stream your personal media collection."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconVideo, IconTicket } from "@tabler/icons-react";
import { embyIntegration } from ".";
import { mediaServerWidget } from "@site/docs/widgets/media-server";
import { mediaReleasesWidget } from "@site/docs/widgets/media-releases";
import { embyIntegration } from '.';
import { mediaServerWidget } from '@site/docs/widgets/media-server';
import { mediaReleasesWidget } from '@site/docs/widgets/media-releases';
<IntegrationHeader integration={embyIntegration} categories={["Media server"]} />
<IntegrationHeader
integration={embyIntegration}
categories={['Media server']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: mediaServerWidget }, { widget: mediaReleasesWidget }]} />
<IntegrationCapabilites
items={[
{ widget: mediaServerWidget },
{ widget: mediaReleasesWidget }
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+22 -28
View File
@@ -1,43 +1,37 @@
---
title: "Glances"
title: 'Glances'
description: "Glances is a cross-platform system monitoring tool."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { glancesIntegration } from ".";
import { systemResourcesWidget } from "@site/docs/widgets/system-resources";
import { healthMonitoringWidget } from "@site/docs/widgets/health-monitoring";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { glancesIntegration } from '.';
import { systemResourcesWidget } from '@site/docs/widgets/system-resources';
import { healthMonitoringWidget } from '@site/docs/widgets/health-monitoring';
<IntegrationHeader integration={glancesIntegration} categories={["System Monitoring"]} />
<IntegrationHeader
integration={glancesIntegration}
categories={['System Monitoring']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: systemResourcesWidget,
},
{
widget: healthMonitoringWidget,
},
]}
items={[{
widget: systemResourcesWidget
}, {
widget: healthMonitoringWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: [],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}]} />
+25 -31
View File
@@ -1,17 +1,20 @@
---
title: "Gluetun"
description: "Connect Gluetun to Homarr as a VPN integration."
title: 'Gluetun'
description: 'Connect Gluetun to Homarr as a VPN integration.'
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { gluetunIntegration } from ".";
import { vpnWidget } from "@site/docs/widgets/vpn";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { gluetunIntegration } from '.';
import { vpnWidget } from '@site/docs/widgets/vpn';
<IntegrationHeader integration={gluetunIntegration} categories={["VPN", "Network"]} />
<IntegrationHeader
integration={gluetunIntegration}
categories={['VPN', 'Network']}
/>
[Gluetun](https://github.com/qdm12/gluetun) exposes a control server that Homarr can query to report the VPN tunnel
status, DNS status, public IP address, and active provider details in the VPN widget.
@@ -19,11 +22,9 @@ status, DNS status, public IP address, and active provider details in the VPN wi
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: vpnWidget,
},
]}
items={[{
widget: vpnWidget,
}]}
/>
### Setting up the Gluetun control server
@@ -39,20 +40,13 @@ Refer to the [Gluetun control server documentation](https://github.com/qdm12/glu
Gluetun's control server can be left unprotected or secured with either an API key or HTTP Basic authentication.
Choose the option that matches how your Gluetun instance integration is set up.
<IntegrationSecrets
secrets={[
{
credentials: [],
steps: [],
},
{
credentials: ["username", "password"],
steps: [],
},
{
credentials: ["apiKey"],
steps: [],
},
,
]}
/>
<IntegrationSecrets secrets={[{
credentials: [],
steps: [],
},{
credentials: ['username', 'password'],
steps: [],
},{
credentials: ['apiKey'],
steps: [],
},,]} />
+18 -24
View File
@@ -1,39 +1,33 @@
---
title: "Gotify"
title: 'Gotify'
description: "Gotify is a simple, self-hosted server for sending and receiving messages in real time."
hide_title: true
---
import { notificationsWidget } from "@site/docs/widgets/notifications";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { gotifyIntegration } from ".";
import { notificationsWidget } from '@site/docs/widgets/notifications';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { gotifyIntegration } from '.';
<IntegrationHeader integration={gotifyIntegration} categories={["Notifications", "Messaging"]} />
<IntegrationHeader
integration={gotifyIntegration}
categories={['Notifications', 'Messaging']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: notificationsWidget,
},
]}
items={[{
widget: notificationsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: []
}]} />
@@ -1,51 +1,45 @@
---
title: "Home Assistant"
title: 'Home Assistant'
description: "Home Assistant is an open-source home automation platform that focuses on privacy and local control, allowing you to automate and control your smart home devices."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconBinaryTree } from "@tabler/icons-react";
import { homeAssistantIntegration } from ".";
import { smartHomeEntityStateWidget } from "@site/docs/widgets/smart-home-entity-state";
import { smartHomeExecuteAutomationWidget } from "@site/docs/widgets/smart-home-execute-automation";
import { homeAssistantIntegration } from '.';
import { smartHomeEntityStateWidget } from '@site/docs/widgets/smart-home-entity-state';
import { smartHomeExecuteAutomationWidget } from '@site/docs/widgets/smart-home-execute-automation';
<IntegrationHeader integration={homeAssistantIntegration} categories={["Smart Home", "Automation"]} />
<IntegrationHeader
integration={homeAssistantIntegration}
categories={['Smart Home', 'Automation']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: smartHomeEntityStateWidget,
},
{
widget: smartHomeExecuteAutomationWidget,
},
]}
items={[{
widget: smartHomeEntityStateWidget
}, {
widget: smartHomeExecuteAutomationWidget,
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
"Click on your profile in the bottom left corner of Home Assistant.",
"Switch to the Security tab.",
"Scroll down to the Long-lived access tokens section.",
"Click on the Create Token button.",
"Enter a name for the token (e.g., 'Homarr Integration') and click Create.",
"Copy the generated token and paste it into the Homarr integration settings.",
],
},
]}
/>
]
}]} />
+19 -25
View File
@@ -1,44 +1,38 @@
---
title: "iCal"
title: 'iCal'
description: "iCal is a standard for calendar data exchange, allowing users to share and manage calendar events across different platforms."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconZoomQuestion, IconChartBar, IconSearch } from "@tabler/icons-react";
import { icalIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { icalIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
<IntegrationHeader integration={icalIntegration} categories={["Calendar"]} />
<IntegrationHeader
integration={icalIntegration}
categories={['Calendar']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: calendarWidget,
},
]}
items={[{
widget: calendarWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["url"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['url'],
steps: []
}]} />
## Using the iCal integration
The iCal integration allows you to add external calendars to Homarr. Any calendar that provides a public iCal URL can be integrated, such as Google Calendar, Apple Calendar, or Outlook.
The iCal integration allows you to add external calendars to Homarr. Any calendar that provides a public iCal URL can be integrated, such as Google Calendar, Apple Calendar, or Outlook.
+22 -16
View File
@@ -1,29 +1,36 @@
---
title: "Immich"
title: 'Immich'
description: "Self-hosted photo and video management solution"
hide_title: true
---
import { immichAlbumCarouselWidget } from "../../widgets/immich-album-carousel";
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { immichIntegration } from ".";
import { immichServerStatsWidget } from "../../widgets/immich-server-stats";
import { immichAlbumCarouselWidget } from '../../widgets/immich-album-carousel';import {
IntegrationHeader
} from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { immichIntegration } from '.';
import { immichServerStatsWidget } from '../../widgets/immich-server-stats';
<IntegrationHeader integration={immichIntegration} categories={["Photo server"]} />
<IntegrationHeader
integration={immichIntegration}
categories={['Photo server']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: immichAlbumCarouselWidget }, { widget: immichServerStatsWidget }]} />
<IntegrationCapabilites
items={[
{ widget: immichAlbumCarouselWidget },
{ widget: immichServerStatsWidget }
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
@@ -48,10 +55,9 @@ import { immichServerStatsWidget } from "../../widgets/immich-server-stats";
<code>asset.statistics</code>,{" "}
<code>server.statistics</code>,{" "}
<code>server.about</code>.
</span>
),
'Create the integration in Homarr and copy the API key to there'
]
'Create the integration in Homarr and copy the API key to there'
]
}]} />
+26 -25
View File
@@ -1,39 +1,40 @@
---
title: "Jellyfin"
title: 'Jellyfin'
description: "Jellyfin is a free and open-source media server software that allows you to organize, manage, and share your media files."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconVideo, IconTicket } from "@tabler/icons-react";
import { jellyfinIntegration } from ".";
import { mediaServerWidget } from "@site/docs/widgets/media-server";
import { mediaReleasesWidget } from "@site/docs/widgets/media-releases";
import { jellyfinIntegration } from '.';
import { mediaServerWidget } from '@site/docs/widgets/media-server';
import { mediaReleasesWidget } from '@site/docs/widgets/media-releases';
<IntegrationHeader integration={jellyfinIntegration} categories={["Media server"]} />
<IntegrationHeader
integration={jellyfinIntegration}
categories={['Media server']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: mediaServerWidget }, { widget: mediaReleasesWidget }]} />
<IntegrationCapabilites
items={[
{ widget: mediaServerWidget },
{ widget: mediaReleasesWidget }
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
},{
credentials: ['username', 'password'],
steps: []
}]} />
@@ -1,52 +1,45 @@
---
title: "Jellyseerr"
title: 'Jellyseerr'
description: "Jellyseerr is a self-hosted media request management system that integrates with Jellyfin and other media servers."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconZoomQuestion, IconChartBar, IconSearch } from "@tabler/icons-react";
import { jellyseerrIntegration } from ".";
import { mediaRequestListWidget } from "@site/docs/widgets/media-request-list";
import { mediaRequestStatsWidget } from "@site/docs/widgets/media-request-stats";
import { jellyseerrIntegration } from '.';
import { mediaRequestListWidget } from '@site/docs/widgets/media-request-list';
import { mediaRequestStatsWidget } from '@site/docs/widgets/media-request-stats';
<IntegrationHeader integration={jellyseerrIntegration} categories={["Media requests"]} />
<IntegrationHeader
integration={jellyseerrIntegration}
categories={['Media requests']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: mediaRequestListWidget,
},
{
widget: mediaRequestStatsWidget,
},
{
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search",
},
},
]}
items={[{
widget: mediaRequestListWidget,
},{
widget: mediaRequestStatsWidget,
}, {
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search"
}
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
@@ -1,16 +1,20 @@
---
title: "Kubernetes"
title: 'Kubernetes'
description: "Kubernetes is a powerful container orchestration platform that automates the deployment, scaling, and management of containerized applications."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconBrandDocker, IconTool } from "@tabler/icons-react";
import { kubernetesIntegration } from ".";
import { kubernetesIntegration } from '.';
<IntegrationHeader integration={kubernetesIntegration} categories={["Containers"]} />
<IntegrationHeader
integration={kubernetesIntegration}
categories={['Containers']}
/>
Homarr provides a user-friendly Kubernetes dashboard that offers an overview of your cluster’s resources in a clear and structured interface. It allows users to view key Kubernetes objects, including `Pods`, `Services`, `Ingresses`, `Nodes`, `ConfigMaps`, `Secrets`, `Namespaces`, and `Volumes`, as well as basic metrics when the Kubernetes Metrics Server is enabled.
@@ -43,7 +47,7 @@ Enabling RBAC in the Helm chart creates the necessary Kubernetes Role-Based Acce
- Service Account: A dedicated service account for Homarr.
- Role & RoleBinding: Grants permissions within a specific namespace to list, watch, and get pods, services, secrets, configmaps, and more.
- ClusterRole & ClusterRoleBinding: Extends the same permissions cluster-wide for additional flexibility.
:::
:::
### Homarr Dashboard Overview
+19 -24
View File
@@ -1,40 +1,35 @@
---
title: "Lidarr"
title: 'Lidarr'
description: "Lidarr is a music management tool that automates the process of downloading, sorting, and renaming music."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconCalendar } from "@tabler/icons-react";
import { lidarrIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { lidarrIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
<IntegrationHeader integration={lidarrIntegration} categories={["Media manager", "Servarr"]} />
<IntegrationHeader
integration={lidarrIntegration}
categories={['Media manager', "Servarr"]}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: calendarWidget,
},
]}
items={[{
widget: calendarWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+26 -24
View File
@@ -1,39 +1,41 @@
---
title: "Navidrome"
title: 'Navidrome'
description: "Navidrome is a self-hosted music server and streamer compatible with the Subsonic API."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { navidromeIntegration } from ".";
import { audioStatsWidget } from "@site/docs/widgets/audio-stats";
import { mediaServerWidget } from "@site/docs/widgets/media-server";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { navidromeIntegration } from '.';
import { audioStatsWidget } from '@site/docs/widgets/audio-stats';
import { mediaServerWidget } from '@site/docs/widgets/media-server';
<IntegrationHeader integration={navidromeIntegration} categories={["Media", "Media server"]} />
<IntegrationHeader
integration={navidromeIntegration}
categories={['Media', 'Media server']}
/>
Navidrome provides music library statistics including artist, album, and song counts, as well as now playing audio streams via the Subsonic API.
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: audioStatsWidget }, { widget: mediaServerWidget }]} />
<IntegrationCapabilites
items={[
{ widget: audioStatsWidget },
{ widget: mediaServerWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [
"Use the same credentials you use to log in to Navidrome",
"The user must have access to the Subsonic API (enabled by default)",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [
'Use the same credentials you use to log in to Navidrome',
'The user must have access to the Subsonic API (enabled by default)',
]
}]} />
+26 -29
View File
@@ -1,43 +1,40 @@
---
title: "Nextcloud"
title: 'Nextcloud'
description: "Nextcloud is a self-hosted productivity platform that provides file storage, collaboration tools, and more."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconCalendar } from "@tabler/icons-react";
import Alert from "@theme/Admonition";
import { nextcloudIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { notificationsWidget } from "@site/docs/widgets/notifications";
import Alert from '@theme/Admonition';
import { nextcloudIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
import { notificationsWidget } from '@site/docs/widgets/notifications';
<IntegrationHeader integration={nextcloudIntegration} categories={["Calendar", "Notifications"]} />
<IntegrationHeader
integration={nextcloudIntegration}
categories={['Calendar', 'Notifications']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: calendarWidget }, { widget: notificationsWidget }]} />
<IntegrationCapabilites
items={[
{ widget: calendarWidget },
{ widget: notificationsWidget }
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
header: (
<Alert type="info">
Users who use 2FA will have to use Nextcloud's App Password tool to grant Homarr access, as 2FA breaks the
traditional process. For more information, see [Nextcloud's
Documentation](https://docs.nextcloud.com/server/stable/admin_manual/configuration_user/authentication.html#app-passwords)
</Alert>
),
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [],
header: <Alert type="info">
Users who use 2FA will have to use Nextcloud's App Password tool to grant Homarr access, as 2FA breaks the traditional process. For more information, see [Nextcloud's Documentation](https://docs.nextcloud.com/server/stable/admin_manual/configuration_user/authentication.html#app-passwords)
</Alert>,
}]} />
+21 -28
View File
@@ -1,44 +1,37 @@
---
title: "Ntfy"
title: 'Ntfy'
description: "Ntfy is a simple, open-source notification service that allows you to send and receive notifications across devices."
hide_title: true
---
import { notificationsWidget } from "@site/docs/widgets/notifications";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { notificationsWidget } from '@site/docs/widgets/notifications';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { IconMessage } from "@tabler/icons-react";
import { ntfyIntegration } from ".";
import { ntfyIntegration } from '.';
<IntegrationHeader integration={ntfyIntegration} categories={["Notifications", "Messaging"]} />
<IntegrationHeader
integration={ntfyIntegration}
categories={['Notifications', 'Messaging']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: notificationsWidget,
},
]}
items={[{
widget: notificationsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["topic"],
steps: [],
},
{
credentials: ["topic", "apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['topic'],
steps: []
}, {
credentials: ['topic', 'apiKey'],
steps: []
}]} />
+20 -25
View File
@@ -1,41 +1,36 @@
---
title: "NZBGet"
title: 'NZBGet'
description: "NZBGet is a lightweight and efficient Usenet downloader."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IconDownload } from "@tabler/icons-react";
import { nzbgetIntegration } from ".";
import Alert from "@theme/Admonition";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import { nzbgetIntegration } from '.';
import Alert from '@theme/Admonition';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={nzbgetIntegration} categories={["Usenet client"]} />
<IntegrationHeader
integration={nzbgetIntegration}
categories={['Usenet client']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: downloadsWidget,
},
]}
items={[{
widget: downloadsWidget,
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [],
}]} />
@@ -1,52 +1,42 @@
---
title: "Open Media Vault"
title: 'Open Media Vault'
description: "Open Media Vault is a free network-attached storage server based on the Debian operating system."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { openMediaVaultIntegration } from ".";
import Alert from "@theme/Admonition";
import { healthMonitoringWidget } from "@site/docs/widgets/health-monitoring";
import { systemResourcesWidget } from "@site/docs/widgets/system-resources";
import { openMediaVaultIntegration } from '.';
import Alert from '@theme/Admonition'
import { healthMonitoringWidget } from '@site/docs/widgets/health-monitoring';
import { systemResourcesWidget } from '@site/docs/widgets/system-resources';
<IntegrationHeader integration={openMediaVaultIntegration} categories={["System monitoring"]} />
<IntegrationHeader
integration={openMediaVaultIntegration}
categories={['System monitoring']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: healthMonitoringWidget,
note: "To use this widget, you must have the OpenMediaVault plugin openmediavault-cputemp installed.",
},
{
widget: systemResourcesWidget,
},
]}
items={[{
widget: healthMonitoringWidget,
note: "To use this widget, you must have the OpenMediaVault plugin openmediavault-cputemp installed.",
}, {
widget: systemResourcesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [],
body: (
<Alert type="caution">
The user used here must have administrative permissions or Homarr won't be able to read the system performance
and resource data.
</Alert>
),
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [],
body: <Alert type="caution">
The user used here must have administrative permissions or Homarr won't be able to read the system performance and resource data.
</Alert>
}]} />
+37 -47
View File
@@ -1,75 +1,65 @@
---
title: "OPNsense"
title: 'OPNsense'
description: "OPNsense is an open-source, easy-to-use, and easy-to-build firewall and routing platform."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { opnsenseIntegration } from ".";
import Alert from "@theme/Admonition";
import { firewallWidget } from "../../widgets/firewall";
import { opnsenseIntegration } from '.';
import Alert from '@theme/Admonition'
import { firewallWidget } from '../../widgets/firewall';
<IntegrationHeader integration={opnsenseIntegration} categories={["Firewall"]} />
<IntegrationHeader
integration={opnsenseIntegration}
categories={['Firewall']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: firewallWidget,
},
]}
items={[{
widget: firewallWidget,
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
tabLabel: "ApiKey (Key & Secret)",
credentials: ["opnsenseApiKey", "opnsenseApiSecret"],
header: (
<Alert type="info">
This integration does not require full API access to the firewall. The following guide outlines the process
for creating a restricted OPNsense user with a scrambled password, designed exclusively for API access. This
ensures minimal permissions and prevents local login capability.
</Alert>
),
steps: [
<IntegrationSecrets secrets={[{
tabLabel: "ApiKey (Key & Secret)",
credentials: ['opnsenseApiKey', 'opnsenseApiSecret'],
header: <Alert type="info">
This integration does not require full API access to the firewall. The following guide outlines the process for creating a restricted OPNsense user with a scrambled password, designed exclusively for API access. This ensures minimal permissions and prevents local login capability.
</Alert>,
steps: [
"Log in to the OPNsense web interface as an administrator",
"Navigate to System → Access → Users",
"Click the + (Add) button to create a new user",
<>
Fill in the user details:
<ul>
<li>Username: Enter a unique name</li>
<li>Password: Leave empty</li>
<li>Scrambled Password: Enable this option</li>
<li>Full name: (Optional) Add a descriptive name</li>
</ul>
Fill in the user details:
<ul>
<li>Username: Enter a unique name</li>
<li>Password: Leave empty</li>
<li>Scrambled Password: Enable this option</li>
<li>Full name: (Optional) Add a descriptive name</li>
</ul>
</>,
<>
In Privileges section → Select the option below:
<ul>
<li>Lobby: Dashboard</li>
<li>Reporting: Traffic</li>
</ul>
In Privileges section → Select the option below:
<ul>
<li>Lobby: Dashboard</li>
<li>Reporting: Traffic</li>
</ul>
</>,
"Click Save",
"Return to the Users list",
"Find the user and click the ticket icon in the Commands column",
"Click Yes in the popup to confirm",
"A txt file will download automatically, containing: Key and Secret",
"Save this file securely—this information will only be shown once",
],
},
]}
/>
"Save this file securely—this information will only be shown once"
],
}]} />
+29 -36
View File
@@ -1,52 +1,45 @@
---
title: "Overseerr"
title: 'Overseerr'
description: "Overseerr is a self-hosted media request management system that integrates with Plex."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconZoomQuestion, IconChartBar, IconSearch } from "@tabler/icons-react";
import { overseerrIntegration } from ".";
import { mediaRequestListWidget } from "@site/docs/widgets/media-request-list";
import { mediaRequestStatsWidget } from "@site/docs/widgets/media-request-stats";
import { overseerrIntegration } from '.';
import { mediaRequestListWidget } from '@site/docs/widgets/media-request-list';
import { mediaRequestStatsWidget } from '@site/docs/widgets/media-request-stats';
<IntegrationHeader integration={overseerrIntegration} categories={["Media requests"]} />
<IntegrationHeader
integration={overseerrIntegration}
categories={['Media requests']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: mediaRequestListWidget,
},
{
widget: mediaRequestStatsWidget,
},
{
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search",
},
},
]}
items={[{
widget: mediaRequestListWidget
}, {
widget: mediaRequestStatsWidget
}, {
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search"
}
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
@@ -1,40 +1,41 @@
---
title: "Paperless-ngx"
title: 'Paperless-ngx'
description: "Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { paperlessNgxIntegration } from ".";
import { paperlessNgxWidget } from "@site/docs/widgets/paperless-ngx";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { paperlessNgxIntegration } from '.';
import { paperlessNgxWidget } from '@site/docs/widgets/paperless-ngx';
<IntegrationHeader integration={paperlessNgxIntegration} categories={["Documents"]} />
<IntegrationHeader
integration={paperlessNgxIntegration}
categories={['Documents']}
/>
Paperless-ngx provides document statistics including total documents, inbox count, correspondents, tags, and document types.
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: paperlessNgxWidget }]} />
<IntegrationCapabilites
items={[
{ widget: paperlessNgxWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
"Open your Paperless-ngx instance",
"Navigate to Settings → Django Admin → Tokens",
"Create a new token for your user",
"Copy the token value",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
'Open your Paperless-ngx instance',
'Navigate to Settings → Django Admin → Tokens',
'Create a new token for your user',
'Copy the token value',
]
}]} />
+26 -25
View File
@@ -1,42 +1,43 @@
---
title: "PatchMon"
title: 'PatchMon'
description: "PatchMon is a patch management and monitoring solution for Linux hosts."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { patchmonIntegration } from ".";
import { patchmonWidget } from "@site/docs/widgets/patchmon";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { patchmonIntegration } from '.';
import { patchmonWidget } from '@site/docs/widgets/patchmon';
<IntegrationHeader integration={patchmonIntegration} categories={["Health Monitoring"]} />
<IntegrationHeader
integration={patchmonIntegration}
categories={['Health Monitoring']}
/>
PatchMon provides host and package update statistics including total hosts, hosts needing updates, and security update counts.
The PatchMon widget requires users to be **signed in to Homarr**; see the [PatchMon widget documentation](/docs/widgets/patchmon#authentication) for details on authentication and public boards.
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: patchmonWidget }]} />
<IntegrationCapabilites
items={[
{ widget: patchmonWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["patchmonApiKey", "patchmonApiSecret"],
steps: [
"Open your PatchMon instance",
"Navigate to Settings and create a new GetHomepage API key",
"Copy the API key and secret from the success dialog",
"Paste them into Homarr — Basic authentication is handled automatically",
],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['patchmonApiKey', 'patchmonApiSecret'],
steps: [
'Open your PatchMon instance',
'Navigate to Settings and create a new GetHomepage API key',
'Copy the API key and secret from the success dialog',
'Paste them into Homarr — Basic authentication is handled automatically',
]
}]} />
+26 -31
View File
@@ -1,53 +1,48 @@
---
title: "PeaNUT"
description: "A tiny dashboard for Network UPS Tools (NUT) servers."
title: 'PeaNUT'
description: 'A tiny dashboard for Network UPS Tools (NUT) servers.'
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { peaNutIntegration } from ".";
import { upsWidget } from "@site/docs/widgets/ups";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { peaNutIntegration } from '.';
import { upsWidget } from '@site/docs/widgets/ups';
<IntegrationHeader integration={peaNutIntegration} categories={["UPS", "Hardware"]} />
<IntegrationHeader
integration={peaNutIntegration}
categories={['UPS', 'Hardware']}
/>
[PeaNUT](https://github.com/Brandawg93/PeaNUT) exposes the data of your [Network UPS Tools](https://networkupstools.org/) servers
over a REST API, which Homarr reads to display the status of your UPS devices.
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
items={[{
widget: upsWidget,
},
]}
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
PeaNUT secures its API with HTTP Basic authentication when the `WEB_USERNAME` and `WEB_PASSWORD` environment variables
are set on the PeaNUT instance. Provide the same credentials here. If the instance runs with `AUTH_DISABLED=true`, no
secrets are required.
<IntegrationSecrets
secrets={[
{
credentials: ["username", "password"],
steps: [
"Set WEB_USERNAME and WEB_PASSWORD on your PeaNUT instance.",
"Create an integration in Homarr with the PeaNUT URL and those credentials.",
],
},
{
credentials: [],
steps: ["If PeaNUT runs with AUTH_DISABLED=true, create the integration with no credentials."],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
steps: [
"Set WEB_USERNAME and WEB_PASSWORD on your PeaNUT instance.",
"Create an integration in Homarr with the PeaNUT URL and those credentials.",
],
},{
credentials: [],
steps: [
"If PeaNUT runs with AUTH_DISABLED=true, create the integration with no credentials.",
],
}]} />
+29 -36
View File
@@ -1,54 +1,47 @@
---
title: "Pi-hole"
title: 'Pi-hole'
description: "Pi-hole is a network-wide ad blocker that acts as a DNS sinkhole, blocking unwanted content and improving your browsing experience."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IconAd, IconDeviceGamepad } from "@tabler/icons-react";
import Alert from "@theme/Admonition";
import { dnsHoleSummaryWidget } from "@site/docs/widgets/dns-hole-summary";
import { dnsHoleControlsWidget } from "@site/docs/widgets/dns-hole-controls";
import { piHoleIntegration } from ".";
import Alert from '@theme/Admonition';
import { dnsHoleSummaryWidget } from '@site/docs/widgets/dns-hole-summary';
import { dnsHoleControlsWidget } from '@site/docs/widgets/dns-hole-controls';
import { piHoleIntegration } from '.';
<IntegrationHeader integration={piHoleIntegration} categories={["DNS Hole"]} />
<Alert type="note" className="my-2">
Homarr supports both Pi-hole v5 and v6. It automatically detects the version and uses the correct API.
<IntegrationHeader
integration={piHoleIntegration}
categories={['DNS Hole']}
/>
<Alert type="note" className='my-2'>
Homarr supports both Pi-hole v5 and v6. It automatically detects the version and uses the correct API.
</Alert>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: dnsHoleSummaryWidget,
},
{
widget: dnsHoleControlsWidget,
},
]}
items={[{
widget: dnsHoleSummaryWidget,
},{
widget: dnsHoleControlsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
body: "Please note, that in v6 of Pi-hole you no longer specify an API key, but instead either a password or application password. We suggest to use the application password when possible.",
},
{
credentials: [],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [],
body: "Please note, that in v6 of Pi-hole you no longer specify an API key, but instead either a password or application password. We suggest to use the application password when possible."
}, {
credentials: [],
steps: []
}]} />
+26 -32
View File
@@ -1,47 +1,41 @@
---
title: "Plex"
title: 'Plex'
description: "Plex is a media server platform that allows you to organize, manage, and stream your personal media collection."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconVideo, IconTicket } from "@tabler/icons-react";
import { plexIntegration } from ".";
import { mediaServerWidget } from "@site/docs/widgets/media-server";
import { mediaReleasesWidget } from "@site/docs/widgets/media-releases";
import { plexIntegration } from '.';
import { mediaServerWidget } from '@site/docs/widgets/media-server';
import { mediaReleasesWidget } from '@site/docs/widgets/media-releases';
<IntegrationHeader integration={plexIntegration} categories={["Media server"]} />
<IntegrationHeader
integration={plexIntegration}
categories={['Media server']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: mediaServerWidget }, { widget: mediaReleasesWidget }]} />
<IntegrationCapabilites
items={[
{ widget: mediaServerWidget },
{ widget: mediaReleasesWidget }
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
<span>
Follow the official{" "}
<a
href="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/"
target="_blank"
rel="noopener noreferrer"
>
Plex documentation
</a>{" "}
to find your authentication token.
</span>,
],
},
]}
/>
Follow the official <a href="https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/" target="_blank" rel="noopener noreferrer">Plex documentation</a> to find your authentication token.
</span>
]
}]} />
+18 -20
View File
@@ -1,34 +1,32 @@
---
title: "Prowlarr"
title: 'Prowlarr'
description: "Prowlarr is an indexer manager for Usenet and BitTorrent, providing a unified interface to manage your indexers."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { prowlarrIntegration } from ".";
import { indexerManagerWidget } from "@site/docs/widgets/indexer-manager";
import { prowlarrIntegration } from '.';
import { indexerManagerWidget } from '@site/docs/widgets/indexer-manager';
<IntegrationHeader integration={prowlarrIntegration} categories={["Indexer"]} />
<IntegrationHeader
integration={prowlarrIntegration}
categories={['Indexer']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: indexerManagerWidget }]} />
<IntegrationCapabilites
items={[{ widget: indexerManagerWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+66 -70
View File
@@ -1,107 +1,103 @@
---
title: "Proxmox"
title: 'Proxmox'
description: "Proxmox is a powerful open-source virtualization platform that allows you to run virtual machines and containers."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconHeartRateMonitor } from "@tabler/icons-react";
import { proxmoxIntegration } from ".";
import Alert from "@theme/Admonition";
import { healthMonitoringWidget } from "@site/docs/widgets/health-monitoring";
import { systemResourcesWidget } from "@site/docs/widgets/system-resources";
import { proxmoxIntegration } from '.';
import Alert from '@theme/Admonition'
import { healthMonitoringWidget } from '@site/docs/widgets/health-monitoring';
import { systemResourcesWidget } from '@site/docs/widgets/system-resources';
<IntegrationHeader integration={proxmoxIntegration} categories={["System monitoring"]} />
<IntegrationHeader
integration={proxmoxIntegration}
categories={['System monitoring']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: healthMonitoringWidget }]} />
<IntegrationCapabilites
items={[
{ widget: healthMonitoringWidget },
]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
header: (
<Alert type="caution">
The Proxmox API may not work with Linux PAM standard authentication. It is strongly recommended to use the
Proxmox VE authentication server realm when creating your API user.
</Alert>
),
credentials: ["username", "tokenId", "realm", "apiKey"],
steps: [
<IntegrationSecrets secrets={[{
header: <Alert type="caution">
The Proxmox API may not work with Linux PAM standard authentication. It is strongly recommended to use the Proxmox VE authentication server realm when creating your API user.
</Alert>,
credentials: ['username', 'tokenId', 'realm', 'apiKey'],
steps: [
"Navigate to the Proxmox portal, click on Datacenter",
"Expand Permissions, click on Groups",
"Click the Create button",
"Name the group something informative, like api-users",
"Click on the Permissions 'folder'",
<>
Click Add {"->"} Group Permission
<ul>
<li>Path: /</li>
<li>Group: group from Step 4 above</li>
<li>Role: PVEAuditor</li>
<li>Propagate: Checked</li>
</ul>
Click Add {"->"} Group Permission
<ul>
<li>Path: /</li>
<li>Group: group from Step 4 above</li>
<li>Role: PVEAuditor</li>
<li>Propagate: Checked</li>
</ul>
</>,
"Expand Permissions, click on Users",
<>
Click the Add button
<ul>
<li>User name: something informative like api</li>
<li>Realm: Proxmox VE authentication server</li>
<li>Password: create a secure password for the user</li>
<li>Confirm Password: re-enter the password</li>
<li>Group: group from Step 4 above</li>
</ul>
Click the Add button
<ul>
<li>User name: something informative like api</li>
<li>Realm: Proxmox VE authentication server</li>
<li>Password: create a secure password for the user</li>
<li>Confirm Password: re-enter the password</li>
<li>Group: group from Step 4 above</li>
</ul>
</>,
"Expand Permissions, click on API Tokens",
<>
Click the Add button
<ul>
<li>User: user from Step 8 above</li>
<li>Token ID: something informative like the application or purpose like homarr</li>
<li>Privilege Separation: unchecked</li>
</ul>
Click the Add button
<ul>
<li>User: user from Step 8 above</li>
<li>Token ID: something informative like the application or purpose like homarr</li>
<li>Privilege Separation: unchecked</li>
</ul>
</>,
"Copy the Secret that is shown in Step 10 because it is only shown once",
"Go back to the 'Permissions' menu",
<>
Click Add {"->"} API Token Permission
<ul>
<li>Path: /</li>
<li>API Token: select the API token created in Step 10</li>
<li>Role: PVE Auditor</li>
<li>Propagate: Checked</li>
</ul>
Click Add {"->"} API Token Permission
<ul>
<li>Path: /</li>
<li>API Token: select the API token created in Step 10</li>
<li>Role: PVE Auditor</li>
<li>Propagate: Checked</li>
</ul>
</>,
],
body: (
<>
<span>
Sometimes you'll also find the <code>api@pve!homarr</code> format, within Homarr you'll have to split it up
into:
</span>
<ul>
],
body: <>
<span>
Sometimes you'll also find the <code>api@pve!homarr</code> format,
within Homarr you'll have to split it up into:
</span>
<ul>
<li>
<b>Username:</b> api
<b>Username:</b> api
</li>
<li>
<b>Realm:</b> pve
<b>Realm:</b> pve
</li>
<li>
<b>Token ID:</b> homarr
<b>Token ID:</b> homarr
</li>
</ul>
</>
),
},
]}
/>
</ul>
</>
}]} />
@@ -1,39 +1,37 @@
---
title: "qBittorrent"
title: 'qBittorrent'
description: "qBittorrent is a free and open-source BitTorrent client."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IconDownload } from "@tabler/icons-react";
import { qBittorentIntegration } from ".";
import Alert from "@theme/Admonition";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import { qBittorentIntegration } from '.';
import Alert from '@theme/Admonition';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={qBittorentIntegration} categories={["Torrent client"]} />
<IntegrationHeader
integration={qBittorentIntegration}
categories={['Torrent client']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: downloadsWidget }]} />
<IntegrationCapabilites
items={[{ widget: downloadsWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
},{
credentials: ['username', 'password'],
steps: [],
}]} />
+19 -20
View File
@@ -1,34 +1,33 @@
---
title: "Radarr"
title: 'Radarr'
description: "Radarr is a movie management tool that automates the process of downloading, sorting, and renaming movies."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconCalendar } from "@tabler/icons-react";
import { radarrIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { radarrIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
<IntegrationHeader integration={radarrIntegration} categories={["Media manager", "Servarr"]} />
<IntegrationHeader
integration={radarrIntegration}
categories={['Media manager', "Servarr"]}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: calendarWidget }]} />
<IntegrationCapabilites
items={[{ widget: calendarWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+19 -20
View File
@@ -1,34 +1,33 @@
---
title: "Readarr"
title: 'Readarr'
description: "Readarr is a book management tool that automates the process of downloading, sorting, and renaming books."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconCalendar } from "@tabler/icons-react";
import { readarrIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { readarrIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
<IntegrationHeader integration={readarrIntegration} categories={["Media manager", "Servarr"]} />
<IntegrationHeader
integration={readarrIntegration}
categories={['Media manager', "Servarr"]}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: calendarWidget }]} />
<IntegrationCapabilites
items={[{ widget: calendarWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+20 -21
View File
@@ -1,35 +1,34 @@
---
title: "SABnzbd"
title: 'SABnzbd'
description: "SABnzbd is an Open Source Binary Newsreader."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { IconDownload } from "@tabler/icons-react";
import { sabnzbdIntegration } from ".";
import Alert from "@theme/Admonition";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import { sabnzbdIntegration } from '.';
import Alert from '@theme/Admonition';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={sabnzbdIntegration} categories={["Usenet client"]} />
<IntegrationHeader
integration={sabnzbdIntegration}
categories={['Usenet client']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: downloadsWidget }]} />
<IntegrationCapabilites
items={[{ widget: downloadsWidget}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [],
}]} />
+29 -36
View File
@@ -1,52 +1,45 @@
---
title: "Seerr"
title: 'Seerr'
description: "Seerr is a self-hosted media request management system that integrates with various media servers."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconZoomQuestion, IconChartBar, IconSearch } from "@tabler/icons-react";
import { seerrIntegration } from ".";
import { mediaRequestListWidget } from "@site/docs/widgets/media-request-list";
import { mediaRequestStatsWidget } from "@site/docs/widgets/media-request-stats";
import { seerrIntegration } from '.';
import { mediaRequestListWidget } from '@site/docs/widgets/media-request-list';
import { mediaRequestStatsWidget } from '@site/docs/widgets/media-request-stats';
<IntegrationHeader integration={seerrIntegration} categories={["Media requests"]} />
<IntegrationHeader
integration={seerrIntegration}
categories={['Media requests']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: mediaRequestListWidget,
},
{
widget: mediaRequestStatsWidget,
},
{
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search",
},
},
]}
items={[{
widget: mediaRequestListWidget
}, {
widget: mediaRequestStatsWidget
}, {
capability: {
icon: IconSearch,
name: "Media Search",
description: "Search for movies and TV shows directly from Spotlight and request them.",
path: "../../management/search-engines/#media-request-search"
}
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
+19 -24
View File
@@ -1,39 +1,34 @@
---
title: "Slskd"
title: 'Slskd'
description: "A modern client-server application for the Soulseek file sharing network."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { slskdIntegration } from ".";
import { downloadsWidget } from "@site/docs/widgets/downloads";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { slskdIntegration } from '.';
import { downloadsWidget } from '@site/docs/widgets/downloads';
<IntegrationHeader integration={slskdIntegration} categories={["System Monitoring"]} />
<IntegrationHeader
integration={slskdIntegration}
categories={['System Monitoring']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: downloadsWidget,
},
]}
items={[{
widget: downloadsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ["apiKey"],
steps: []
}]} />
+19 -20
View File
@@ -1,34 +1,33 @@
---
title: "Sonarr"
title: 'Sonarr'
description: "Sonarr is a TV series management tool that automates the process of downloading, sorting, and renaming episodes."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconCalendar } from "@tabler/icons-react";
import { sonarrIntegration } from ".";
import { calendarWidget } from "@site/docs/widgets/calendar";
import { sonarrIntegration } from '.';
import { calendarWidget } from '@site/docs/widgets/calendar';
<IntegrationHeader integration={sonarrIntegration} categories={["Media manager", "Servarr"]} />
<IntegrationHeader
integration={sonarrIntegration}
categories={['Media manager', "Servarr"]}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: calendarWidget }]} />
<IntegrationCapabilites
items={[{ widget: calendarWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
@@ -1,39 +1,34 @@
---
title: "Speedtest Tracker"
description: "Speedtest Tracker is a self-hosted internet performance tracking application."
title: 'Speedtest Tracker'
description: 'Speedtest Tracker is a self-hosted internet performance tracking application.'
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { speedtestTrackerIntegration } from ".";
import { speedtestTrackerWidget } from "@site/docs/widgets/speedtest-tracker";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { speedtestTrackerIntegration } from '.';
import { speedtestTrackerWidget } from '@site/docs/widgets/speedtest-tracker';
<IntegrationHeader integration={speedtestTrackerIntegration} categories={["Speedtest"]} />
<IntegrationHeader
integration={speedtestTrackerIntegration}
categories={['Speedtest']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: speedtestTrackerWidget,
},
]}
items={[{
widget: speedtestTrackerWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}]} />
@@ -19,7 +19,11 @@ import Alert from "@theme/Admonition";
### Widgets & Capabilities
<IntegrationCapabilites
items={[{ widget: healthMonitoringWidget }, { widget: systemResourcesWidget }, { widget: systemDisksWidget }]}
items={[
{ widget: healthMonitoringWidget },
{ widget: systemResourcesWidget },
{ widget: systemDisksWidget },
]}
/>
### Supported metrics
+22 -24
View File
@@ -1,38 +1,36 @@
---
title: "Tdarr"
title: 'Tdarr'
description: "Tdarr is a distributed media transcoding system that allows you to process and optimize your media files across multiple nodes."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
import { IconTransform } from "@tabler/icons-react";
import { tdarrIntegration } from ".";
import { mediaTranscodingWidget } from "@site/docs/widgets/media-transcoding";
import { tdarrIntegration } from '.';
import { mediaTranscodingWidget } from '@site/docs/widgets/media-transcoding';
<IntegrationHeader integration={tdarrIntegration} categories={["Media Transcoding"]} />
<IntegrationHeader
integration={tdarrIntegration}
categories={['Media Transcoding']}
/>
### Widgets & Capabilities
<IntegrationCapabilites items={[{ widget: mediaTranscodingWidget }]} />
<IntegrationCapabilites
items={[{ widget: mediaTranscodingWidget }]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [],
},
{
credentials: [],
steps: [],
},
]}
/>
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: []
}, {
credentials: [],
steps: []
}]} />
@@ -1,72 +1,62 @@
---
title: "Technitium DNS"
title: 'Technitium DNS'
description: "Technitium DNS Server is a self-hosted DNS server with ad-blocking and network-wide filtering capabilities."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { IntegrationCapabilites } from "@site/src/components/integrations/widgets";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import Alert from "@theme/Admonition";
import { technitiumDnsIntegration } from ".";
import { dnsHoleSummaryWidget } from "@site/docs/widgets/dns-hole-summary";
import { dnsHoleControlsWidget } from "@site/docs/widgets/dns-hole-controls";
import { IntegrationHeader } from '@site/src/components/integrations/header';
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
import { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import Alert from '@theme/Admonition';
import { technitiumDnsIntegration } from '.';
import { dnsHoleSummaryWidget } from '@site/docs/widgets/dns-hole-summary';
import { dnsHoleControlsWidget } from '@site/docs/widgets/dns-hole-controls';
<IntegrationHeader integration={technitiumDnsIntegration} categories={["DNS Hole"]} />
<IntegrationHeader
integration={technitiumDnsIntegration}
categories={['DNS Hole']}
/>
<Alert type="note" className="my-2">
Homarr supports Technitium DNS v11 and above. The correct API version is detected automatically - no manual
configuration is required.
<Alert type="note" className='my-2'>
Homarr supports Technitium DNS v11 and above. The correct API version is detected automatically - no manual configuration is required.
</Alert>
### Widgets & Capabilities
<IntegrationCapabilites
items={[
{
widget: dnsHoleSummaryWidget,
},
{
widget: dnsHoleControlsWidget,
},
]}
items={[{
widget: dnsHoleSummaryWidget,
},{
widget: dnsHoleControlsWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<Alert type="info" className="my-2">
The **API token** option is recommended. API tokens are non-expiring and do not require storing your admin password in
Homarr. Username and password authentication uses a session token that expires after inactivity, triggering a silent
re-login on the next request.
<Alert type="info" className='my-2'>
The **API token** option is recommended. API tokens are non-expiring and do not require storing your admin password in Homarr.
Username and password authentication uses a session token that expires after inactivity, triggering a silent re-login on the next request.
</Alert>
<Alert type="warning" className="my-2">
The **DNS Hole Controls** widget (enabling, disabling, and timed disabling of blocking) requires the token to have
settings-level permissions. The **DNS Hole Summary** widget (stats only) works with any valid token. If Homarr cannot
read the blocking status due to insufficient permissions, the status indicator will be hidden but statistics will
still be shown.
<Alert type="warning" className='my-2'>
The **DNS Hole Controls** widget (enabling, disabling, and timed disabling of blocking) requires the token to have settings-level permissions.
The **DNS Hole Summary** widget (stats only) works with any valid token.
If Homarr cannot read the blocking status due to insufficient permissions, the status indicator will be hidden but statistics will still be shown.
</Alert>
<IntegrationSecrets
secrets={[
{
credentials: ["apiKey"],
steps: [
<IntegrationSecrets secrets={[{
credentials: ['apiKey'],
steps: [
"Open the Technitium DNS web console and sign in.",
'Click on your username in the top-right corner and select "Create API Token".',
'Give the token a name (e.g. "Homarr") and click "Create Token".',
"Copy the generated token and paste it into the API Key field in Homarr.",
],
},
{
credentials: ["username", "password"],
steps: [],
body: "Enter your Technitium DNS admin credentials. Homarr will exchange them for a session token and transparently re-authenticate when the session expires.",
},
]}
/>
"Click on your username in the top-right corner and select \"Create API Token\".",
"Give the token a name (e.g. \"Homarr\") and click \"Create Token\".",
"Copy the generated token and paste it into the API Key field in Homarr."
]
}, {
credentials: ['username', 'password'],
steps: [],
body: "Enter your Technitium DNS admin credentials. Homarr will exchange them for a session token and transparently re-authenticate when the session expires."
}]} />
Loaded 100 of 824 files, more files were not shown because too many files have changed in this diff. Show more