Compare commits

..
Author SHA1 Message Date
Thomas Camlong e5b8c18cd2 chore: edit renovate base branch 2026-06-12 14:03:07 +02:00
Thomas Camlong ab46d822cc fix: copy pnpm-workspace.yaml in prebuilt-debian Dockerfile
pnpm v11 reads allowBuilds from pnpm-workspace.yaml, not package.json.
Without it, better-sqlite3 build scripts are rejected.
2026-06-11 23:48:12 +02:00
Thomas Camlong 93bcc5945f fix: address PR review findings
- Revert undici to v7.27.1 (v8 blocked by #5467 qBittorrent regression)
- Broaden cache invalidation to utils.integration.invalidate() (covers
  all integration queries, not just integration.all)
- Add invalidation to onboarding integration create path
- Harden renovate test to assert allowBuilds key exists
- Remove deprecated Vitest 4 poolOptions config
- Fix CSS typo in docs menu transition (0.s → 0.5s)
- Update renovate.json5 comment to reference allowBuilds
2026-06-11 23:47:38 +02:00
Thomas Camlong d5776e7d2a fix: set verifyDepsBeforeRun to warn to prevent parallel install deadlock
pnpm v11 defaults to verifyDepsBeforeRun: install, which auto-runs
pnpm install before every pnpm run. When turbo runs multiple build
tasks in parallel, concurrent installs on the same workspace deadlock.
2026-06-11 22:46:16 +02:00
Thomas Camlong 0156fa0413 fix: bump pnpm to 11.5.3 and remove from mise (use corepack)
11.5.2 hit minimumReleaseAge constraints. Removed pnpm from .mise.toml
since corepack handles it via packageManager in package.json.
2026-06-11 22:36:08 +02:00
Thomas Camlong 38ce6edd17 chore(deps): upgrade to TypeScript 6, Vite 8, and @vitejs/plugin-react 6
- TypeScript 5.9 → 6.0: add types: ["node"] to base tsconfig, remove
  deprecated baseUrl where unused, add ignoreDeprecations for docs app,
  fix node_modules/ prefix imports with proper package exports
- Vite 7 → 8 + @vitejs/plugin-react 5 → 6: add vite as direct dep
- Add CSS module type declarations for TS6 compatibility
- Update renovate test for pnpm v11 allowBuilds migration
2026-06-11 22:28:21 +02:00
Thomas Camlong f1fb760c69 chore(deps): batch dependency updates, pnpm v11 migration, and fix integration cache
Batch-update dependencies from 17 open renovate PRs:
- @modelcontextprotocol/sdk 1.26→1.29, tiptap 3.25→3.26, ioredis 5.11.0→5.11.1
- mysql2 3.22.4→3.22.5, sql.js ^1.12→^1.14, @types/react 19.2.16→19.2.17
- oxlint ^1.67→^1.68, oxfmt ^0.53→^0.54, concurrently ^9→^10, mantine-contextmenu ^7→^9
- postcss ^8.4→^8.5.10 (security), @fec/remark-a11y-emoji ^3→^4

Migrate docs Tailwind CSS v3 → v4:
- Replace tailwindcss v3 + autoprefixer with tailwindcss v4 + @tailwindcss/postcss
- Convert config from tailwind.config.js to CSS @theme block
- Update PostCSS plugin in docusaurus.config.ts

Upgrade GitHub Actions:
- configure-pages v5→v6, upload-pages-artifact v4→v5, deploy-pages v4→v5
- EndBug/add-and-commit v9→v10, git-auto-commit-action v5→v7

Migrate pnpm v10 → v11 (unblocks #5679):
- Move package.json#pnpm config to pnpm-workspace.yaml
- Convert onlyBuiltDependencies/ignoredBuiltDependencies to allowBuilds map
- Migrate .npmrc settings (nodeLinker, strictPeerDependencies, engineStrict)
- Update packageManager to pnpm@11.5.2

Fix proxmox-api undici type incompatibility:
- Cast fetch parameter to resolve type mismatch between bundled undici versions

Fix integration cache not invalidating after create/update/delete:
- Add utils.integration.all.invalidate() to all mutation onSuccess handlers
- Previously only Next.js server cache was revalidated, not client tRPC cache
2026-06-11 22:14:03 +02:00
497 changed files with 12271 additions and 63929 deletions

No files matched your search

@@ -0,0 +1,197 @@
---
name: Revert Demo MCP
overview: Revert the demo-specific MCP accommodations (API key bypass, read-only filtering, demo UI) since demo MCP is an edge case with poor ROI. Replace with a clear "MCP not available in demo mode" message. Keep the core MCP feature intact for real instances.
todos:
- id: revert-backend
content: Revert demoSafe procedure, apiKeys demo bypass, isDemoMode export from mcp.ts
status: pending
- id: revert-mcp-route
content: Remove isDemoMode filtering and demo SERVER_INSTRUCTIONS from MCP route
status: pending
- id: revert-ui
content: Remove isDemoMode prop plumbing; add demo-not-supported Alert via info.isDemoMode
status: pending
- id: verify
content: Run lint + typecheck; smoke-test demo vs non-demo MCP tab
status: pending
isProject: false
---
# Revert Demo MCP Accommodations
## Why Docker works but integrations do not
Demo mode has **two different mock mechanisms**:
```mermaid
flowchart TB
subgraph dockerPath [Docker - works in demo]
MCP1[MCP docker_getContainers]
Handler[docker.ts request handler]
MockContainers[Hardcoded mockContainers]
MCP1 --> Handler
Handler -->|"if DEMO_MODE"| MockContainers
end
subgraph integrationPath [Integrations - confusing in demo]
MCP2[MCP widget tools e.g. calendar_findAllEvents]
TRPC[tRPC procedure]
RH[cached-integration-request-handler]
Kind{integration.kind}
Mock[MockIntegration fake data]
Real[Sonarr/Radarr HTTP - fails]
MCP2 --> TRPC --> RH --> Kind
Kind -->|mock| Mock
Kind -->|sonarr/radarr| Real
end
```
- **Docker**:
[`packages/request-handler/src/docker.ts`](packages/request-handler/src/docker.ts)
checks `DEMO_MODE` directly and returns inline fake containers. No integration
record needed.
- **Everything else**: Demo seed creates **one** integration with `kind: "mock"`
([`packages/db/migrations/seed.ts`](packages/db/migrations/seed.ts)), not
Sonarr/Radarr/Overseerr.
[`MockIntegration`](packages/integrations/src/mock/mock-integration.ts) can
serve calendar, downloads, health, DNS, media server, etc. **if** callers pass
that integration ID.
- **Why it felt broken**: MCP tool descriptions tell AI clients to look for
"Overseerr/Sonarr IDs". Demo has none. Tools like
`integration_searchMediaRequests` hard-filter to `mediaSearch` kinds
(`overseerr`, `jellyseerr`, `seerr`) — `mock` is excluded from that category
in
[`packages/definitions/src/integration.ts`](packages/definitions/src/integration.ts).
Fixing this properly means extending the mock integration system (new
`searchAsync`, category changes, middleware relaxations, instruction rewrites)
for a feature almost nobody will use on the public demo.
## Option comparison
| | **Revert (chosen)** | **Minimal fix** | **Full fix** |
| -------------------- | --------------------------- | ---------------------------------------------- | --------------------------------------------------- |
| **Effort** | ~1 hour, ~100 lines removed | ~2 hours | 1-2 days |
| **Files touched** | 8 MCP/demo-specific files | 8 + route instructions | 15+ across integrations, definitions, seed, routers |
| **Risk** | Low | Low-medium | Medium (mock/search semantics) |
| **Demo MCP outcome** | Clearly unsupported | Partial (widget reads only, no search/request) | Near-full read coverage |
| **Maintenance** | None ongoing | Instructions drift with tool changes | Mock parity with real integrations |
**Recommendation: Revert.** The demo accommodation code added special-case
plumbing (`demoSafePermissionRequiredProcedure`, dual mutation guards, hardcoded
UI strings) for a workflow that still cannot deliver a convincing "homelab AI"
experience without substantial integration-layer work.
## What to revert (demo MCP accommodations only)
These changes were added specifically for demo + MCP. **Do not revert** the core
MCP server, OAuth, Scalar docs, tool descriptions, or `normalizeSchema`.
### Backend (3 files)
1. [`packages/api/src/trpc.ts`](packages/api/src/trpc.ts)
- Remove `demoSafePermissionRequiredProcedure` export (~12 lines)
- Keep existing `enforceDemoModeReadOnly` (pre-dates MCP; used by whole app)
2. [`packages/api/src/router/apiKeys.ts`](packages/api/src/router/apiKeys.ts)
- Change `create` back to `permissionRequiredProcedure` (mutations blocked in
demo again)
- Remove `isDemoMode` guard on `delete` (global demo middleware already
blocks delete mutations)
- Remove `TRPCError` / `isDemoMode` imports if unused
3. [`packages/api/src/mcp.ts`](packages/api/src/mcp.ts)
- Stop exporting `isDemoMode`; keep only `createTRPCContext` and `mcpRouter`
### MCP route (1 file)
4. [`apps/nextjs/src/app/api/mcp/[transport]/route.ts`](apps/nextjs/src/app/api/mcp/[transport]/route.ts)
- Remove `isDemoMode` import and all demo branches:
- `ListToolsRequestSchema` mutation filter
- `CallToolRequestSchema` mutation rejection
- `SERVER_INSTRUCTIONS` demo section
- Keep `getToolType` / `buildToolTypeMap` only if still needed elsewhere;
otherwise remove dead code from demo-only filtering
### UI (4 files)
5. [`apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx`](apps/nextjs/src/app/[locale]/manage/tools/api/page.tsx)
- Remove `isDemoMode` import from `@homarr/api/mcp`
- Stop passing `isDemoMode` to child components
6. [`apps/nextjs/src/app/[locale]/manage/tools/api/components/api-page-tabs.tsx`](apps/nextjs/src/app/[locale]/manage/tools/api/components/api-page-tabs.tsx)
- Remove `isDemoMode` prop from interface and `McpInstructions` call
7. [`apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx`](apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx)
- Remove `isDemoMode` prop
- Replace current demo banner with a single **Alert**: MCP requires a
self-hosted instance; demo mode does not support MCP (API key creation and
write tools are disabled)
- Use `clientApi.info.isDemoMode.useQuery()` or pass `isDemoMode` from server
via existing `api.info.isDemoMode()` tRPC query (already exists in
[`packages/api/src/router/info.ts`](packages/api/src/router/info.ts)) —
avoids re-exporting from `@homarr/api/mcp`
8. [`apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx`](apps/nextjs/src/app/[locale]/manage/tools/api/components/api-keys.tsx)
- Remove `isDemoMode` prop and conditional delete-column spread
- Delete button hidden automatically in demo because `apiKeys.delete`
mutation is blocked by global `enforceDemoModeReadOnly`
## What to add instead
- **MCP tab**: When `info.isDemoMode` is true, show a prominent notice at the
top of
[`mcp-instructions.tsx`](apps/nextjs/src/app/[locale]/manage/tools/api/components/mcp-instructions.tsx)
explaining MCP is for self-hosted instances only.
- **Optional**: Disable/hide the "Create API Key" button on the MCP tab in demo
(mutations will fail anyway).
- **No changes** to integrations, definitions, seed, or mock services.
## Files NOT impacted by revert
Core MCP feature stays as-is:
- [`apps/nextjs/src/app/api/mcp/[transport]/route.ts`](apps/nextjs/src/app/api/mcp/[transport]/route.ts)
— auth, rate limiting, `normalizeSchema`, `SERVER_INSTRUCTIONS` (minus demo
section)
- OAuth routes under `apps/nextjs/src/app/api/mcp/oauth/`
- All MCP-enabled routers in `packages/api/src/router/`
- [`packages/api/src/mcp.ts`](packages/api/src/mcp.ts) — eager router (minus
`isDemoMode` export)
- Scalar UI, dynamic tools table,
[`.cursor/rules/mcp-integration.mdc`](.cursor/rules/mcp-integration.mdc)
Pre-existing demo infrastructure (unchanged):
- [`packages/request-handler/src/docker.ts`](packages/request-handler/src/docker.ts)
— Docker mock containers
- [`packages/db/migrations/seed.ts`](packages/db/migrations/seed.ts) — mock
integration + demo board
- [`packages/integrations/src/mock/`](packages/integrations/src/mock/) — mock
services for widgets on demo board UI
## Verification
1. `pnpm lint` and `pnpm typecheck` pass
2. Demo mode (`DEMO_MODE=true`):
- MCP tab shows "not supported" notice
- API key create fails with existing "Mutations are disabled in demo mode"
- `docker_getContainers` still works on demo board UI (unchanged; separate
code path)
3. Non-demo mode: MCP unchanged (all tools listed, mutations work with
permissions)
## Commit strategy
Single focused commit on `feat/add-mcp-server`:
```
revert(mcp): drop demo-mode accommodations for MCP
MCP is intended for self-hosted instances. Remove demo-specific API key
bypass, read-only tool filtering, and demo UI plumbing. Show a clear
notice on the MCP tab when demo mode is active.
```
Then push to the existing PR (no force-push needed unless you want another
squash).
-5
View File
@@ -33,11 +33,6 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.68.0
- 1.67.0
- 1.66.1
- 1.66.0
- 1.65.0
- 1.64.0
- 1.62.0
- 1.61.0
-62
View File
@@ -163,65 +163,3 @@ jobs:
run: cp .env.example .env
- name: Build
run: pnpm turbo build --affected
memory-regression:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker image
uses: docker/build-push-action@v7
with:
platforms: linux/amd64
context: .
push: false
load: true
tags: homarr-memory-test
network: host
cache-from: type=gha
cache-to: type=gha,mode=max
env:
SKIP_ENV_VALIDATION: true
- name: Start container and measure memory
run: |
docker run -d --name homarr-mem-test -p 17575:7575 homarr-memory-test
# Wait for container to be healthy (up to 60s)
healthy=false
for i in $(seq 1 30); do
if curl -sf http://localhost:17575/api/health 2>/dev/null; then
echo "Container healthy after $((i * 2))s"
healthy=true
break
fi
sleep 2
done
if [ "$healthy" != "true" ]; then
echo "ERROR: Container never became healthy"
docker logs homarr-mem-test --tail 50
exit 1
fi
# Warm up: hit the dashboard page a few times
for i in $(seq 1 5); do
curl -sf http://localhost:17575/ > /dev/null 2>&1 || true
sleep 2
done
# Measure RSS after warmup
MEM_KB=$(docker stats --no-stream --format '{{.MemUsage}}' homarr-mem-test | sed 's| /.*||')
echo "Raw memory: $MEM_KB"
# Convert to numeric MB (handles GiB and MiB suffixes)
MEM_NUM=$(echo "$MEM_KB" | awk '{
if ($0 ~ /GiB/) { gsub(/GiB/,""); print $1 * 1024 }
else if ($0 ~ /MiB/) { gsub(/MiB/,""); print $1 }
else if ($0 ~ /KiB/) { gsub(/KiB/,""); print $1 / 1024 }
else { print $1 / 1048576 }
}')
echo "Memory usage: ${MEM_NUM} MB"
MAX_MB=700
if awk "BEGIN {exit !($MEM_NUM > $MAX_MB)}"; then
echo "ERROR: Memory usage ${MEM_NUM} MB exceeds ${MAX_MB} MB threshold"
docker logs homarr-mem-test --tail 50
exit 1
fi
echo "Memory check passed: ${MEM_NUM} MB <= ${MAX_MB} MB"
@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-node@v6
if: env.SKIP_RELEASE == 'false'
with:
node-version: 24.18.0
node-version: 24.16.0
package-manager-cache: false
- run: npm i -g pnpm
if: env.SKIP_RELEASE == 'false'
@@ -3,7 +3,6 @@ name: "[Deployment] Automatic Weekly Release"
on:
schedule:
- cron: "0 19 * * 5" # https://crontab.guru/#0_19_*_*_5
timezone: Europe/Zurich
workflow_dispatch:
inputs:
send-notifications:
-4
View File
@@ -2,7 +2,6 @@
# dependencies
node_modules
.pnpm-store/
.pnp
.pnp.js
.idea/
@@ -71,10 +70,7 @@ apps/nextjs/public/images/background.png
# personal agent config
.opencode
.agents/
skills-lock.json
# docusaurus
apps/docs/.docusaurus
apps/docs/build
.pnpm-store
+1 -1
View File
@@ -1,2 +1,2 @@
[tools]
node = "24.18.0"
node = "24.16.0"
+1 -1
View File
@@ -1 +1 @@
24.18.0
24.16.0
+1 -1
View File
@@ -1,7 +1,7 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"oxc.oxc-vscode",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"million.million-lint",
"lokalise.i18n-ally"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"js/ts.tsdk.path": "node_modules\\typescript\\lib",
"typescript.tsdk": "node_modules\\typescript\\lib",
"js/ts.implicitProjectConfig.experimentalDecorators": true,
"cSpell.words": [
"ajnart",
-107
View File
@@ -1,110 +1,3 @@
## [1.69.0](https://github.com/homarr-labs/homarr/compare/v1.68.1...v1.69.0) (2026-07-03)
### Features
* add per-widget secret storage for release provider authentication ([77884a3](https://github.com/homarr-labs/homarr/commit/77884a31794e12a6f7bc59d96c8f61aa429ca68d))
* **config:** enable pnpm global virtual store for worktree support ([#6098](https://github.com/homarr-labs/homarr/issues/6098)) ([3864727](https://github.com/homarr-labs/homarr/commit/3864727502a64052c4e3838468094b30bf04a133))
* **docker-widget:** add per-column visibility toggle ([#6093](https://github.com/homarr-labs/homarr/issues/6093)) ([0765262](https://github.com/homarr-labs/homarr/commit/076526293d13379004461633d7ba4c2e83bd5ffe))
* **docs:** addd in documentation for archiveteam warrior ([f66469d](https://github.com/homarr-labs/homarr/commit/f66469d79797a2da214588f4e8253b03633c59a5))
* simplify caching — TanStack Query as single source of truth ([#6075](https://github.com/homarr-labs/homarr/issues/6075)) ([f19033e](https://github.com/homarr-labs/homarr/commit/f19033e0d53d248aed678737f3d04d5d8c7815b5))
* **widgets:** right-click context menu with auto-generated toggle actions ([#6126](https://github.com/homarr-labs/homarr/issues/6126)) ([7e58858](https://github.com/homarr-labs/homarr/commit/7e58858353ae866efd657270d4bb16d2c7d3e551))
* **widgets:** show calendar events on hover with pin on click ([#2637](https://github.com/homarr-labs/homarr/issues/2637)) ([#6081](https://github.com/homarr-labs/homarr/issues/6081)) ([db3d588](https://github.com/homarr-labs/homarr/commit/db3d58821e4de8d46b110125d0fa36919765248f))
### Bug Fixes
* add id to repository schema and handle missing ids gracefully ([8747d5f](https://github.com/homarr-labs/homarr/commit/8747d5f93ee9a295aad3c7035e8c6c7080bf5ee6))
* address PR review - atomic upsert, MCP metadata, stale tokens, save UX ([757d598](https://github.com/homarr-labs/homarr/commit/757d59854e5ec82d91c053b8bc3d7eaf2e70efc4))
* address PR review findings for widget-only integration refactor ([c892731](https://github.com/homarr-labs/homarr/commit/c892731479b47fab6218bd1f52fa6eccbdaa371e))
* address review - paginate GH releases, GitLab tag_name, docs, test ([e687bc8](https://github.com/homarr-labs/homarr/commit/e687bc8c9277797acaa164a09c74b3808a2cb346))
* apply version filter to LinuxServer.io provider ([34d976e](https://github.com/homarr-labs/homarr/commit/34d976eeea09e396334ce8fce5c492c07e7dd32b))
* catch individual repo fetch failures to prevent batch crashes ([d893515](https://github.com/homarr-labs/homarr/commit/d893515b932608ea08c9c6e90b83f340e40f6f8e))
* compile versionRegex once and add fetch timeouts ([64c7512](https://github.com/homarr-labs/homarr/commit/64c75122833981a49e9c8bf1789d501faff6d62d))
* **deps:** update dependency postcss to ^8.5.16 ([#6169](https://github.com/homarr-labs/homarr/issues/6169)) ([9252fdc](https://github.com/homarr-labs/homarr/commit/9252fdc13971bd982224a31d7be577112f99ec45))
* dns-hole widgets showing stale disconnected state ([#6096](https://github.com/homarr-labs/homarr/issues/6096)) ([2443f1c](https://github.com/homarr-labs/homarr/commit/2443f1cd3ff6765a91783d60d42aa03d9ba63fcd))
* gracefully degrade when widget_secret table is missing or access denied ([30107e4](https://github.com/homarr-labs/homarr/commit/30107e4e76a454ff7402de9a8bc2e0dd6f422aa8))
* handle missing widgetSecrets table and add default releases repo ([af35a50](https://github.com/homarr-labs/homarr/commit/af35a50fa05fee2f0118e98727eb108b0200f762))
* **immich:** v3 API compatibility — use searchAssets for album assets, drop deviceAssetId ([#5977](https://github.com/homarr-labs/homarr/issues/5977)) ([a5ec880](https://github.com/homarr-labs/homarr/commit/a5ec880eec276e0814d2ab3ed7f9bc0b804d0465))
* quick wins — calendar, about, glances, admin UX, releases, iframe, notes ([#6116](https://github.com/homarr-labs/homarr/issues/6116)) ([f9da71a](https://github.com/homarr-labs/homarr/commit/f9da71a19bdfa589aafe26b164d0e0782208ac0a))
* restore ArchiveTeam Warrior as a proper integration ([8a5ad58](https://github.com/homarr-labs/homarr/commit/8a5ad5861ba4c2d0978f1fa3f6115a1c5974f8a2))
* **timetable:** restore station search by removing disabled query guard ([d670b69](https://github.com/homarr-labs/homarr/commit/d670b6939672932c1a3d17ab9913901491cb46e3))
* update CHANGES.md for ArchiveTeam Warrior restoration, remove typo translation key ([62baaf7](https://github.com/homarr-labs/homarr/commit/62baaf78130c4b74e899a948cee17097d8c02593))
* verbose error messages in all release providers ([2155f13](https://github.com/homarr-labs/homarr/commit/2155f13f245f601b99458e227b6e058fbfa44cd9))
* **widgets:** make downloads widget table background transparent ([#6025](https://github.com/homarr-labs/homarr/issues/6025)) ([73bed51](https://github.com/homarr-labs/homarr/commit/73bed51df895250f415efd7a87e721e403b3fd99))
## [1.68.1](https://github.com/homarr-labs/homarr/compare/v1.68.0...v1.68.1) (2026-06-30)
### Bug Fixes
* make source maps opt-in to restore v1.67.0 memory baseline ([57ef0af](https://github.com/homarr-labs/homarr/commit/57ef0af4a252ea39727caeba7e13587dabc6254e)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
* revert production source maps to restore v1.67.0 memory baseline ([60a69e3](https://github.com/homarr-labs/homarr/commit/60a69e31efc7168b8f4309daf75954d7730d883b)), closes [#6123](https://github.com/homarr-labs/homarr/issues/6123)
## [1.68.0](https://github.com/homarr-labs/homarr/compare/v1.67.0...v1.68.0) (2026-06-26)
### Features
* add VPN status widget ([#5812](https://github.com/homarr-labs/homarr/issues/5812)) ([a2bb53a](https://github.com/homarr-labs/homarr/commit/a2bb53a8b8fd17da3d2e23851b75d4e179f66225))
* re-add Docker quick-access button in user menu ([#6054](https://github.com/homarr-labs/homarr/issues/6054)) ([abd1635](https://github.com/homarr-labs/homarr/commit/abd16354bc9ef3dddbb497fd16b03476535046ac)), closes [#2165](https://github.com/homarr-labs/homarr/issues/2165)
* ship source maps for readable production stack traces ([#6031](https://github.com/homarr-labs/homarr/issues/6031)) ([89380f8](https://github.com/homarr-labs/homarr/commit/89380f81ddd132f5b1041a8f2c3b6c6b138923dd))
### Bug Fixes
* **beszel:** harden cache layer and prevent blank widgets ([#6052](https://github.com/homarr-labs/homarr/issues/6052)) ([49c2663](https://github.com/homarr-labs/homarr/commit/49c266374df12fea2404710232ced42e2de11dbf)), closes [#6030](https://github.com/homarr-labs/homarr/issues/6030)
* **beszel:** improve stats date display, add click-to-stats modal, fix grid ordering ([#6026](https://github.com/homarr-labs/homarr/issues/6026)) ([55fb227](https://github.com/homarr-labs/homarr/commit/55fb227305fe51963e486ffb25c4ce6811a2f0e4))
* **beszel:** leverage TanStack Query for stale-while-revalidate caching ([#6055](https://github.com/homarr-labs/homarr/issues/6055)) ([bbc4589](https://github.com/homarr-labs/homarr/commit/bbc4589c9e1608721fb58a6f9b03cca8d65c1c4a))
* **beszel:** use 480m interval for 1w/30d stats views ([4383da3](https://github.com/homarr-labs/homarr/commit/4383da38ef634327c288626c58480cc1e5a2f805))
* **beszel:** widget transparency, error handling, and cache invalidation ([#5993](https://github.com/homarr-labs/homarr/issues/5993)) ([39e4245](https://github.com/homarr-labs/homarr/commit/39e424563a3187dcf0dba5f654e2225be76aef51))
* **icon-picker:** keep URL visible in edit mode, add SVG-first ordering ([#6051](https://github.com/homarr-labs/homarr/issues/6051)) ([e01571b](https://github.com/homarr-labs/homarr/commit/e01571b221812cd19a6d67ebe3cf44432104ec27))
* **integrations:** prevent certificate trust button from submitting the form ([#6020](https://github.com/homarr-labs/homarr/issues/6020)) ([0f66bfd](https://github.com/homarr-labs/homarr/commit/0f66bfd6c128253ba644d80d7c908de5314cf997))
* **manage/about:** bypass self-fetch + add manage error boundary (LLM exploration) ([#6009](https://github.com/homarr-labs/homarr/issues/6009)) ([19f9769](https://github.com/homarr-labs/homarr/commit/19f9769d9e669d56d793e9f6a0d265316113d743)), closes [#6008](https://github.com/homarr-labs/homarr/issues/6008) [#6008](https://github.com/homarr-labs/homarr/issues/6008) [#6008](https://github.com/homarr-labs/homarr/issues/6008)
* **plex:** use series title for media releases instead of season label ([#6083](https://github.com/homarr-labs/homarr/issues/6083)) ([2443b89](https://github.com/homarr-labs/homarr/commit/2443b89b46f88fddbf41f0382a9f3bc5469c6fa8))
* show app first in add item modal ([#6087](https://github.com/homarr-labs/homarr/issues/6087)) ([915c8a3](https://github.com/homarr-labs/homarr/commit/915c8a3501551fdccf786d0c34dc01107d873ce0))
* **truenas:** report actual memory usage and pool free space ([#6017](https://github.com/homarr-labs/homarr/issues/6017)) ([f69e545](https://github.com/homarr-labs/homarr/commit/f69e545d6429aece0e55975c3b2426f20e91e545))
* **unraid:** report disk free space instead of total size ([#6018](https://github.com/homarr-labs/homarr/issues/6018)) ([1fb97ee](https://github.com/homarr-labs/homarr/commit/1fb97ee9c6fbe656a7dd3a9d1e436125982533f4))
* **widgets:** clean up health monitoring disk and size display ([#6019](https://github.com/homarr-labs/homarr/issues/6019)) ([e29c79a](https://github.com/homarr-labs/homarr/commit/e29c79a9028d9311dcb3d8323e0950a444589c7a))
* **widgets:** hide health monitoring default tab option for non-proxmox integrations ([#6021](https://github.com/homarr-labs/homarr/issues/6021)) ([52df56b](https://github.com/homarr-labs/homarr/commit/52df56b19da8e0bcb6c2e5df47b7936ec63f3149))
* **widgets:** hide system disk usage value when it overflows and show it in a tooltip ([#6022](https://github.com/homarr-labs/homarr/issues/6022)) ([a19c9d8](https://github.com/homarr-labs/homarr/commit/a19c9d88caacc86b42854c732818f0b5e5cc8dd5))
## [1.67.0](https://github.com/homarr-labs/homarr/compare/v1.66.1...v1.67.0) (2026-06-19)
### Features
* **analytics:** migrate from Umami to PostHog server-side analytics ([#5983](https://github.com/homarr-labs/homarr/issues/5983)) ([d059023](https://github.com/homarr-labs/homarr/commit/d059023b51a323df027c1cf033454440bd0d7e57))
* Calculate and display total CPU and memory usage ([#5911](https://github.com/homarr-labs/homarr/issues/5911)) ([d9cbd54](https://github.com/homarr-labs/homarr/commit/d9cbd5427d0e5be3e5dbecca8d6069dc8074d810))
### Bug Fixes
* **auth:** isolate session cookies from other Auth.js apps on same host ([#5996](https://github.com/homarr-labs/homarr/issues/5996)) ([e1a245f](https://github.com/homarr-labs/homarr/commit/e1a245ff2d8520d08957c4a7dbe01d039607a973))
* **deps:** update tailwindcss monorepo to ^4.3.1 ([#5988](https://github.com/homarr-labs/homarr/issues/5988)) ([78b6e52](https://github.com/homarr-labs/homarr/commit/78b6e520ac095129398d3da8f2441df408e93e9f))
* **downloads:** sort stalled torrents last when sorting by finish time ([#6011](https://github.com/homarr-labs/homarr/issues/6011)) ([c56d9d6](https://github.com/homarr-labs/homarr/commit/c56d9d6894c717b5c70ab75d6a100d4d1071c2d3)), closes [#5416](https://github.com/homarr-labs/homarr/issues/5416)
## [1.65.0](https://github.com/homarr-labs/homarr/compare/v1.64.0...v1.65.0) (2026-06-12)
### Features
* add MCP server for AI assistant integration ([#5882](https://github.com/homarr-labs/homarr/issues/5882)) ([6baff7b](https://github.com/homarr-labs/homarr/commit/6baff7be3dc8f2473d93d17862b6d0b2df5c4d15))
* add Technitium DNS integration, usable with DNS Hole widgets ([#5832](https://github.com/homarr-labs/homarr/issues/5832)) ([cd1a82a](https://github.com/homarr-labs/homarr/commit/cd1a82a640101cdca88d1e825edb11bd51f5ce66))
* add Uptime Kuma, Audiobookshelf, Paperless-ngx, and Navidrome integrations ([#5910](https://github.com/homarr-labs/homarr/issues/5910)) ([d77c6f8](https://github.com/homarr-labs/homarr/commit/d77c6f8d4e50108f50831ee365e382a535acb254))
* **beszel:** add Beszel integration with system monitoring widgets ([#5900](https://github.com/homarr-labs/homarr/issues/5900)) ([a4db9cf](https://github.com/homarr-labs/homarr/commit/a4db9cf0dcd8034015064f9d23f2d351b1d0c50a))
* **board:** add widget hover overlay on dashboard ([#5888](https://github.com/homarr-labs/homarr/issues/5888)) ([29bd710](https://github.com/homarr-labs/homarr/commit/29bd7101b180acfb750c7d4414c30bd6450065e2))
* custom widgets ([#5889](https://github.com/homarr-labs/homarr/issues/5889)) ([aecbca2](https://github.com/homarr-labs/homarr/commit/aecbca2b81ad1693c599e3168fd0c0723311b3b4))
* **docs:** migrate documentation site into monorepo ([#5912](https://github.com/homarr-labs/homarr/issues/5912)) ([9e421c6](https://github.com/homarr-labs/homarr/commit/9e421c67bd87b5b7065118afedd08e7eb2140773))
* **lang:** add Finnish language support ([#5892](https://github.com/homarr-labs/homarr/issues/5892)) ([2fb7bde](https://github.com/homarr-labs/homarr/commit/2fb7bde9002c2670aa49db804e352c3821fa8097))
* search widgets by integration type when adding items ([#5909](https://github.com/homarr-labs/homarr/issues/5909)) ([48e1844](https://github.com/homarr-labs/homarr/commit/48e1844b5b9dc6d322bc22a972fdd29b6e03d3d2))
* SQLite backup & restore with WASM preview ([#5887](https://github.com/homarr-labs/homarr/issues/5887)) ([5d1f268](https://github.com/homarr-labs/homarr/commit/5d1f268438b84bd19f5a79658a43931dd874cd41))
* **ui:** add global query cache leveraging Redis ([#5811](https://github.com/homarr-labs/homarr/issues/5811)) ([74443a4](https://github.com/homarr-labs/homarr/commit/74443a49b2330b7d71c5ccdb873c61b270d1c72a))
### Bug Fixes
* **boards:** add permissions to board summary schema to fix manage page crash ([#5917](https://github.com/homarr-labs/homarr/issues/5917)) ([a42af0e](https://github.com/homarr-labs/homarr/commit/a42af0e678b58b6c63a8bac4978fb2951c6ddef8))
* **ci:** rename baseBranches to baseBranchPatterns in renovate config ([15e061c](https://github.com/homarr-labs/homarr/commit/15e061cf03f2301996aaf11a25095194e5759fb5))
* **db:** inline custom widget seed data to fix CJS migration crash ([f26007a](https://github.com/homarr-labs/homarr/commit/f26007a29a0b04765baba72122d38a9e5c639a0b))
* **definitions:** gracefully handle sitemap fetch failure in codegen ([7f8b012](https://github.com/homarr-labs/homarr/commit/7f8b0121b303fa09a213c73e6a690463e9fc6b44))
* **docs:** add trailingSlash for GitHub Pages compatibility ([28ceb37](https://github.com/homarr-labs/homarr/commit/28ceb37cbfe45f8ce45fd88e6a8c87ffe4c9b5c2))
* **docs:** disable swcHtmlMinimizer and fix broken links ([905f795](https://github.com/homarr-labs/homarr/commit/905f795b5607744d0aeec4bb65d84aabd1505047))
* gracefully skip widgets with unknown kind instead of crashing ([#5915](https://github.com/homarr-labs/homarr/issues/5915)) ([484b8cf](https://github.com/homarr-labs/homarr/commit/484b8cf9f18122a298dc7fae1027653cd287c09c))
* **image-proxy:** use ArrayBuffer instead of Blob to prevent OOM on Node v24 ([#5926](https://github.com/homarr-labs/homarr/issues/5926)) ([3e464a7](https://github.com/homarr-labs/homarr/commit/3e464a76f2d1e5985f259270a4258f3db4f17c18))
* **integrations:** support truenas json-rpc api, certificate trust, and api key auth ([#5861](https://github.com/homarr-labs/homarr/issues/5861)) ([f6d5f97](https://github.com/homarr-labs/homarr/commit/f6d5f97933cd257ee8851f2d6a27f56e2942076d))
## [1.64.0](https://github.com/homarr-labs/homarr/compare/v1.63.0...v1.64.0) (2026-06-05)
### Features
-96
View File
@@ -1,96 +0,0 @@
# Changes
## Goal
Remove widget-only services from Homarr's global Integration system.
The affected services are release providers and Search.ch. They are only used by specific widgets, so keeping them as global integrations added unnecessary setup, UI, database rows, docs pages, and integration package code.
ArchiveTeam Warrior remains a proper integration but is no longer auto-seeded on first install.
## Why
These services do not need shared integration state.
- Release providers are selected per Releases widget repository.
- Search.ch is only used by the Timetable widget.
Moving them into widget options makes configuration simpler and removes dead-weight integration code from `packages/integrations`.
## What Changed
### Release Providers
- Added release provider definitions in `packages/definitions/src/release-provider.ts`.
- Removed release providers from `IntegrationKind`.
- Moved release-fetching code out of `packages/integrations` and into `packages/request-handler/src/release-providers.ts`.
- Updated the Releases widget to store provider configuration directly in widget options:
- `provider`
- `identifier`
- `versionFilter`
- `providerUrl`
- Updated the Releases widget editor to use a provider select instead of an integration select.
- Updated Docker import mapping so `ghcr.io` maps to `gitHubContainerRegistry`.
- GitHub Container Registry now uses the public OCI registry API and accepts full image references such as `ghcr.io/homarr-labs/homarr:latest`.
### Timetable / Search.ch
- Removed Search.ch as an integration.
- Added a Timetable widget `baseUrl` option, defaulting to `https://search.ch`.
- Moved Search.ch request/parsing logic into `packages/request-handler/src/timetable.ts`.
- Updated the Timetable API to receive `baseUrl` directly from widget options.
### ArchiveTeam Warrior
- Kept as a proper integration (not removed).
- Removed `defaultUrl` from the integration definition to prevent auto-seeding on first install.
### Integration Cleanup
- Removed creators, exports, docs references, and implementation files for the deleted integrations.
- Removed unused dependencies from `packages/integrations/package.json`.
### Database Migration
- Added `packages/db/migrations/custom/0003_remove_widget_only_integrations.ts`.
- The migration moves old integration-linked widget config into widget options, then deletes the old widget-only integration rows.
- Updated the earlier release widget migration to use the new `provider` field instead of `providerIntegrationId`.
### Docs
- Updated Releases widget docs to describe providers instead of integrations.
- Updated Timetable docs to remove Search.ch integration setup.
- Removed obsolete integration docs pages for the deleted integration kinds.
- Updated docs sitemap and integration docs mappings.
### Translations
- Added English labels for the new widget options:
- `widget.timetable.option.baseUrl.label`
- `widget.releases.option.repositories.providerUrl.label`
## How It Works Now
Release provider configuration lives on each Releases widget repository entry. The request handler resolves provider metadata and fetches releases directly from the provider-specific endpoint. GHCR uses anonymous OCI registry access for public packages.
Timetable passes its widget URL option directly to its API router and request handler. No global integration lookup is needed.
ArchiveTeam Warrior continues to use the standard integration system, but users must manually add the integration (it is not auto-created on install).
## Deliberate Non-Goals
- No provider secret/token support was added.
- No new secret storage model was introduced.
- No compatibility layer was kept in `packages/integrations`.
Widget options are client-visible, so storing provider credentials there would be wrong. If authenticated provider requests are needed later, they need a separate secret-storage design.
## Verification So Far
- Focused typechecks passed for affected packages.
- Full `pnpm turbo typecheck` passed.
- Full `pnpm lint` passed with existing warnings.
- Focused release provider normalization tests passed.
- Widget translation test passed.
- Live GHCR smoke checks passed for `ghcr.io/muchobien/pocketbase:latest` and `ghcr.io/homarr-labs/homarr:latest`.
- `pnpm test packages/integrations/test/pi-hole.spec.ts` passed earlier, but failed on rebased reruns because Pi-hole reported `domainsBeingBlocked` as `-2`/`0` instead of `> 1`.
+1 -187
View File
@@ -1,193 +1,7 @@
# ── CODEOWNERS ───────────────────────────────────────
* @homarr-labs/maintainers
# Specific features / integrations
# These owners come from the PR that introduced each integration implementation.
# Specific rules override the global fallback because CODEOWNERS uses the last
# matching pattern; do not include @homarr-labs/maintainers here unless they
# should also be requested for that integration.
# AdGuard Home - introduced in https://github.com/homarr-labs/homarr/pull/929 by @hillaliy
apps/docs/docs/integrations/adguard-home @hillaliy
packages/integrations/src/adguard-home @hillaliy
# Anchor - introduced in https://github.com/homarr-labs/homarr/pull/5040 by @ohmzi
apps/docs/docs/integrations/anchor @ohmzi
packages/integrations/src/anchor @ohmzi
# ArchiveTeam Warrior - introduced in https://github.com/homarr-labs/homarr/pull/5784 by @darbyjack
apps/docs/docs/integrations/archive-team-warrior @darbyjack
packages/integrations/src/archive-team-warrior @darbyjack
# Audiobookshelf - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
apps/docs/docs/integrations/audiobookshelf @ajnart
packages/integrations/src/audiobookshelf @ajnart
# Beszel - introduced in https://github.com/homarr-labs/homarr/pull/5900 by @ajnart
apps/docs/docs/integrations/beszel @ajnart
packages/integrations/src/beszel @ajnart
# Codeberg - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
apps/docs/docs/integrations/codeberg @Aandree5
packages/integrations/src/codeberg @Aandree5
# Coolify - introduced in https://github.com/homarr-labs/homarr/pull/4789 by @Rhiz3K
apps/docs/docs/integrations/coolify @Rhiz3K
packages/integrations/src/coolify @Rhiz3K
# Dash. - introduced in https://github.com/homarr-labs/homarr/pull/1541 by @manuel-rw
apps/docs/docs/integrations/dash-dot @manuel-rw
packages/integrations/src/dashdot @manuel-rw
# Docker Hub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
apps/docs/docs/integrations/docker-hub @Aandree5
packages/integrations/src/docker-hub @Aandree5
# Emby - introduced in https://github.com/homarr-labs/homarr/pull/2466 by @Meierschlumpf
apps/docs/docs/integrations/emby @Meierschlumpf
packages/integrations/src/emby @Meierschlumpf
# GitHub - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
apps/docs/docs/integrations/github @Aandree5
packages/integrations/src/github @Aandree5
# GitHub Container Registry - introduced in https://github.com/homarr-labs/homarr/pull/3727 by @Aandree5
apps/docs/docs/integrations/github-containerregistry @Aandree5
packages/integrations/src/github-container-registry @Aandree5
# GitLab - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
apps/docs/docs/integrations/gitlab @Aandree5
packages/integrations/src/gitlab @Aandree5
# Glances - introduced in https://github.com/homarr-labs/homarr/pull/5102 by @manuel-rw
apps/docs/docs/integrations/glances @manuel-rw
packages/integrations/src/glances @manuel-rw
# Gluetun - introduced in https://github.com/homarr-labs/homarr/pull/5812 by @Lok3sh-kumar
apps/docs/docs/integrations/gluetun @Lok3sh-kumar
packages/integrations/src/gluetun @Lok3sh-kumar
# Gotify - introduced in https://github.com/homarr-labs/homarr/pull/5847 by @Nerdy-Technician
apps/docs/docs/integrations/gotify @Nerdy-Technician
packages/integrations/src/gotify @Nerdy-Technician
# Home Assistant - introduced in https://github.com/homarr-labs/homarr/pull/578 by @manuel-rw
apps/docs/docs/integrations/home-assistant @manuel-rw
packages/integrations/src/homeassistant @manuel-rw
# ICal - introduced in https://github.com/homarr-labs/homarr/pull/3980 by @NicolasNewman
apps/docs/docs/integrations/ical @NicolasNewman
packages/integrations/src/ical @NicolasNewman
# Immich - introduced in https://github.com/homarr-labs/homarr/pull/5165 by @manuel-rw
apps/docs/docs/integrations/immich @manuel-rw
packages/integrations/src/immich @manuel-rw
# Jellyfin - introduced in https://github.com/homarr-labs/homarr/pull/672 by @manuel-rw
apps/docs/docs/integrations/jellyfin @manuel-rw
packages/integrations/src/jellyfin @manuel-rw
# Jellyseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw
apps/docs/docs/integrations/jellyseerr @manuel-rw
packages/integrations/src/jellyseerr @manuel-rw
# LinuxServer.io - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5
apps/docs/docs/integrations/linux-server-io @Aandree5
packages/integrations/src/linuxserverio @Aandree5
# Navidrome - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
apps/docs/docs/integrations/navidrome @ajnart
packages/integrations/src/navidrome @ajnart
# Nextcloud - introduced in https://github.com/homarr-labs/homarr/pull/2501 by @manuel-rw
apps/docs/docs/integrations/nextcloud @manuel-rw
packages/integrations/src/nextcloud @manuel-rw
# npm - introduced in https://github.com/homarr-labs/homarr/pull/3253 by @Aandree5
apps/docs/docs/integrations/npm @Aandree5
packages/integrations/src/npm @Aandree5
# ntfy - introduced in https://github.com/homarr-labs/homarr/pull/2900 by @itzTheMeow
apps/docs/docs/integrations/ntfy @ajnart @manuel-rw
packages/integrations/src/ntfy @ajnart @manuel-rw
# OpenMediaVault - introduced in https://github.com/homarr-labs/homarr/pull/1142 by @hillaliy
apps/docs/docs/integrations/open-media-vault @hillaliy
packages/integrations/src/openmediavault @hillaliy
# OPNsense - introduced in https://github.com/homarr-labs/homarr/pull/3424 by @Oupsman
apps/docs/docs/integrations/opnsense @Oupsman
packages/integrations/src/opnsense @Oupsman
# Overseerr - introduced in https://github.com/homarr-labs/homarr/pull/774 by @manuel-rw
apps/docs/docs/integrations/overseerr @manuel-rw
packages/integrations/src/overseerr @manuel-rw
# Paperless-ngx - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
apps/docs/docs/integrations/paperless-ngx @ajnart
packages/integrations/src/paperless-ngx @ajnart
# PeaNUT - introduced in https://github.com/homarr-labs/homarr/pull/5862 by @afonsojramos
apps/docs/docs/integrations/peanut @afonsojramos
packages/integrations/src/peanut @afonsojramos
# Pi-hole - introduced in https://github.com/homarr-labs/homarr/pull/521 by @Meierschlumpf
apps/docs/docs/integrations/pi-hole @Meierschlumpf
packages/integrations/src/pi-hole @Meierschlumpf
# Plex - introduced in https://github.com/homarr-labs/homarr/pull/1342 by @hillaliy
apps/docs/docs/integrations/plex @hillaliy
packages/integrations/src/plex @hillaliy
# Prowlarr - introduced in https://github.com/homarr-labs/homarr/pull/965 by @hillaliy
apps/docs/docs/integrations/prowlarr @hillaliy
packages/integrations/src/prowlarr @hillaliy
# Proxmox - introduced in https://github.com/homarr-labs/homarr/pull/1969 by @Meierschlumpf
apps/docs/docs/integrations/proxmox @Meierschlumpf
packages/integrations/src/proxmox @Meierschlumpf
# Quay - introduced in https://github.com/homarr-labs/homarr/pull/3607 by @Aandree5
apps/docs/docs/integrations/quay @Aandree5
packages/integrations/src/quay @Aandree5
# search.ch - introduced in https://github.com/homarr-labs/homarr/pull/5180 by @Meierschlumpf
apps/docs/docs/integrations/search-ch @Meierschlumpf
packages/integrations/src/search-ch @Meierschlumpf
# Speedtest Tracker - introduced in https://github.com/homarr-labs/homarr/pull/5363 by @kylegrieder
apps/docs/docs/integrations/speedtest-tracker @kylegrieder
packages/integrations/src/speedtest-tracker @kylegrieder
# Technitium DNS - introduced in https://github.com/homarr-labs/homarr/pull/5832 by @lopsch
apps/docs/docs/integrations/technitium-dns @lopsch
packages/integrations/src/technitium @lopsch
# Tracearr - introduced in https://github.com/homarr-labs/homarr/pull/5060 by @StrandedTurtle
apps/docs/docs/integrations/tracearr @StrandedTurtle
packages/integrations/src/tracearr @StrandedTurtle
# TrueNAS - introduced in https://github.com/homarr-labs/homarr/pull/3745 by @manuel-rw
apps/docs/docs/integrations/truenas @manuel-rw @afonsojramos
packages/integrations/src/truenas @manuel-rw @afonsojramos
# Umami - introduced in https://github.com/homarr-labs/homarr/pull/5362 by @febrile42
apps/docs/docs/integrations/umami @febrile42
packages/integrations/src/umami @febrile42
# UniFi Controller - introduced in https://github.com/homarr-labs/homarr/pull/2236 by @pitschi
apps/docs/docs/integrations/unifi-controller @pitschi
packages/integrations/src/unifi-controller @pitschi
# Unraid - introduced in https://github.com/homarr-labs/homarr/pull/4439 by @manuel-rw
apps/docs/docs/integrations/unraid @manuel-rw
packages/integrations/src/unraid @manuel-rw
# Uptime Kuma - introduced in https://github.com/homarr-labs/homarr/pull/5910 by @ajnart
apps/docs/docs/integrations/uptime-kuma @ajnart
packages/integrations/src/uptime-kuma @ajnart
# Exempt Renovate-managed files (no owners)
# Exempt Renovate‑managed files (no owners)
package.json
**/package.json
package-lock.json
+1 -2
View File
@@ -1,4 +1,4 @@
FROM node:24.18.0-alpine AS base
FROM node:24.16.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
@@ -42,7 +42,6 @@ RUN mkdir -p /var/cache/nginx && \
COPY --from=builder /app/apps/nextjs/next.config.ts .
COPY --from=builder /app/apps/nextjs/package.json .
COPY --from=builder /app/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=builder /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node /app/build/better_sqlite3.node
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};
@@ -23,7 +23,6 @@ Homarr supports multiple authentication options, from internal userbase (credent
| 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>
@@ -125,8 +124,6 @@ Homarr supports multiple authentication options, from internal userbase (credent
Roles are fetched from group claims. This can also be changed to roles for example added to a azure app registration by using the `AUTH_OIDC_GROUPS_ATTRIBUTE`.
If you'd rather manage group memberships for OIDC users locally (instead of via the IdP), set `AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT=true`. While enabled, Homarr stops syncing OIDC users' group memberships from the groups claim, and admins can add/remove OIDC users on a group's members page. If you turn this back to `false`, the next login of each user resumes the regular sync and reconciles their groups with the IdP's claim again, which may remove memberships that were added manually while local management was enabled.
<details>
<summary>
Example Setup
@@ -200,7 +197,6 @@ Homarr supports multiple authentication options, from internal userbase (credent
| ``AUTH_OIDC_AUTO_LOGIN`` | Automatically redirect to OIDC login | false |
| ``AUTH_OIDC_SCOPE_OVERWRITE`` | Overwrite default scopes (openid, profile, email) | openid email profile groups |
| ``AUTH_OIDC_GROUPS_ATTRIBUTE`` | Attribute used for groups (roles) claim | groups |
| ``AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT`` | Stop syncing group memberships of OIDC users from the groups claim and manage them locally via the group members page instead | false |
| ``AUTH_OIDC_NAME_ATTRIBUTE_OVERWRITE`` | Overwrite name attribute. By default it will use preferred_username if it does not contain a `@` and otherwise name. | --- |
| ``AUTH_OIDC_FORCE_USERINFO`` | Force userinfo endpoint to be used for user information. | false |
| ``AUTH_OIDC_ENABLE_DANGEROUS_ACCOUNT_LINKING`` | Enable account linking for OIDC provider. This will link the OIDC accounts by email. Be sure that you have verified emails to prevent stealing accounts. | false |
+1 -1
View File
@@ -22,4 +22,4 @@ Homarr is completely free to use. We'd highly appreciate if you consider donatin
## 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](./get-in-touch).
@@ -21,14 +21,13 @@ import manageImportDecryptionToken from "./img/old-homarr-import-decryption-toke
:::tip
Read our [glossary](/docs/getting-started/glossary) if you are new to Homarr.
Read our [glossary](./glossary) if you are new to Homarr.
:::
Congratulations on installing Homarr. Now it's time to get yourself familiar with the concepts of Homarr and how to use it.
## Onboarding
The onboard will show the first time you start Homarr.
This process will help you with your first steps and adding some initial data, such as the administrator account.
@@ -46,68 +45,50 @@ You can either import a previous dashboard using the ZIP archive or start from s
You may refresh anytime and continue from a different device - the onboarding is saved immediately on the server for your convenience.
### Starting from scratch
Depending on your auth provider configuration you will either first see the admin user creation form or the external group creation form.
If the credentials provider was selected, you'll be prompted to create an admin user for Homarr.
This user cannot be deleted and has full control over Homarr and all of its boards.
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.
If credentials provider was selected, you'll be prompted to create an admin user for Homarr.
This user cannot be deleted and has full control over Homarr and all of it's boards.
You must choose an username and password (both can later be changed).
We enforce strict password requirements, when you enter your password, to protect your instance if you accidentially
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.
When a user logs in through the provider, any of their groups that match groups in Homarr will be assigned to the user.
The name of the group must match exactly the name of the group in the provider.
This group will be used to synchronize users from your provider to Homarr.
When a user is logged in through the provider the groups of him that match groups with any in Homarr will be added to the user.
The name of the group has to match exactly the name of the group in the provider.
<img
src={imageStartFromScratchStepExternalAdminGroup}
alt={"Screenshot form to create external admin group"}
width={300}
/>
<img src={imageStartFromScratchStepExternalAdminGroup} alt={"Screenshot form to create external admin group"} width={300} />
Next, you can configure some basic settings for Homarr. We recommend to leave all settings at default for most users.
Here you can also disable analytics. Please note that our analytics are completly anonymous and is fully compliant with GDPR and PECR.
<img
src={imageStartFromScratchStepServerSettings}
alt={"Screenshot of analytics and indexing settings in the onboarding"}
width={300}
/>
<img src={imageStartFromScratchStepServerSettings} alt={"Screenshot of analytics and indexing settings in the onboarding"} width={300} />
### Setting up integrations
After configuring server settings, the onboarding guides you through setting up your integrations.
#### Docker service discovery
If Docker is available (socket mounted), Homarr automatically detects running containers and matches them to known integration types.
Matching integrations are pre-selected in the integration list, saving you from manually picking each one.
#### URL templating
You can define a base URL template to auto-fill integration URLs in bulk.
Two modes are supported:
- **Subdomain mode** - e.g. `{app}.example.com` where `{app}` is replaced with the integration name
- **Port mode** - e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
- **Subdomain mode** — e.g. `{app}.example.com` where `{app}` is replaced with the integration name
- **Port mode** — e.g. `192.168.1.1:{port}` where `{port}` is replaced with the integration's default port
#### API key helper links
For integrations that require API keys (such as Sonarr, Radarr, SABnzbd, or Seerr), a helper link opens the relevant settings page in the target application so you can quickly copy your key.
#### Creating apps from Docker containers
You can optionally auto-create bookmark apps for all discovered Docker containers, making them immediately available on your board.
:::note
Release providers and Search.ch are configured directly in their widgets instead of the integrations page.
Some niche and registry integrations (GitHub, npm, Docker Hub, GitLab, Codeberg, LinuxServer.io, GHCR, Quay, Search.ch) are hidden from the onboarding selection grid but remain available on the manage integrations page.
:::
@@ -116,41 +97,30 @@ 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**
1. Navigate to your old Homarr, open your management pages, open the tools submenu and open `Migrate to 1.0`.
1. Navigate to your old Homarr, open your management pages, open the tools submenu and open ``Migrate to 1.0``.
2. Check the contents that you want to export. We recommend you to export all data.
3. Click on the export data button
4. Save the ZIP file, that is being downloaded, on your device. If you selected "integrations" or "users", you also get an encryption / decryption key.
Copy it and do not loose it!
5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose `Import from Homarr before 1.0`
Copy it and do not loose it!
5. Go to your new >1.0.0 Homarr instance and start the onboarding. Choose ``Import from Homarr before 1.0``
6. Drag and drop your previously downloaded ZIP archive into the file upload: <br/><img src={manageImportOldHomarrDropzone} style={{ maxWidth: 500 }} />
7. Next, choose the data that you want to import and how to import it. Since screen sizes were removed in 1.0, we create a new board for every screen size by default. We recommend you to only keep the best layout and adjust it later in 1.0.0 - but you can also import all of them and pick later.<br/><img src={manageImportOldHomarrChooseData} style={{ maxWidth: 500 }} />
8. If you previously exported integrations or users, you will be asked to decrypt the data safely using your obtained encryption token. Paste into the modal and confirm your action.<br/><img src={manageImportDecryptionToken} style={{ maxWidth: 500 }} />
9. Next, you can [follow the same setup as when starting from scratch](#starting-from-scratch). After you completed the onboarding, all of your data will be migrated and available.
## Creating your first board
In Homarr, you organize your apps and widgets on boards.
Boards can be organized using the interactive drag and drop.
Let's create your first board by navigating here: `Manage` > `Boards` > `New board`:
Let's create your first board by navigating here: ``Manage`` > ``Boards`` > ``New board``:
![](./img/manage-create-board-button.png)
A board must always have a name and can either be public or private.
@@ -163,13 +133,12 @@ Congratulations - you've just created your first board. You can see boards, that
![](./img/manage-view-board.png)
## Add your first app to Homarr
To add links to your dashboard, you must first add them as an "app".
Navigate to this page to add an app:
![](./img/manage-navigate-apps.png)
Next, click on the `New app` button. You'll need to provide a name and an icon to create an app.
Next, click on the ``New app`` button. You'll need to provide a name and an icon to create an app.
The name must be at least one character long but you can provide any image URL for the icon.
Homarr ships with over 11K icons - so you can just search by entering a search text instead.
@@ -180,9 +149,8 @@ After you created the app, you'll see in on the apps page:
![](./img/manage-created-app.png)
## Arrange and organize your board
Let's add your freshly created app, from the previous step, to the board.
To do this, edit your previously created board by clicking on the big `Open board` button:
To do this, edit your previously created board by clicking on the big ``Open board`` button:
![](./img/manage-view-board.png)
@@ -197,38 +165,23 @@ 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.
@@ -275,18 +228,17 @@ Be sure to save your changes by exiting edit mode!
![](./img/exit-edit-mode.png)
## Adding your first integration
Now it's time to integrate some basic data into your board.
To do this, we first need to create an integration.
An integration is a connection between Homarr and a third party application - usually something that you run and deploy yourself.
Navigate to `Manage` > `Integrations` (for example via your user menu).
Navigate to ``Manage`` > ``Integrations`` (for example via your user menu).
As an example, we are going to use the public demo instance of **Dash.** - a popular application to monitor CPU / hardware usage.
Choose the **Dash.** integration from the list of available integrations:
![](./img/choose-integration.gif)
Next, choose a name (or leave it at default), enter `https://dash.mauz.dev/` as the URL in the form and click on `Test connection and create`.
Next, choose a name (or leave it at default), enter ``https://dash.mauz.dev/`` as the URL in the form and click on ``Test connection and create``.
Pressing the save button will perform a test request to the integration, to confirm that the networking setup is working as expected.
Sometimes invalid configurations, firewalls and other mistakes can lead to connectivity problems - [follow this guide here](/docs/management/integrations/#troubleshooting-connection) if you experience issues.
@@ -302,14 +254,13 @@ the target application or your router.
:::
## Embed integration data using widgets
Let's add your integration, that you have configured in the section above, as a widget to your board.
First, navigate back to your board that you want to add it to.
Next, enter edit mode again and add another item to your board:
![](./img/manage-board-header-add-item-menu.png)
Instead of choosing an app, let's choose `System Health Monitoring` this time.
Instead of choosing an app, let's choose ``System Health Monitoring`` this time.
Click on "Add to board":
![](./img/manage-board-header-choose-system-health.png)
@@ -339,12 +290,10 @@ As with every change, don't forget to save your changes using the exit edit mode
![](./img/exit-edit-mode.png)
## Grouping your items
As the last step, let's organize your freshly created items using two different options.
You can choose either or both of them on your boards.
### Using dynamic sections
Dynamic sections are a simple, resizable visual group to group items.
![](./img/create-dynamic-section.png)
@@ -354,7 +303,6 @@ After adding a dynamic section, you can simply drag and drop items into the sect
![](./img/dynamic-section.gif)
### Using categories
Categories are more advanced and complicated than dynamic sections.
To create a category, open the add item modal and choose "new category":
@@ -381,33 +329,28 @@ You cannot reduce their horizontal width.
After completing the onboarding, Homarr provides interactive guided tours to help you discover key features.
### Management tour
When you first visit the management area (`/manage`), Homarr walks you through the main pages: boards, apps, integrations, and users (admin only).
Each page highlights the list view and the create action so you know where to find and add things.
### Board tour
When you first open a board, a tour highlights the board header elements: search, edit mode, settings, board switcher, and user menu.
### Tour controls
- Press **Enter** (or click **Next**) to advance through tour steps
- Tours auto-start on first visit and do not repeat once completed
- Tours are skipped on mobile screens (below `sm` breakpoint)
- You can **reset tours** from the user settings page under **Manage > Users > [Your user] > General**
## Next steps
You're done with the basic setup 🔥. Now it's time to organize and customize even more.
We highly recommend you to experiment with different options, styles and configurations.
See these curated pages for your next steps:
- Create new apps using the [apps management](/docs/management/apps/)
- Customize your board or add more boards using the [boards management](/docs/management/boards/)
- Integrate software that you use using the [integrations management](/docs/management/integrations/)
- Create new apps using the [apps management](../management/apps/)
- Customize your board or add more boards using the [boards management](../management/boards/)
- Integrate software that you use using the [integrations management](../management/integrations)
- Choose from our beautiful and interactive [widgets](/docs/category/widgets)
- See [all supported integrations](/docs/category/integrations)
- Integrate with Homarr with your own application using [the API](/docs/management/api/)
- Integrate with Homarr with your own application using [the API](../management/api/)
:::danger
@@ -2,9 +2,9 @@
<img src="https://raw.githubusercontent.com/homarr-labs/charts/refs/heads/main/charts/homarr/icon.svg" align="right" width="92" alt="homarr logo">
![Version: 8.20.0](https://img.shields.io/badge/Version-8.20.0-informational?style=flat)
![Version: 8.19.0](https://img.shields.io/badge/Version-8.19.0-informational?style=flat)
![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat)
![AppVersion: v1.68.0](https://img.shields.io/badge/AppVersion-v1.68.0-informational?style=flat)
![AppVersion: v1.60.0](https://img.shields.io/badge/AppVersion-v1.60.0-informational?style=flat)
A Helm chart to deploy homarr for Kubernetes
@@ -383,7 +383,6 @@ All available values are listed on the [artifacthub](https://artifacthub.io/pack
| 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 |
@@ -1,35 +0,0 @@
---
title: "ArchiveTeam Warrior"
description: "ArchiveTeam Warrior is an easy-to-run virtual machine that helps preserve websites by using some of your bandwidth and disk space to download and upload content to ArchiveTeam’s archive."
hide_title: true
---
import { IntegrationHeader } from "@site/src/components/integrations/header";
import { AddingIntegration } from "@site/src/components/integrations/adding";
import { IntegrationSecrets } from "@site/src/components/integrations/secrets";
import { archiveTeamWarriorIntegration } from ".";
<IntegrationHeader integration={archiveTeamWarriorIntegration} categories={["archiving"]} />
The ArchiveTeam Warrior integration lets Homarr connect to your ArchiveTeam Warrior instance so you can manage and monitor your archiving tasks directly from your dashboard.
### Adding the integration
<AddingIntegration />
### Secrets
ArchiveTeam Warrior does not require authentication by default. If your instance is protected with basic authentication, you can provide a username and password.
<IntegrationSecrets
secrets={[
{
credentials: [],
steps: [],
},
{
credentials: ["username", "password"],
steps: [],
},
]}
/>
@@ -1,9 +0,0 @@
import { IntegrationDefinition } from "@site/src/types";
export const archiveTeamWarriorIntegration: IntegrationDefinition = {
name: "ArchiveTeam Warrior",
description:
"ArchiveTeam Warrior is an easy-to-run virtual machine that helps preserve websites by using some of your bandwidth and disk space to download and upload content to ArchiveTeam’s archive.",
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/archiveteam-warrior.png",
path: "../../integrations/archiveteam-warrior",
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 948 KiB

@@ -1,54 +0,0 @@
---
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 beszelDashboard from './img/beszel-dashboard.png';
<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.
### Screenshots
<img src={beszelDashboard} alt="Beszel dashboard overview showing all four widgets" />
### Widgets & Capabilities
<IntegrationCapabilites
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',
]
}]} />
:::tip
Beszel uses PocketBase under the hood. The default admin credentials are set during the initial Beszel setup.
:::
@@ -1,9 +0,0 @@
import { IntegrationDefinition } from "@site/src/types";
export const beszelIntegration: IntegrationDefinition = {
name: "Beszel",
description:
"Beszel is a lightweight server monitoring platform with Docker stats, historical data, and alert functions.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg",
path: "../../integrations/beszel",
};
@@ -0,0 +1,37 @@
---
title: 'Codeberg'
description: "Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects."
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 { codebergIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={codebergIntegration}
categories={['Version Control', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ['personalAccessToken'],
steps: []
}]} />
@@ -0,0 +1,9 @@
import { IntegrationDefinition } from "@site/src/types";
export const codebergIntegration: IntegrationDefinition = {
name: "Codeberg",
description:
"Codeberg is a non-profit, community-driven platform for hosting and collaborating on software projects.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/codeberg.svg",
path: "../../integrations/codeberg",
};
@@ -0,0 +1,37 @@
---
title: 'Docker Hub'
description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images."
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 { dockerHubIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={dockerHubIntegration}
categories={['Container Management', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ['username', 'personalAccessToken'],
steps: []
}]} />
@@ -0,0 +1,8 @@
import { IntegrationDefinition } from "@site/src/types";
export const dockerHubIntegration: IntegrationDefinition = {
name: "Docker Hub",
description: "Docker Hub is a cloud-based registry service for sharing and managing Docker images.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/docker.svg",
path: "../../integrations/docker-hub",
};
+6 -6
View File
@@ -58,12 +58,12 @@ In Windows, you'll need to adjust the slashes: `-v //var/run/docker.sock:/var/ru
### 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
- **Restart** - restart a container
- **Remove** - remove a container
- **Add to Homarr** - create a bookmark app from the container
- **View logs** — opens a dedicated logs page with a live terminal
- **Start** — start a stopped container
- **Stop** — stop a running container
- **Restart** — restart a container
- **Remove** — remove a container
- **Add to Homarr** — create a bookmark app from the container
You can also search and filter containers by **container** or **image** name:
@@ -0,0 +1,42 @@
---
title: 'GitHub Container Registry'
description: "GitHub Container Registry is a platform for hosting and managing packages."
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 { gitHubContainerRegistryIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={gitHubContainerRegistryIntegration}
categories={['Package Management', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ["personalAccessToken"],
steps: []
}, {
tabLabel: "GitHub App",
credentials: ['githubAppId', 'githubInstallationId', 'privateKey'],
steps: ['Create GitHub App', 'Create a private key for it', 'Install it to your organization / user']
}]} />
@@ -0,0 +1,11 @@
import { IntegrationDefinition } from "@site/src/types";
export const gitHubContainerRegistryIntegration: IntegrationDefinition = {
name: "GitHub Container Registry",
description: "GitHub Container Registry is a platform for hosting and managing packages.",
iconUrl: {
light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg",
dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg",
},
path: "../../integrations/github-packages",
};
@@ -0,0 +1,41 @@
---
title: 'GitHub'
description: "GitHub is a web-based platform for version control and collaboration."
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 { githubIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={githubIntegration}
categories={['Version Control', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ['personalAccessToken'],
steps: []
}, {
tabLabel: "GitHub App",
credentials: ['githubAppId', 'githubInstallationId', 'privateKey'],
steps: ['Create GitHub App', 'Create a private key for it', 'Install it to your organization / user']
}]} />
@@ -0,0 +1,11 @@
import { IntegrationDefinition } from "@site/src/types";
export const githubIntegration: IntegrationDefinition = {
name: "GitHub",
description: "GitHub is a web-based platform for version control and collaboration.",
iconUrl: {
light: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github.svg",
dark: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/github-light.svg",
},
path: "../../integrations/github",
};
@@ -0,0 +1,37 @@
---
title: 'GitLab'
description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager."
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 { gitlabIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={gitlabIntegration}
categories={['Version Control', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ['personalAccessToken'],
steps: []
}]} />
@@ -0,0 +1,8 @@
import { IntegrationDefinition } from "@site/src/types";
export const gitlabIntegration: IntegrationDefinition = {
name: "GitLab",
description: "GitLab is a web-based DevOps lifecycle tool that provides a Git repository manager.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gitlab.svg",
path: "../../integrations/gitlab",
};
@@ -1,52 +0,0 @@
---
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';
<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.
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: vpnWidget,
}]}
/>
### Setting up the Gluetun control server
Refer to the [Gluetun control server documentation](https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md#authentication) to enable and configure authentication.
### Adding the integration
<AddingIntegration />
### Secrets
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: [],
},,]} />
@@ -1,8 +0,0 @@
import { IntegrationDefinition } from "@site/src/types";
export const gluetunIntegration: IntegrationDefinition = {
name: "Gluetun",
description: "VPN control server integration for Homarr's VPN widget.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gluetun.svg",
path: "../../integrations/gluetun",
};
@@ -1,8 +0,0 @@
import { IntegrationDefinition } from "@site/src/types";
export const gotifyIntegration: IntegrationDefinition = {
name: "Gotify",
description: "Gotify is a simple, self-hosted server for sending and receiving messages in real time.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/gotify.svg",
path: "../../integrations/gotify",
};
@@ -1,25 +1,26 @@
---
title: 'Gotify'
description: "Gotify is a simple, self-hosted server for sending and receiving messages in real time."
title: 'LinuxServer.io'
description: "LinuxServer.io is a platform for managing and deploying containerized applications."
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 { AddingIntegration } from '@site/src/components/integrations/adding';
import { IntegrationSecrets } from '@site/src/components/integrations/secrets';
import { linuxServerIoIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={gotifyIntegration}
categories={['Notifications', 'Messaging']}
integration={linuxServerIoIntegration}
categories={['Container Management', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: notificationsWidget
widget: releasesWidget
}]}
/>
@@ -28,6 +29,6 @@ import { gotifyIntegration } from '.';
### Secrets
<IntegrationSecrets secrets={[{
credentials: ['username', 'password'],
credentials: [],
steps: []
}]} />
}]} />
@@ -0,0 +1,8 @@
import { IntegrationDefinition } from "@site/src/types";
export const linuxServerIoIntegration: IntegrationDefinition = {
name: "LinuxServer.io",
description: "LinuxServer.io is a platform for managing and deploying containerized applications.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/linuxserver-io.svg",
path: "../../integrations/linux-server-io",
};
+34
View File
@@ -0,0 +1,34 @@
---
title: 'NPM'
description: "NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages."
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 { npmIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={npmIntegration}
categories={['Package Management', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}]} />
+9
View File
@@ -0,0 +1,9 @@
import { IntegrationDefinition } from "@site/src/types";
export const npmIntegration: IntegrationDefinition = {
name: "NPM",
description:
"NPM is a package manager for the JavaScript programming language, primarily used for managing Node.js packages.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/npm.svg",
path: "../../integrations/npm",
};
@@ -0,0 +1,37 @@
---
title: 'Quay'
description: "Quay is a container registry for storing and managing container images."
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 { quayIntegration } from '.';
import { releasesWidget } from '@site/docs/widgets/releases';
<IntegrationHeader
integration={quayIntegration}
categories={['Container Registry', 'Software']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{
widget: releasesWidget
}]}
/>
### Adding the integration
<AddingIntegration />
### Secrets
<IntegrationSecrets secrets={[{
credentials: [],
steps: []
}, {
credentials: ["personalAccessToken"],
steps: []
}]} />
@@ -0,0 +1,8 @@
import { IntegrationDefinition } from "@site/src/types";
export const quayIntegration: IntegrationDefinition = {
name: "Quay",
description: "Quay is a container registry for storing and managing container images.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/quay.png",
path: "../../integrations/quay",
};
@@ -0,0 +1,28 @@
---
title: 'Search.ch'
description: "Search.ch supports searching for timetables in Switzerland."
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 { IconDownload } from "@tabler/icons-react";
import { searchChIntegration } from '.';
import Alert from '@theme/Admonition';
import { timetableWidget } from '@site/docs/widgets/timetable';
<IntegrationHeader
integration={searchChIntegration}
categories={['Timetable']}
/>
### Widgets & Capabilities
<IntegrationCapabilites
items={[{ widget: timetableWidget }]}
/>
### Adding the integration
<AddingIntegration />
@@ -0,0 +1,7 @@
import { IntegrationDefinition } from "@site/src/types";
export const searchChIntegration: IntegrationDefinition = {
name: "Search.ch",
description: "Search.ch supports searching for timetables in Switzerland.",
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/search-ch.png",
path: "../../integrations/search-ch",
};
@@ -19,7 +19,7 @@ import { dnsHoleControlsWidget } from '@site/docs/widgets/dns-hole-controls';
/>
<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.
Homarr supports Technitium DNS v11 and above. The correct API version is detected automatically — no manual configuration is required.
</Alert>
### Widgets & Capabilities
@@ -44,7 +44,7 @@ import Alert from '@theme/Admonition';
steps: [
"In the TrueNAS web interface, click your user avatar in the top-right corner and select \"API Keys\".",
"Click \"Add\", give the key a name such as \"Homarr\", and confirm.",
"Copy the generated key immediately - TrueNAS only shows it once - and paste it as the API Key when creating the integration in Homarr.",
"Copy the generated key immediately — TrueNAS only shows it once — and paste it as the API Key when creating the integration in Homarr.",
],
},
{
+1 -1
View File
@@ -47,7 +47,7 @@ import { umamiWidget } from '../../widgets/umami';
credentials: ['username', 'password'],
steps: [
'Use the username and password of your Umami account',
'Note: username/password auth is for self-hosted instances only - Umami Cloud requires an API key',
'Note: username/password auth is for self-hosted instances only — Umami Cloud requires an API key',
]
}
]} />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

+28 -17
View File
@@ -6,6 +6,8 @@ tags:
- Interface
---
import { ApiReferenceReact } from "@scalar/api-reference-react"
Using the Homarr API, you can interface with the application with your own code and business logic.
It enables you to trigger almost all actions that you can perform from the Homarr UI.
With the API, you can build resilient systems that automatically update Homarr with data - for example syncing it with
@@ -13,25 +15,20 @@ your deployments in a cluster or dynamically adding integrations depending on ho
This page is language-neutral, and you must implement the API on your own in your desired language.
It requires you to know your programming language well and have a basic understanding of HTTP, JSON and networking.
You can test out API endpoints using any OpenAPI client - for example [Yaak](https://yaak.app/).
You can test out API endpoints using any OpenAPI client - for example [Postman](https://www.postman.com/), [Insomnia](https://insomnia.rest/) or [Hoppscotch](https://hoppscotch.io/).
## Opening the API specification
The specification is implemented according to the OpenAPI standard.
To access the documentation, you'll need to spin up an instance of Homarr.
Navigate to `Management` > `Tools` > `API`.
Navigate to ``Management`` > ``Tools`` > ``API``.
You can copy the JSON url into your OpenAPI client or try out the endpoints directly from your browser.
Alternatively, you can [open the full interactive API reference](/api-reference) directly in your browser.
## Authentication
Many endpoints will require you to authenticate. At the top of the API reference, you'll find a button to authenticate yourself with an API key.
You can obtain API keys from the "Authentication" tab at the top of the page.
API keys will not expire and can be used indefinitely.
### API Key Format
API keys are provided in the format `<id>.<token>`. When making requests, include the key in the `ApiKey` header:
```
@@ -39,35 +36,49 @@ ApiKey: abc123def456.xyz789tokenvalue...
```
### Supported Endpoints
API key authentication works with both endpoint types:
| Endpoint Type | Path Pattern | API Key Support |
| ------------- | --------------- | --------------- |
| OpenAPI | `/api/...` | ✅ Supported |
| tRPC | `/api/trpc/...` | ✅ Supported |
| Endpoint Type | Path Pattern | API Key Support |
|--------------|--------------|-----------------|
| OpenAPI | `/api/...` | ✅ Supported |
| tRPC | `/api/trpc/...` | ✅ Supported |
If no API key is provided, Homarr falls back to session cookie authentication (for browser-based access).
### Usage Examples
**OpenAPI endpoint:**
```bash
curl -H 'ApiKey: <id>.<token>' http://localhost:7575/api/health/live
```
**tRPC endpoint:**
```bash
curl -H 'ApiKey: <id>.<token>' http://localhost:7575/api/trpc/app.all
```
### Use Cases
API key authentication enables programmatic access for:
- Automation scripts and CI/CD pipelines
- Third-party integrations
- Custom dashboards and monitoring tools
- Container orchestration systems that need to sync app definitions
- Container orchestration systems that need to sync app definitions
## Specification
:::info
This section is automatically being updated hourly.
Last update:
``TEST`` <!-- OPEN_API_SPEC_LAST_UPDATED -->
:::
<ApiReferenceReact
configuration={{
url: "/api/open-api-schema.json",
theme: "kepler",
hideModels: false,
hideDownloadButton: false,
}}
/>
-76
View File
@@ -1,76 +0,0 @@
---
tags:
- Backup
- SQLite
- Management
- Data
---
# Backup & Restore
Homarr provides a built-in backup and restore system for **SQLite** instances. You can export your entire database - including apps, integrations, boards, users, and encryption keys - as a ZIP archive, and restore it on the same or a different Homarr instance.
:::warning
Backup and restore is only available when using the **SQLite** database driver. MySQL and PostgreSQL instances do not show this page.
:::
## Accessing the Backup Page
Navigate to `Management` → `Tools` → `Backup`. You must have **admin** permissions to access this page.
## Exporting a Backup
Click the **Export** button in the export card. You'll be prompted to type `I understand` to confirm. Once confirmed, a ZIP file downloads containing:
| File | Description |
| -------------------- | ------------------------------------------------------------------- |
| `db.sqlite` | Full SQLite database with all entities |
| `metadata.json` | Backup metadata: Homarr version, creation timestamp, encryption key |
The backup includes everything: apps, integrations, secrets, boards, users, groups, permissions, search engines, custom widgets, and server settings. (It just backs-up the whole database)
## Restoring a Backup
### 1. Upload
Drag and drop or select a `.zip` backup file. The import card validates that the file contains a valid `db.sqlite` and metadata.
### 2. Preview
Before restoring, Homarr uses **sql.js WASM** to analyze the backup directly in your browser. You'll see:
- **Entity counts** - how many apps, integrations, boards, users, etc. are in the backup
- **Board names** - which boards will be restored
- **Migration status** - Drizzle migrations are applied in-browser with a visual timeline showing each migration step
### 3. Confirm
Type `I understand` to acknowledge that restoring will replace your current database. This cannot be undone.
### 4. Restore
The backup ZIP is sent to the server, validated, and applied. After a successful restore:
- **The Next.js server restarts automatically** to reload the new database state
- Nginx and Redis (if running in Docker) stay up - only the Next.js process restarts
- The page reloads once the server is ready
## Cross-Instance Restore
You can restore a backup from one Homarr instance onto another. Homarr handles:
- **Encryption key re-encryption** - If the target instance has a different `SECRET_ENCRYPTION_KEY`, integration secrets are decrypted with the old key and re-encrypted with the new one during import
- **Version compatibility** - Backups include the Homarr version they were created with. Backward compatibility is maintained for older backup formats
## Backup During Onboarding
When setting up a new SQLite Homarr instance, the onboarding flow includes a **Restore from SQLite backup** step. You can upload a backup ZIP directly during onboarding to skip manual setup.
## Security
- Export requires admin permissions
- The `I understand` confirmation dialog prevents accidental exports and overwrites
- Backups contain encrypted integration secrets - they cannot be read without the `SECRET_ENCRYPTION_KEY`
- Import only works on SQLite instances - the API routes refuse requests from MySQL/PostgreSQL instances
@@ -1,208 +0,0 @@
---
tags:
- Custom Widgets
- API
- Widgets
- JSX
---
# Custom Widgets
:::warning
Custom widgets are a new feature. Share feedback, ask questions, and discuss ideas in our [GitHub Discussions](https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets).
:::
Custom widgets allow you to fetch data from any HTTP API and display it on your Homarr dashboard. You define the API endpoint, authentication, and how the response should be rendered - no code compilation or server restart required.
## Creating a Custom Widget
To create a custom widget, navigate to the **Custom Widgets** section in the management sidebar and click the **Add** button. Fill in the configuration:
- **Name** - Display name shown in the widget picker and on the dashboard
- **Description** - Optional description shown in tooltips and management UI
- **Icon URL** - Optional URL to an icon image for the widget
- **URL** - The API endpoint to fetch data from (must include protocol, e.g. `https://api.example.com/stats`)
- **Authentication** - How the API request is authenticated:
- `none` - Public APIs with no authentication
- `bearer` - Bearer token authentication
- `basic` - HTTP Basic auth (username + password)
- `apiKeyHeader` - API key sent as a custom HTTP header
- `apiKeyQuery` - API key sent as a query parameter
- **Header Name** - Custom header or query parameter name for API key auth (e.g. `X-API-Key`)
- **HTTP Method** - `GET`, `POST`, `PUT`, `PATCH`, or `DELETE`
- **Request Body** - JSON string body for `POST`/`PUT`/`PATCH` requests
- **Display Type** - How the API response is rendered (see below)
## Display Types
| Display Type | Description | Best For |
| -------------------- | ---------------------------------------------------- | ---------------------------------------- |
| **Single Value** | Extracts a single value with a label and unit | Metrics: temperature, uptime, user count |
| **Key Value** | Multiple labeled key-value pairs with optional units | System stats: CPU, memory, disk |
| **Table** | API array data rendered as a sortable table | Lists of items with columns |
| **Stat Grid** | Labeled values in colored stat cards | Counts: movies, series, albums |
| **Progress Bars** | Values as progress bars with percentages | Usage: disk, quota, capacity |
| **Status Indicator** | Green/red status dots for health checks | Service monitoring |
| **Count Grid** | Simple counts in a grid (no colors) | Library counts |
| **Raw** | Pretty-prints the raw JSON response | Debugging |
| **Action Button** | Clickable button executing the API request | Triggers: restart, clear cache |
| **Custom JSX** | Custom JSX template with Mantine v9 components | Full layout control |
### AI-Powered JSX Generation
When editing or creating a custom widget with the **Custom JSX** display type, you'll find a **Copy Prompt** button. Clicking it copies a pre-formatted prompt describing your widget's name, API endpoint, and available Mantine components. Paste this into your preferred AI assistant (ChatGPT, Claude, Copilot, etc.) to generate a JSX template tailored to your data.
<details>
<summary>Example: Pokédex widget using Custom JSX</summary>
```json
{
"$schema": "homarr-custom-widget-v2",
"name": "Pokédex",
"description": "Browseable Pokémon list",
"iconUrl": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png",
"url": "https://pokeapi.co/api/v2/pokemon?limit=75",
"authType": "none",
"method": "GET",
"displayType": "customJsx",
"displayConfig": {
"type": "customJsx",
"template": "<Stack gap=\"md\" p=\"xs\">\n <Card withBorder radius=\"xl\" p=\"md\" shadow=\"md\">\n <Group justify=\"space-between\">\n <Title order={3}>Pokédex</Title>\n </Group>\n </Card>\n <PaginatedList pageSize={12}>\n <Grid gutter=\"sm\">\n {data.results.map((pokemon, i) =>\n <Grid.Col span={1.5}>\n <Anchor href={pokemon.url} target=\"_blank\" underline=\"never\">\n <Card withBorder radius=\"xl\" p=\"xs\" shadow=\"md\">\n <Stack gap=\"sm\" align=\"center\">\n <Group justify=\"space-between\" style={{ width: \"100%\" }}>\n <Badge size=\"sm\" color=\"red\">#{i + 1}</Badge>\n <Text fw={800} tt=\"capitalize\">{pokemon.name}</Text>\n </Group>\n <Avatar\n src={\"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/\" + (i + 1) + \".png\"}\n size={92}\n radius=\"xl\"\n />\n </Stack>\n </Card>\n </Anchor>\n </Grid.Col>\n )}\n </Grid>\n </PaginatedList>\n</Stack>"
}
}
```
</details>
## Importing and Exporting
Custom widgets can be exported and imported as JSON files. To export, select the widget in the list and click the export button. To import, click the import button and paste the JSON. Secrets (API keys, passwords) are never included in exports and must be configured after import.
The import format uses `"$schema": "homarr-custom-widget-v2"` to identify the widget version.
## Available Mantine Components for Custom JSX
When using the `customJsx` display type, your template has access to a whitelist of [Mantine v9](https://mantine.dev) components. Unrecognized components, event handlers, and dangerous tags are stripped for security.
### Layout
| Component | Doc |
| ------------------- | --------------------------------------------- |
| `Stack` | [docs](https://mantine.dev/core/stack) |
| `Group` | [docs](https://mantine.dev/core/group) |
| `Flex` | [docs](https://mantine.dev/core/flex) |
| `Grid` / `Grid.Col` | [docs](https://mantine.dev/core/grid) |
| `SimpleGrid` | [docs](https://mantine.dev/core/simple-grid) |
| `Center` | [docs](https://mantine.dev/core/center) |
| `Container` | [docs](https://mantine.dev/core/container) |
| `Space` | [docs](https://mantine.dev/core/space) |
| `AspectRatio` | [docs](https://mantine.dev/core/aspect-ratio) |
### Typography & Content
| Component | Doc |
| ----------------- | ------------------------------------------------- |
| `Text` | [docs](https://mantine.dev/core/text) |
| `Title` | [docs](https://mantine.dev/core/title) |
| `Code` | [docs](https://mantine.dev/core/code) |
| `Highlight` | [docs](https://mantine.dev/core/highlight) |
| `Mark` | [docs](https://mantine.dev/core/mark) |
| `Kbd` | [docs](https://mantine.dev/core/kbd) |
| `Blockquote` | [docs](https://mantine.dev/core/blockquote) |
| `Anchor` | [docs](https://mantine.dev/core/anchor) |
| `NumberFormatter` | [docs](https://mantine.dev/core/number-formatter) |
### Data Display
| Component | Doc |
| ------------------------- | ------------------------------------------------- |
| `Badge` | [docs](https://mantine.dev/core/badge) |
| `Card` / `Card.Section` | [docs](https://mantine.dev/core/card) |
| `Paper` | [docs](https://mantine.dev/core/paper) |
| `Alert` | [docs](https://mantine.dev/core/alert) |
| `ThemeIcon` | [docs](https://mantine.dev/core/theme-icon) |
| `ColorSwatch` | [docs](https://mantine.dev/core/color-swatch) |
| `Avatar` / `Avatar.Group` | [docs](https://mantine.dev/core/avatar) |
| `Image` | [docs](https://mantine.dev/core/image) |
| `BackgroundImage` | [docs](https://mantine.dev/core/background-image) |
| `Indicator` | [docs](https://mantine.dev/core/indicator) |
| `Pill` | [docs](https://mantine.dev/core/pill) |
| `Spoiler` | [docs](https://mantine.dev/core/spoiler) |
### Tables
| Component | Doc |
| --------------------------------------- | -------------------------------------- |
| `Table` / `Table.Thead` / `Table.Tbody` | [docs](https://mantine.dev/core/table) |
| `Table.Tr` / `Table.Th` / `Table.Td` | [docs](https://mantine.dev/core/table) |
### Lists
| Component | Doc |
| ---------------------------- | ----------------------------------------- |
| `List` / `List.Item` | [docs](https://mantine.dev/core/list) |
| `Timeline` / `Timeline.Item` | [docs](https://mantine.dev/core/timeline) |
### Feedback & Loading
| Component | Doc |
| ------------------------------- | ---------------------------------------------- |
| `Progress` / `Progress.Section` | [docs](https://mantine.dev/core/progress) |
| `RingProgress` | [docs](https://mantine.dev/core/ring-progress) |
| `Skeleton` | [docs](https://mantine.dev/core/skeleton) |
| `Loader` | [docs](https://mantine.dev/core/loader) |
### Navigation & Overlay
| Component | Doc |
| ------------------------------------------------------------------------ | -------------------------------------------- |
| `TabsContainer` / `TabPanel` | Custom interactive component |
| `Accordion` / `Accordion.Item` / `Accordion.Control` / `Accordion.Panel` | [docs](https://mantine.dev/core/accordion) |
| `ScrollArea` | [docs](https://mantine.dev/core/scroll-area) |
| `Tooltip` | [docs](https://mantine.dev/core/tooltip) |
### Charts
| Component | Doc |
| ---------------- | --------------------------------------------------- |
| `AreaChart` | [docs](https://mantine.dev/charts/area-chart) |
| `BarChart` | [docs](https://mantine.dev/charts/bar-chart) |
| `LineChart` | [docs](https://mantine.dev/charts/line-chart) |
| `DonutChart` | [docs](https://mantine.dev/charts/donut-chart) |
| `PieChart` | [docs](https://mantine.dev/charts/pie-chart) |
| `RadarChart` | [docs](https://mantine.dev/charts/radar-chart) |
| `RadialBarChart` | [docs](https://mantine.dev/charts/radial-bar-chart) |
| `Sparkline` | [docs](https://mantine.dev/charts/sparkline) |
### Interactive Components
| Component | Description |
| --------------- | ---------------------------------------- |
| `PaginatedList` | Client-side pagination wrapper |
| `Collapsible` | Expandable/collapsible section |
| `StatBar` | Horizontal stat bar |
| `TypeBadge` | Type-styled badge |
| `Divider` | [docs](https://mantine.dev/core/divider) |
## Template Bindings
Your template receives:
- `data` - The sanitized API response object. Access fields via `data.fieldName` or `data.results[0].name`.
- `String(v)` - Type-safe string conversion
- `Number(v)` - Type-safe number conversion
- `Boolean(v)` - Type-safe boolean conversion
- `Math` - Safe subset: `round`, `floor`, `ceil`, `abs`, `min`, `max`, `pow`, `sqrt`, `PI`
- `JSON.stringify(v)` - JSON serialization
- `Array.isArray(v)` - Array type check
- `Object.keys(v)`, `Object.values(v)`, `Object.entries(v)` - Object utilities
## Security Constraints
- Event handlers (`onClick`, `onChange`, etc.) are stripped
- Dangerous HTML attributes (`dangerouslySetInnerHTML`) are stripped
- `script`, `iframe`, `object`, `embed`, `form`, `style`, `link`, `meta`, `base` tags are forbidden
- Template strings containing `eval`, `Function`, `import`, `require`, `globalThis`, `window`, `document`, `fetch` are rejected
- Anchor `href` attributes are validated to only allow `https://`, relative (`/`), and hash (`#`) URLs
-166
View File
@@ -1,166 +0,0 @@
---
tags:
- MCP
- API
- AI
- Automation
---
# Model Context Protocol (MCP)
Homarr exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp/mcp`, enabling AI assistants like Claude, Cursor, VS Code Copilot, and Claude Code to interact with your Homarr instance using natural language.
With MCP, you can ask your AI assistant to manage apps, integrations, users, boards, Docker containers, media servers, DNS settings, and more - all through Homarr's existing tRPC API, now exposed as 50+ AI-callable tools.
## Getting Started
The MCP endpoint lives under the **API** page at `Management` → `Tools` → `API` → **MCP** tab.
![MCP Instructions Page](./api/img/mcp-instructions.png)
### 1. Create an API Key
Navigate to the **Authentication** tab in the API page and create an API key. API keys follow the format `<id>.<token>`.
You can also create one directly from the MCP tab if you don't have any keys yet.
### 2. Choose a Transport
| Transport | Best For | Requirements |
| ------------------- | ------------------------------------------------------------ | ------------------------------ |
| **Streamable HTTP** | Cursor, Claude Desktop, VS Code Copilot, Claude Code | Direct HTTP access to Homarr |
| **STDIO** | Firewall-restricted environments, offline-compatible clients | `npx` and `mcp-remote` package |
**Streamable HTTP** is recommended for most setups. It works with all major AI clients.
### 3. Configure Your Client
Add the following to your client's MCP configuration file:
**Streamable HTTP** (recommended):
```json
{
"mcpServers": {
"homarr": {
"url": "https://your-homarr-instance.com/api/mcp/mcp",
"headers": {
"ApiKey": "<your-api-key>"
}
}
}
}
```
Where to place this config:
| Client | Config File |
| --------------- | ----------------------------------------------- |
| Cursor | `.cursor/mcp.json` |
| Claude Desktop | `claude_desktop_config.json` |
| Claude Code | `.claude/mcp.json` (or `claude mcp add homarr`) |
| VS Code Copilot | `.vscode/mcp.json` |
:::warning
Replace `<your-api-key>` with your actual API key. Never commit API keys to version control.
:::
**STDIO** (alternative):
```json
{
"mcpServers": {
"homarr": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://your-homarr-instance.com/api/mcp/mcp",
"--header",
"ApiKey:<your-api-key>"
]
}
}
}
```
This option tunnels MCP communication through STDIO using the `mcp-remote` package. Useful when the AI client cannot make HTTP requests directly.
## Authentication
MCP supports two authentication methods:
| Method | Description | Best For |
| -------------------- | ----------------------------------------------------- | ---------------------------------------------------------------- |
| **API Key** | Include `ApiKey: <id>.<token>` header in all requests | Scripts, CI/CD, direct client configs |
| **OAuth 2.1 + PKCE** | Full OAuth flow with dynamic client registration | MCP clients with OAuth support (e.g. `opencode mcp auth homarr`) |
API key authentication is simpler and recommended for most setups. The API key inherits the permissions of the user who created it. Tools that require higher privileges will return permission errors if the key lacks access.
## Testing the Connection
Verify your MCP endpoint is working:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "ApiKey: <your-api-key>" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' \
https://your-homarr-instance.com/api/mcp/mcp
```
A successful response includes the server capabilities and available tools. Use `tools/list` to see all available tools:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "ApiKey: <your-api-key>" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
https://your-homarr-instance.com/api/mcp/mcp
```
## Available Tools
Homarr exposes 50+ tools organized by namespace. Each tool is either a **query** (GET, read-only) or **mutation** (POST, write action).
| Namespace | Example Tools | Type |
| ------------------ | -------------------------------------------------------- | ---------- |
| `app` | List all apps, get app by ID, create/update/delete app | GET + POST |
| `apiKeys` | List API keys | GET |
| `beszel` | Systems status, alerts, historical system metrics | GET |
| `board` | List boards, get board by ID, create/update/delete board | GET + POST |
| `docker` | List containers, inspect container, start/stop/restart | GET + POST |
| `icon` | Search icons | GET |
| `info` | Server health, version info | GET |
| `integration` | List integrations, test connection, delete integration | GET + POST |
| `invite` | List invitations, delete invite | GET + POST |
| `calendar` | Calendar events from media servers | GET |
| `dnsHole` | DNS hole summary, toggle blocking | GET + POST |
| `downloads` | Active downloads, pause/resume/delete | GET + POST |
| `healthMonitoring` | Resource usage stats | GET |
| `mediaRequests` | Approve/decline media requests | GET + POST |
| `mediaServer` | Media server sessions and stats | GET |
| `smartHome` | Execute automations, device states | GET + POST |
The tools table on the MCP tab is dynamically generated - it always reflects the latest available procedures from your Homarr instance.
## Security
- API keys are required for authentication - unauthenticated requests are rejected
- Tools inherit the permissions of the API key's owner
- OAuth endpoints are rate-limited to prevent abuse
- Tool execution errors are contained - a failing tool call does not crash the MCP server
- All communication uses HTTPS (configure HTTP → HTTPS redirects on your reverse proxy)
## Use Cases
- **"Show me all apps on my Homarr dashboard"** - AI lists all apps with their names and URLs
- **"Restart the Plex Docker container"** - AI finds and restarts the container via Docker integration
- **"What's downloading right now?"** - AI fetches active download status across all download integrations
- **"Create a new app pointing to my Grafana instance"** - AI creates an app with the correct icon and URL
- **"How many users are on my Homarr instance?"** - AI lists all users (admin only)
- **"Toggle Pi-hole blocking for 5 minutes"** - AI interacts with DNS controls
- **"How are my servers doing?"** - AI fetches Beszel system stats including CPU, memory, disk, and alerts
@@ -42,7 +42,7 @@ Homarr will replace the ``%s`` with the term that the user entered.
## Media request search
Jellyseerr, Overseerr, and Seerr integrations have a dedicated **Spotlight mode** for searching and requesting media.
This mode does not require creating a search engine entry - it is available automatically when you have a compatible integration configured.
This mode does not require creating a search engine entry — it is available automatically when you have a compatible integration configured.
You can access it from:
- The **Modes list** in Spotlight (opened with `CTRL + K`)
@@ -51,10 +51,10 @@ You can access it from:
### Availability indicators
Search results display colored status badges showing the media's current state in your integration:
- **Available** - fully available in your media library
- **Partial** - some content (e.g. seasons) is available
- **Processing** - currently being downloaded or processed
- **Requested** - requested but not yet available
- **Available** — fully available in your media library
- **Partial** — some content (e.g. seasons) is available
- **Processing** — currently being downloaded or processed
- **Requested** — requested but not yet available
### Requesting TV show seasons
Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel Alerts'
description: 'View Beszel alert configurations and history with triggered/ok status indicators.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelAlertsWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelAlerts from './img/beszel-alerts.png';
<WidgetHeader
widget={beszelAlertsWidget}
categories={['System Monitoring', 'Alerts']}
/>
This widget displays your Beszel alert configurations separated by status (triggered and ok). It also shows alert history with timestamps. Alerts are updated in real time via WebSocket subscriptions.
### Screenshots
<img src={beszelAlerts} alt="Beszel Alerts widget showing triggered and ok alerts with history" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays alert configurations and trigger history"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelAlertsWidget.configuration} />
@@ -1,25 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconBell } from "@tabler/icons-react";
export const beszelAlertsWidget: WidgetDefinition = {
icon: IconBell,
name: "Beszel Alerts",
description: "View Beszel alert configurations and history with triggered/ok status indicators.",
path: "../../widgets/beszel-alerts",
configuration: {
items: [
{
name: "Show history",
description: "Displays the alert trigger history",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Max history items",
description: "Maximum number of history entries to display (1-100)",
values: "Number (1-100)",
defaultValue: "10",
},
],
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel Systems (Grid)'
description: 'Card grid view of all Beszel-monitored systems with real-time metrics.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemGridWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelSystemGrid from './img/beszel-system-grid.png';
<WidgetHeader
widget={beszelSystemGridWidget}
categories={['System Monitoring']}
/>
This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Cards adapt their layout based on available space.
### Screenshots
<img src={beszelSystemGrid} alt="Beszel System Grid widget showing system cards with metrics" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays all monitored systems as a card grid"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemGridWidget.configuration} />
@@ -1,88 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconLayoutGrid } from "@tabler/icons-react";
export const beszelSystemGridWidget: WidgetDefinition = {
icon: IconLayoutGrid,
name: "Beszel Systems (Grid)",
description: "Card grid view of all Beszel-monitored systems with real-time metrics.",
path: "../../widgets/beszel-system-grid",
configuration: {
items: [
{
name: "Status filter",
description: "Filter systems by their current status",
values: {
type: "select",
options: ["All Systems", "Up", "Down", "Paused"],
},
defaultValue: "All Systems",
},
{
name: "Show CPU",
description: "Displays CPU usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays memory usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays disk usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show GPU",
description: "Displays GPU usage on each system card",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show load average",
description: "Displays system load average",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays network throughput",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show temperature",
description: "Displays system temperature",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show battery",
description: "Displays battery level (if available)",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show services",
description: "Displays running Docker services count",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show uptime",
description: "Displays system uptime",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show agent version",
description: "Displays the Beszel agent version",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 KiB

@@ -1,39 +0,0 @@
---
title: 'Beszel System Stats'
description: 'Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemStatsWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
import beszelSystemStats from './img/beszel-system-stats.png';
<WidgetHeader
widget={beszelSystemStatsWidget}
categories={['System Monitoring', 'Charts']}
/>
This widget displays time-series charts for a selected Beszel system, including CPU, memory, disk, network, and Docker container metrics. Data is cached and refreshed periodically. You can select a time period ranging from 1 hour to 30 days.
### Screenshots
<img src={beszelSystemStats} alt="Beszel System Stats widget showing CPU, memory, disk and network charts" />
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays time-series charts for a selected system"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemStatsWidget.configuration} />
@@ -1,77 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconChartAreaLine } from "@tabler/icons-react";
export const beszelSystemStatsWidget: WidgetDefinition = {
icon: IconChartAreaLine,
name: "Beszel System Stats",
description: "Time-series charts for CPU, memory, disk, network, and Docker container metrics from Beszel.",
path: "../../widgets/beszel-system-stats",
configuration: {
items: [
{
name: "System",
description: "The Beszel system to display stats for. Searchable select populated from the integration.",
values: { type: "string" },
defaultValue: "First available system",
},
{
name: "Time period",
description:
"The time range for chart data. '1 Minute' enables live streaming via PocketBase SSE (requires Beszel agent >= 0.13.0)",
values: {
type: "select",
options: ["1 Minute", "1 Hour", "12 Hours", "24 Hours", "1 Week", "30 Days"],
},
defaultValue: "1 Hour",
},
{
name: "Show CPU",
description: "Displays the CPU usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays the memory usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays the disk usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk I/O",
description: "Displays the disk I/O chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays the network chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker CPU",
description: "Displays Docker container CPU usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker memory",
description: "Displays Docker container memory usage chart",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show Docker network",
description: "Displays Docker container network chart",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
@@ -1,34 +0,0 @@
---
title: 'Beszel Systems (Table)'
description: 'Table view of all Beszel-monitored systems with sortable columns and status indicators.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { beszelSystemTableWidget } from '.';
import { beszelIntegration } from '@site/docs/integrations/beszel';
<WidgetHeader
widget={beszelSystemTableWidget}
categories={['System Monitoring']}
/>
This widget displays all your Beszel-monitored systems in a sortable data table. Each row shows system metrics like CPU, memory, disk, network, temperature, and more. Columns can be toggled on or off, and the table supports sorting by any column.
### Supported Integrations
<WidgetIntegrations items={[{
integration: beszelIntegration,
note: "Displays all monitored systems in a sortable table"
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={beszelSystemTableWidget.configuration} />
@@ -1,106 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconTable } from "@tabler/icons-react";
export const beszelSystemTableWidget: WidgetDefinition = {
icon: IconTable,
name: "Beszel Systems (Table)",
description: "Table view of all Beszel-monitored systems with sortable columns and status indicators.",
path: "../../widgets/beszel-system-table",
configuration: {
items: [
{
name: "Status filter",
description: "Filter systems by their current status",
values: {
type: "select",
options: ["All Systems", "Up", "Down", "Paused"],
},
defaultValue: "All Systems",
},
{
name: "Sort by",
description: "Default column to sort the table by",
values: {
type: "select",
options: ["System", "CPU", "Memory", "Disk", "GPU", "Load Avg", "Net", "Temp", "Services", "Uptime", "Agent"],
},
defaultValue: "System",
},
{
name: "Sort direction",
description: "Default sort direction",
values: {
type: "select",
options: ["Ascending", "Descending"],
},
defaultValue: "Ascending",
},
{
name: "Show CPU",
description: "Displays the CPU column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show memory",
description: "Displays the memory column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show disk",
description: "Displays the disk column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show GPU",
description: "Displays the GPU column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show load average",
description: "Displays the load average column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show network",
description: "Displays the network column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show temperature",
description: "Displays the temperature column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show battery",
description: "Displays the battery column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show services",
description: "Displays the services column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show uptime",
description: "Displays the uptime column",
values: { type: "boolean" },
defaultValue: "yes",
},
{
name: "Show agent version",
description: "Displays the agent version column",
values: { type: "boolean" },
defaultValue: "yes",
},
],
},
};
+1 -3
View File
@@ -63,9 +63,7 @@ import withoutIntegrations from './img/without-integrations.png';
### Interactions
- Hover over a marked date to preview its events in a popover.
- Click on a marked date to pin the popover open (it will stay visible even after moving the mouse away).
- Click the date again or click outside to close the pinned popover.
- View calendar events by clicking on the marked dates.
### Adding the widget
@@ -1,29 +0,0 @@
---
title: 'Custom API'
description: 'Displays data from a custom API endpoint using configurable display types and optional JSX templates.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { customApiWidget } from '.';
<WidgetHeader
widget={customApiWidget}
categories={['Custom', 'API']}
/>
This widget fetches data from a custom API endpoint defined in [Custom Widgets management](/docs/management/custom-widgets) and renders it using configurable display types.
Custom API widgets are created and configured through the admin management page. Each widget definition specifies the API endpoint, authentication, display type, and refresh interval.
See the [Custom Widgets documentation](/docs/management/custom-widgets) for full details on creating and configuring custom widgets.
### Adding the widget
<AddingWidget />
:::note
Custom API widgets must first be created in **Management → Custom Widgets** before they appear in the widget picker.
:::
@@ -1,9 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconApi } from "@tabler/icons-react";
export const customApiWidget: WidgetDefinition = {
icon: IconApi,
name: "Custom API",
description: "Displays data from a custom API endpoint using configurable display types and optional JSX templates.",
path: "../../widgets/custom-api",
};
@@ -1,17 +1,16 @@
import { WidgetDefinition } from "@site/src/types";
import { IconDeviceGamepad } from "@tabler/icons-react";
import { integrations } from "../dns-hole-summary";
export const dnsHoleControlsWidget: WidgetDefinition = {
icon: IconDeviceGamepad,
name: "DNS Hole Controls",
description: `Control the blocking feature of your ${integrations} from your dashboard`,
description: "Control PiHole or AdGuard from your dashboard",
path: "../../widgets/dns-hole-controls",
configuration: {
items: [
{
name: "Show Toggle All Buttons",
description: "Show or not the 2 buttons that may be redundant when having only one DNS Hole.",
description: "Show or not the 2 buttons that may be redundant when having only one DNS-hole.",
values: { type: "boolean" },
defaultValue: "yes",
},
@@ -1,12 +1,10 @@
import { WidgetDefinition } from "@site/src/types";
import { IconAd } from "@tabler/icons-react";
export const integrations = "Pi-hole, AdGuard Home or Technitium DNS";
export const dnsHoleSummaryWidget: WidgetDefinition = {
icon: IconAd,
name: "DNS Hole Summary",
description: `Displays the summary of your ${integrations} blocks and queries statistics`,
description: "Displays the summary of your DNS Hole",
path: "../../widgets/dns-hole-summary",
configuration: {
items: [
@@ -21,7 +19,7 @@ export const dnsHoleSummaryWidget: WidgetDefinition = {
},
{
name: "Use Pi-Hole colors",
description: `Color the tiles using the background color, that is used by default in ${integrations}.`,
description: "Color the tiles using the background color, that is used by default in PiHole.",
values: { type: "boolean" },
defaultValue: "yes",
},
@@ -41,10 +41,10 @@ import dockerTable from './img/table.png';
### Interactions
Each container row has an action menu (accessible via the three-dot icon) with the following options:
- **View logs** - opens a dedicated terminal page streaming the container's logs
- **Start** / **Stop** / **Restart** - manage the container's state
- **Remove** - remove the container
- **Add to Homarr** - create a bookmark app from the container
- **View logs** — opens a dedicated terminal page streaming the container's logs
- **Start** / **Stop** / **Restart** — manage the container's state
- **Remove** — remove the container
- **Add to Homarr** — create a bookmark app from the container
### Adding the widget
@@ -9,8 +9,9 @@ export const immichAlbumCarouselWidget: WidgetDefinition = {
configuration: {
items: [
{
name: "Album",
description: "Select the Immich album to show in the slideshow.",
name: "Album ID",
description:
"This is a unique ID which refers to your Immich album. You can find this id by opening an album and copying the unique string from the URL, for example 'a21aca09-9f05-47dc-b1a1-7b53c547226e'",
values: { type: "string" },
defaultValue: "",
},
@@ -24,13 +25,7 @@ export const immichAlbumCarouselWidget: WidgetDefinition = {
name: "Show photo info",
description: "Display date of the picture and the current index at the bottom of the slideshow",
values: { type: "boolean" },
defaultValue: "no",
},
{
name: "Randomize photos",
description: "Show photos in a random order instead of always starting with the same album order.",
values: { type: "boolean" },
defaultValue: "no",
defaultValue: "yes",
},
],
},
@@ -13,8 +13,7 @@ import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import { notificationsWidget } from '.';
import { ntfyIntegration } from '@site/docs/integrations/ntfy';
import { nextcloudIntegration } from '@site/docs/integrations/nextcloud';
import { gotifyIntegration } from '@site/docs/integrations/gotify';
import { nextcloudIntegration } from '@site/docs/integrations/nextcloud'
<WidgetHeader
widget={notificationsWidget}
@@ -34,8 +33,7 @@ import notificationsScreenshot from './img/notifications.png';
<WidgetIntegrations
items={[
{ integration: ntfyIntegration },
{ integration: nextcloudIntegration },
{ integration: gotifyIntegration }
{ integration: nextcloudIntegration }
]}
/>
+45 -19
View File
@@ -1,44 +1,69 @@
---
title: "Releases"
description: "Displays a list of the current version of the given repositories with the given version regex."
title: 'Releases'
description: 'Displays a list of the current version of the given repositories with the given version regex.'
hide_title: true
---
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header";
import { releasesWidget } from ".";
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetHeader } from '@site/src/components/widgets/header';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { releasesWidget } from '.';
import { githubIntegration } from '@site/docs/integrations/github';
import { gitlabIntegration } from '@site/docs/integrations/gitlab';
import { dockerHubIntegration } from '@site/docs/integrations/docker-hub';
import { npmIntegration } from '@site/docs/integrations/npm';
import { codebergIntegration } from '@site/docs/integrations/codeberg';
<WidgetHeader widget={releasesWidget} categories={["Releases", "Software"]} />
<WidgetHeader
widget={releasesWidget}
categories={['Releases', 'Software']}
/>
The releases widget enables you to track a list of repositories and see their respective latest release.
It also supports importing releases from your local docker containers.
The releases widget enables you to track a list of repositories and see their respective latest release.
It also supports importing releases from your local docker containers.
### Screenshots
import collapsibleCard from "./img/collapsible.png";
import fullCard from "./img/full.png";
import noDetailsCard from "./img/no-details.png";
import collapsibleCard from './img/collapsible.png';
import fullCard from './img/full.png';
import noDetailsCard from './img/no-details.png';
import { quayIntegration } from '@site/docs/integrations/quay';
import { gitHubContainerRegistryIntegration } from '@site/docs/integrations/github-containerregistry';
import { linuxServerIoIntegration } from '@site/docs/integrations/linux-server-io';
<div className="flex gap-1">
<div className='flex gap-1'>
<img src={fullCard} alt="releases full" width={128 * 3} />
<img src={collapsibleCard} alt="releases collapsible" width={128 * 3} />
<img src={noDetailsCard} alt="releases no details" width={128 * 3} />
</div>
### Supported Providers
### Supported Integrations
GitHub, Docker Hub, GitLab, NPM, Codeberg, GitHub Container Registry, Quay, and LinuxServer.io are configured per repository in the widget.
For GitHub Container Registry, use `owner/package` identifiers such as `homarr-labs/homarr`.
Full image references such as `ghcr.io/homarr-labs/homarr:latest` are also accepted.
<WidgetIntegrations items={[{
integration: githubIntegration,
}, {
integration: gitlabIntegration,
}, {
integration: dockerHubIntegration,
}, {
integration: npmIntegration,
}, {
integration: codebergIntegration,
}, {
integration: gitHubContainerRegistryIntegration,
note: "Use if release is only available in ghcr.io"
}, {
integration: quayIntegration
}, {
integration: linuxServerIoIntegration
}]} />
### Adding the widget
<AddingWidget />
### Configuration
<WidgetConfig configuration={releasesWidget.configuration} />
:::note
@@ -49,3 +74,4 @@ Setting the Prefix to `v`, precision to `2` and the Suffix to `-alpha`, will mat
Setting the Prefix to `v`, precision to `3`, will match `v5.2.0` but not `v1.0.2-alpha` or `Beta v5.2.1`.
:::
+4 -11
View File
@@ -54,23 +54,16 @@ export const releasesWidget: WidgetDefinition = {
items: [
{
name: "Provider",
description: "Provider to fetch the repository or package releases from.",
values: "Select from the available providers.",
description: "Integration to fetch the repository from.",
values: "Select from the available integrations.",
defaultValue: "-",
},
{
name: "Identifier",
description: 'The identifier of the repository or package, e.g. "homarr-labs/homarr".',
values:
'Any valid provider identifier. GHCR also accepts full image refs like "ghcr.io/owner/package:tag".',
description: 'The identifier of the repository, e.g. "homarr-labs/homarr".',
values: "Any valid repository identifier.",
defaultValue: "-",
},
{
name: "Provider URL",
description: "Base URL for the provider.",
values: "Any valid URL.",
defaultValue: "Provider default",
},
{
name: "Name",
description: "Display name of the repository.",
+24 -20
View File
@@ -1,40 +1,44 @@
---
title: "Timetable"
description: "Displays departure times for a station."
title: 'Timetable'
description: 'Displays departure times for a station.'
hide_title: true
---
import { AddingWidget } from "@site/src/components/widgets/adding";
import { WidgetConfig } from "@site/src/components/widgets/configuration";
import { WidgetHeader } from "@site/src/components/widgets/header";
import { timetableWidget } from ".";
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetConfig } from '@site/src/components/widgets/configuration';
import { WidgetHeader } from '@site/src/components/widgets/header';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { timetableWidget } from '.';
import { searchChIntegration } from '@site/docs/integrations/search-ch';
<WidgetHeader widget={timetableWidget} categories={["Timetable"]} />
<WidgetHeader
widget={timetableWidget}
categories={['Timetable']}
/>
The Timetable widget provides real-time departure times for a selected station.
### Screenshots
import trainDepartures from "./img/train-departures.png";
import streetDepartures from "./img/street-departures.png";
import trainDepartures from './img/train-departures.png';
import streetDepartures from './img/street-departures.png';
<div className="flex gap-2">
<img
src={trainDepartures}
alt="timetable-widget-train-departures"
width={128 * 2}
/>
<img
src={streetDepartures}
alt="timetable-widget-bus-and-tram-departures"
width={128 * 2}
/>
<img src={trainDepartures} alt="timetable-widget-train-departures" width={128 * 2} />
<img src={streetDepartures} alt="timetable-widget-bus-and-tram-departures" width={128 * 2} />
</div>
### Adding the widget
<AddingWidget />
### Configuration
### Supported Integrations
<WidgetIntegrations items={[{
integration: searchChIntegration,
note: 'Switzerland only'
}]} />
### Configuration
<WidgetConfig configuration={timetableWidget.configuration} />
@@ -8,12 +8,6 @@ export const timetableWidget: WidgetDefinition = {
path: "../../widgets/timetable",
configuration: {
items: [
{
name: "Search.ch URL",
description: "Base URL for the Search.ch timetable API.",
values: "Any valid URL.",
defaultValue: "https://search.ch",
},
{
name: "Station",
description: "Select the station",
+2 -2
View File
@@ -16,7 +16,7 @@ import { umamiIntegration } from '@site/docs/integrations/umami';
categories={['Analytics']}
/>
The Umami Analytics widget displays key visitor metrics from a website tracked by your [Umami](https://umami.is) instance - pageviews, unique visitors, visits, bounce rate, avg. visit duration, and real-time active visitor count. The widget body can be switched between a visitor/event chart, a multi-event chart, and ranked top pages or referrers lists.
The Umami Analytics widget displays key visitor metrics from a website tracked by your [Umami](https://umami.is) instance — pageviews, unique visitors, visits, bounce rate, avg. visit duration, and real-time active visitor count. The widget body can be switched between a visitor/event chart, a multi-event chart, and ranked top pages or referrers lists.
### Supported Integrations
@@ -37,7 +37,7 @@ The Umami Analytics widget displays key visitor metrics from a website tracked b
| View | Description |
|---|---|
| **Chart** | Bar chart or sparkline of unique visitors over the selected time frame. Optionally overlay a single named event as a second series. |
| **Events chart** | Multi-select one or more named events and chart each as its own colored series - no visitor baseline. Supports bar and sparkline. |
| **Events chart** | Multi-select one or more named events and chart each as its own colored series — no visitor baseline. Supports bar and sparkline. |
| **Top pages** | Ranked list of URLs by pageview count for the selected time frame. |
| **Top referrers** | Ranked list of referrer sources by session count. Direct traffic (no referrer) is labeled "Direct". |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

-40
View File
@@ -1,40 +0,0 @@
---
title: 'VPN'
description: 'Monitor the status of your VPN integrations.'
hide_title: true
---
import { WidgetHeader } from '@site/src/components/widgets/header';
import { AddingWidget } from '@site/src/components/widgets/adding';
import { WidgetIntegrations } from '@site/src/components/widgets/integrations';
import { vpnWidget } from '.';
import { gluetunIntegration } from '@site/docs/integrations/gluetun';
<WidgetHeader
widget={vpnWidget}
categories={['Network', 'Security']}
/>
The VPN widget provides a compact overview of each configured VPN integration. It shows whether the tunnel and DNS
services are up, the currently exposed public IP address, and the VPN provider and protocol being used.
When multiple VPN integrations are attached, the widget renders one card per integration so you can compare their
current status at a glance.
### Screenshots
import singleIntegrationImage from './img/single.png';
<div>
<img src={singleIntegrationImage} alt="VPN widget showing a single integration" width={128 * 3} />
</div>
### Supported Integrations
<WidgetIntegrations items={[{
integration: gluetunIntegration,
}]} />
### Adding the widget
<AddingWidget />
-9
View File
@@ -1,9 +0,0 @@
import { WidgetDefinition } from "@site/src/types";
import { IconShieldLock } from "@tabler/icons-react";
export const vpnWidget: WidgetDefinition = {
icon: IconShieldLock,
name: "VPN",
description: "Monitor the connection status, public IP, and provider details of your VPN integrations.",
path: "../../widgets/vpn",
};
+2 -8
View File
@@ -8,7 +8,6 @@ const config: Config = {
tagline: "A simple yet powerful dashboard for your server.",
url: "https://homarr.dev",
baseUrl: "/",
trailingSlash: true,
favicon: "img/logo.png",
organizationName: "homarr-labs",
projectName: "homarr",
@@ -17,6 +16,7 @@ const config: Config = {
locales: ["en"],
},
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "throw",
onBrokenAnchors: "throw",
onDuplicateRoutes: "throw",
@@ -28,17 +28,11 @@ const config: Config = {
fasterByDefault: true,
mdx1CompatDisabledByDefault: false,
},
faster: {
swcHtmlMinimizer: false,
},
},
markdown: {
mermaid: true,
format: "detect",
hooks: {
onBrokenMarkdownLinks: "throw",
},
},
themes: ["@docusaurus/theme-mermaid"],
@@ -62,7 +56,7 @@ const config: Config = {
sidebarPath: require.resolve("./sidebars.js"),
editUrl: ({ docPath }) => `https://github.com/homarr-labs/homarr/edit/dev/apps/docs/docs/${docPath}`,
remarkPlugins: [a11yEmoji],
exclude: [],
exclude: ["**/custom-widget.mdx"],
showLastUpdateAuthor: false,
showLastUpdateTime: false,
},
+3 -3
View File
@@ -32,19 +32,19 @@
"@scalar/api-reference-react": "catalog:",
"@signalwire/docusaurus-plugin-llms-txt": "^1.2.2",
"@tabler/icons-react": "catalog:",
"@tailwindcss/postcss": "^4.3.1",
"@tailwindcss/postcss": "^4.3.0",
"clsx": "catalog:",
"dayjs": "catalog:",
"docusaurus-plugin-image-zoom": "^3.0.1",
"leader-line-new": "^1.1.9",
"postcss": "^8.5.16",
"postcss": "^8.5.10",
"posthog-docusaurus": "^2.0.5",
"prism-react-renderer": "^2.4.1",
"random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-dom": "catalog:",
"swiper": "^12.2.0",
"tailwindcss": "^4.3.1"
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.10.1",
@@ -32,7 +32,7 @@ export const IntegrationCapabilites = ({ items }: IntegrationCapabilitesProps) =
className="flex gap-6 rounded-xl border border-solid dark:border-[#333] border-[#e5e7eb] p-4 shadow-sm w-full items-center justify-between"
>
<div className="flex gap-6 items-center">
<div className="w-10 h-10 flex justify-center items-center bg-red-500/10 rounded-md">
<div className="w-10 h-10 flex justify-center items-center bg-red-500 bg-opacity-10 rounded-md">
<capability.icon size={24} stroke={1.5} className="stroke-red-500" />
</div>
@@ -1,20 +0,0 @@
"use client";
import type { AnyApiReferenceConfiguration } from "@scalar/types/api-reference";
import { ApiReferenceReact } from "@scalar/api-reference-react";
import { useColorMode } from "@docusaurus/theme-common";
import "@scalar/api-reference-react/style.css";
export function ThemedApiReference({ configuration }: { configuration: AnyApiReferenceConfiguration }) {
const { colorMode } = useColorMode();
return (
<ApiReferenceReact
configuration={{
...configuration,
forceDarkModeState: colorMode === "dark" ? "dark" : "light",
}}
/>
);
}
+1 -1
View File
@@ -12,7 +12,7 @@ export const WidgetHeader = (props: WidgetHeaderProps) => {
title={props.widget.name}
description={props.widget.description}
icon={
<div className="w-12 h-12 flex justify-center items-center bg-red-500/10 rounded-md">
<div className="w-12 h-12 flex justify-center items-center bg-red-500 bg-opacity-10 rounded-md">
<props.widget.icon size={32} stroke={1.5} className="stroke-red-500" />
</div>
}
-29
View File
@@ -1,29 +0,0 @@
import React from "react";
import Layout from "@theme/Layout";
import BrowserOnly from "@docusaurus/BrowserOnly";
export default function ApiReferencePage() {
return (
<Layout title="API Reference" description="Homarr OpenAPI Reference">
<BrowserOnly fallback={<div style={{ height: "100vh" }} />}>
{() => {
const { ThemedApiReference } = require("@site/src/components/themed-api-reference");
return (
<ThemedApiReference
configuration={{
url: "/api/open-api-schema.json",
theme: "kepler",
hideModels: false,
hideDownloadButton: false,
mcp: { disabled: true },
customCss: `
.scalar-sidebar-footer a[href="https://www.scalar.com"] { display: none !important; }
`,
}}
/>
);
}}
</BrowserOnly>
</Layout>
);
}
+35 -28
View File
@@ -1,29 +1,36 @@
{
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
},
"include": [
"**/*.ts",
"**/*.jsx",
"**/*.js",
"**/*.tsx",
"**/*.md",
"**/*.mdx",
],
"exclude": ["node_modules"],
}
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": [
"**/*.ts",
"**/*.jsx",
"**/*.js",
"**/*.tsx",
"**/*.md",
"**/*.mdx"
],
"exclude": [
"node_modules"
]
}
+1 -3
View File
@@ -59,11 +59,10 @@
"@modelcontextprotocol/sdk": "catalog:",
"@scalar/api-reference-react": "catalog:",
"@tabler/icons-react": "catalog:",
"@tanstack/query-async-storage-persister": "catalog:",
"@tanstack/query-persist-client-core": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-query-devtools": "catalog:",
"@tanstack/react-query-next-experimental": "catalog:",
"@tanstack/react-query-persist-client": "catalog:",
"@trpc/client": "catalog:",
"@trpc/next": "catalog:",
"@trpc/react-query": "catalog:",
@@ -90,7 +89,6 @@
"mcp-handler": "catalog:",
"next": "catalog:",
"postcss-preset-mantine": "catalog:",
"posthog-js": "catalog:",
"prismjs": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
@@ -1,29 +1,49 @@
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
import type { AsyncStorage, PersistedQuery } from "@tanstack/query-persist-client-core";
import { experimental_createQueryPersister } from "@tanstack/query-persist-client-core";
import { parse, stringify } from "superjson";
import { fetchApi } from "@homarr/api/client";
import { getActiveQueryCacheBoardId, queryCacheStoragePrefix } from "@homarr/api/query-cache";
import {
getActiveQueryCacheBoardId,
isPersistableWidgetQueryKey,
queryCacheStoragePrefix,
} from "@homarr/api/query-cache";
const queryCacheStorage = {
getItem: (_key: string) => null as string | null,
async setItem(_key: string, value: string) {
async getItem(_key) {
return null;
},
async setItem(key, value) {
const boardId = getActiveQueryCacheBoardId();
if (!boardId) return;
await fetchApi.queryCache.setItem.mutate({ boardId, value }).catch((error) => {
console.warn("[query-cache] persist failed", error);
});
try {
await fetchApi.queryCache.setItem.mutate({ boardId, key, value });
} catch {
// Query persistence is best-effort; failed writes should not affect widget rendering.
}
},
async removeItem(_key: string) {
async removeItem(key) {
const boardId = getActiveQueryCacheBoardId();
if (!boardId) return;
await fetchApi.queryCache.removeItem.mutate({ boardId }).catch((error) => {
console.warn("[query-cache] remove failed", error);
});
try {
await fetchApi.queryCache.removeItem.mutate({ boardId, key });
} catch {
// Query persistence is best-effort; failed removals should not affect widget rendering.
}
},
};
} satisfies AsyncStorage<string>;
export const createWidgetQueryPersister = () =>
createAsyncStoragePersister({
experimental_createQueryPersister<string>({
storage: typeof window === "undefined" ? undefined : queryCacheStorage,
key: queryCacheStoragePrefix,
throttleTime: 2000,
maxAge: Number.POSITIVE_INFINITY,
prefix: queryCacheStoragePrefix,
refetchOnRestore: true,
serialize: (query) => stringify(query),
deserialize: (value) => parse<PersistedQuery>(value),
filters: {
predicate: (query) => isPersistableWidgetQueryKey(query.queryKey),
},
});
@@ -2,10 +2,9 @@
import type { PropsWithChildren } from "react";
import { useState } from "react";
import { QueryClient } from "@tanstack/react-query";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
import {
createWSClient,
httpBatchStreamLink,
@@ -22,12 +21,7 @@ import { TRPCClientError } from "@trpc/client";
import type { AppRouter } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import {
isPersistableWidgetQueryKey,
queryCacheDefaultGcTimeMs,
queryCacheDefaultRefetchIntervalMs,
queryCacheDefaultStaleTimeMs,
} from "@homarr/api/query-cache";
import { queryCacheDefaultStaleTimeMs } from "@homarr/api/query-cache";
import { createHeadersCallbackForSource, getTrpcUrl } from "@homarr/api/shared";
import { env } from "@homarr/common/env";
import { showWarningNotification } from "@homarr/notifications";
@@ -35,6 +29,7 @@ import { showWarningNotification } from "@homarr/notifications";
import { createWidgetQueryPersister } from "./query-cache-persister";
const getWebSocketProtocol = () => {
// window is not defined on server side
if (typeof window === "undefined") {
return "ws";
}
@@ -60,36 +55,32 @@ const wsClient = createWSClient({
});
export function TRPCReactProvider(props: PropsWithChildren) {
const [persister] = useState(() => createWidgetQueryPersister());
const [queryClient] = useState(() => {
const client = new QueryClient({
defaultOptions: {
queries: {
staleTime: queryCacheDefaultStaleTimeMs,
gcTime: queryCacheDefaultGcTimeMs,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: 3,
},
mutations: {
onError(error) {
if (
error instanceof TRPCClientError &&
error.data?.code === "FORBIDDEN" &&
error.message === "Mutations are disabled in demo mode"
) {
showWarningNotification({
title: "Demo mode",
message: "This action is disabled in demo mode.",
});
}
const [queryPersister] = useState(() => createWidgetQueryPersister());
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
persister: queryPersister.persisterFn,
staleTime: queryCacheDefaultStaleTimeMs,
},
mutations: {
onError(error) {
if (
error instanceof TRPCClientError &&
error.data?.code === "FORBIDDEN" &&
error.message === "Mutations are disabled in demo mode"
) {
showWarningNotification({
title: "Demo mode",
message: "This action is disabled in demo mode.",
});
}
},
},
},
},
});
client.setQueryDefaults([["widget"]], { refetchInterval: queryCacheDefaultRefetchIntervalMs });
return client;
});
}),
);
const [trpcClient] = useState(() => {
return clientApi.createClient({
@@ -107,6 +98,9 @@ export function TRPCReactProvider(props: PropsWithChildren) {
false: splitLink({
condition: ({ input }) => isNonJsonSerializable(input),
true: httpLink({
/**
* We don't want to transform the data here as we want to use form data
*/
transformer: {
serialize(object: unknown) {
return object;
@@ -121,7 +115,7 @@ export function TRPCReactProvider(props: PropsWithChildren) {
false: httpBatchStreamLink({
transformer: superjson,
url: getTrpcUrl(),
maxURLLength: 2083,
maxURLLength: 2083, // Suggested by tRPC: https://trpc.io/docs/client/links/httpBatchLink#setting-a-maximum-url-length
headers: createHeadersCallbackForSource("nextjs-react (json)"),
}),
}),
@@ -132,20 +126,10 @@ export function TRPCReactProvider(props: PropsWithChildren) {
return (
<clientApi.Provider client={trpcClient} queryClient={queryClient}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
maxAge: queryCacheDefaultGcTimeMs,
dehydrateOptions: {
shouldDehydrateQuery: (query) =>
query.state.status === "success" && isPersistableWidgetQueryKey(query.queryKey),
},
}}
>
<QueryClientProvider client={queryClient}>
<ReactQueryStreamedHydration transformer={superjson}>{props.children}</ReactQueryStreamedHydration>
<ReactQueryDevtools initialIsOpen={false} />
</PersistQueryClientProvider>
</QueryClientProvider>
</clientApi.Provider>
);
}
@@ -5,8 +5,10 @@ import { TRPCError } from "@trpc/server";
// Placed here because gridstack styles are used for board content
import "~/styles/gridstack.scss";
import type { DehydratedState } from "@tanstack/react-query";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import type { PersistedClient } from "@tanstack/react-query-persist-client";
import { parse } from "superjson";
import { getQueryClient } from "@homarr/api/server";
import { IntegrationProvider } from "@homarr/auth/client";
import { auth } from "@homarr/auth/next";
@@ -15,7 +17,7 @@ import { isNullOrWhitespace } from "@homarr/common";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
import type { WidgetKind } from "@homarr/definitions";
import { getQueryCacheAsync } from "@homarr/redis";
import { getAllQueryCacheAsync } from "@homarr/redis";
import { getI18n } from "@homarr/translation/server";
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
@@ -116,17 +118,36 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
};
};
interface PersistedEntry {
state: { data: unknown; dataUpdatedAt: number; status: string };
queryKey: unknown[];
queryHash: string;
}
async function QueryCacheHydration({ userId, boardId }: { userId: string; boardId: string }) {
try {
const serialized = await getQueryCacheAsync(userId, boardId);
if (!serialized) return null;
const entries = await getAllQueryCacheAsync(userId, boardId);
const queries: DehydratedState["queries"] = [];
const persisted = JSON.parse(serialized) as PersistedClient;
if (!persisted?.clientState?.queries?.length) return null;
for (const serialized of Object.values(entries)) {
try {
const persisted = parse<PersistedEntry>(serialized);
if (persisted.state.status !== "success") continue;
return <HydrationBoundary state={persisted.clientState} />;
} catch (error) {
logger.warn(new ErrorWithMetadata("Failed to hydrate query cache", { userId, boardId }, { cause: error }));
queries.push({
state: persisted.state as DehydratedState["queries"][number]["state"],
queryKey: persisted.queryKey,
queryHash: persisted.queryHash,
});
} catch {
// skip malformed entries
}
}
if (queries.length === 0) return null;
return <HydrationBoundary state={{ mutations: [], queries }} />;
} catch {
return null;
}
}
@@ -16,7 +16,11 @@ export const InitSqliteRestore = () => {
if (expanded) {
return (
<Stack gap="sm">
<Button variant="subtle" size="xs" onClick={() => setExpanded(false)}>
<Button
variant="subtle"
size="xs"
onClick={() => setExpanded(false)}
>
{t("backToOptions")}
</Button>
<Text size="sm" c="dimmed">
+1 -1
View File
@@ -139,11 +139,11 @@ export default async function Layout(props: {
suppressHydrationWarning
>
<head>
<Analytics />
<SearchEngineOptimization />
<CrowdinLiveTranslation locale={locale} />
</head>
<body className={["font-sans", fontSans.variable].join(" ")}>
<Analytics enabled={serverSettings.analytics.enableGeneral} />
<StackedProvider>
<Notifications pauseResetOnHover="notification" />
<ServiceWorkerRegistration />
@@ -1,3 +1,4 @@
import { headers } from "next/headers";
import Image from "next/image";
import {
Accordion,
@@ -40,10 +41,11 @@ import { getScopedI18n } from "@homarr/translation/server";
import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { getDependenciesAsync, getPackageVersion } from "~/versions/package-reader";
import githubContributors from "@static-data/contributors.json";
import openCollectiveContributors from "@static-data/opencollective-contributors.json";
import crowdinContributors from "@static-data/translators.json";
import type { PackageJsonDependencies } from "~/versions/package-reader";
import { getPackageVersion } from "~/versions/package-reader";
import type githubContributorsJson from "../../../../../../../static-data/contributors.json";
import type openCollectiveContributorsJson from "../../../../../../../static-data/opencollective-contributors.json";
import type crowdinContributorsJson from "../../../../../../../static-data/translators.json";
import classes from "./about.module.css";
export async function generateMetadata() {
@@ -54,10 +56,32 @@ export async function generateMetadata() {
};
}
const getHostAsync = async () => {
if (process.env.HOSTNAME) {
return `${process.env.HOSTNAME}:3000`;
}
return (await headers()).get("host");
};
export default async function AboutPage() {
const baseServerUrl = `http://${await getHostAsync()}`;
const t = await getScopedI18n("management.page.about");
const version = getPackageVersion();
const dependencies = await getDependenciesAsync();
const dependencies = (await fetch(`${baseServerUrl}/api/about/dependencies`).then((res) =>
res.json(),
)) as PackageJsonDependencies;
const githubContributors = (await fetch(`${baseServerUrl}/api/about/contributors/github`).then((res) =>
res.json(),
)) as typeof githubContributorsJson;
const crowdinContributors = (await fetch(`${baseServerUrl}/api/about/contributors/crowdin`).then((res) =>
res.json(),
)) as typeof crowdinContributorsJson;
const openCollectiveContributors = (await fetch(`${baseServerUrl}/api/about/contributors/opencollective`).then(
(res) => res.json(),
)) as typeof openCollectiveContributorsJson;
return (
<div>
@@ -161,7 +185,7 @@ export default async function AboutPage() {
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing="md">
{Object.entries(dependencies)
.filter(([, value]) => !value.includes("workspace:"))
.toSorted(([key1], [key2]) => key1.localeCompare(key2))
.sort(([key1], [key2]) => key1.localeCompare(key2))
.map(([name, version]) => (
<UnstyledButton
key={name}
@@ -1,7 +1,7 @@
import { Alert, Anchor, Text } from "@mantine/core";
import { IconFlask } from "@tabler/icons-react";
const FEEDBACK_URL = "https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets";
const FEEDBACK_URL = "https://github.com/homarr-labs/homarr/discussions/5916";
export function CustomWidgetBetaBanner() {
return (
@@ -150,13 +150,7 @@ const requiredFieldValidators: Record<string, (data: Record<string, unknown>, ct
});
},
statusIndicator: (data, ctx) => {
(
data.statusItems as Array<{
label: string;
jsonPath: string;
goodValues: string;
}>
)?.forEach((item, i) => {
(data.statusItems as Array<{ label: string; jsonPath: string; goodValues: string }>)?.forEach((item, i) => {
if (!item.label)
ctx.addIssue({
code: "too_small",
@@ -266,34 +260,15 @@ const formSchema = z
columns: z.array(z.object({ header: z.string(), jsonPath: z.string() })),
striped: z.boolean(),
compact: z.boolean(),
statGridItems: z.array(
z.object({
label: z.string(),
jsonPath: z.string(),
unit: z.string(),
color: z.string(),
}),
),
statGridItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), unit: z.string(), color: z.string() })),
statGridColumns: z.number(),
cardStyle: z.string(),
progressBars: z.array(
z.object({
label: z.string(),
valuePath: z.string(),
maxPath: z.string(),
unit: z.string(),
color: z.string(),
}),
z.object({ label: z.string(), valuePath: z.string(), maxPath: z.string(), unit: z.string(), color: z.string() }),
),
showPercentage: z.boolean(),
barSize: z.string(),
statusItems: z.array(
z.object({
label: z.string(),
jsonPath: z.string(),
goodValues: z.string(),
}),
),
statusItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), goodValues: z.string() })),
statusLayout: z.string(),
dotSize: z.string(),
countGridItems: z.array(z.object({ label: z.string(), jsonPath: z.string(), unit: z.string() })),
@@ -306,13 +281,7 @@ const formSchema = z
confirmText: z.string(),
successMessage: z.string(),
template: z.string(),
secrets: z.array(
z.object({
kind: z.string(),
value: z.string(),
hasValue: z.boolean().optional(),
}),
),
secrets: z.array(z.object({ kind: z.string(), value: z.string(), hasValue: z.boolean().optional() })),
})
.superRefine((data, ctx) => {
const validator = requiredFieldValidators[data.displayType];
@@ -393,31 +362,12 @@ const displayConfigBuilders: Record<string, (values: z.infer<typeof formSchema>)
valueSize: v.valueSize,
labelPosition: v.labelPosition,
}),
keyValue: (v) => ({
type: "keyValue",
mappings: v.mappings,
layout: v.kvLayout,
columns: v.kvColumns,
}),
table: (v) => ({
type: "table",
tablePath: v.tablePath,
columns: v.columns,
striped: v.striped,
compact: v.compact,
}),
statGrid: (v) => ({
type: "statGrid",
items: v.statGridItems,
columns: v.statGridColumns,
cardStyle: v.cardStyle,
}),
keyValue: (v) => ({ type: "keyValue", mappings: v.mappings, layout: v.kvLayout, columns: v.kvColumns }),
table: (v) => ({ type: "table", tablePath: v.tablePath, columns: v.columns, striped: v.striped, compact: v.compact }),
statGrid: (v) => ({ type: "statGrid", items: v.statGridItems, columns: v.statGridColumns, cardStyle: v.cardStyle }),
progressBars: (v) => ({
type: "progressBars",
bars: v.progressBars.map((b) => ({
...b,
maxPath: b.maxPath || undefined,
})),
bars: v.progressBars.map((b) => ({ ...b, maxPath: b.maxPath || undefined })),
showPercentage: v.showPercentage,
barSize: v.barSize,
}),
@@ -439,11 +389,7 @@ const displayConfigBuilders: Record<string, (values: z.infer<typeof formSchema>)
columns: v.countGridColumns,
valueSize: v.countValueSize,
}),
raw: (v) => ({
type: "raw",
jsonPath: v.rawJsonPath,
maxHeight: v.rawMaxHeight,
}),
raw: (v) => ({ type: "raw", jsonPath: v.rawJsonPath, maxHeight: v.rawMaxHeight }),
actionButton: (v) => ({
type: "actionButton",
buttonLabel: v.buttonLabel,
@@ -469,10 +415,7 @@ const serverToFormFieldMap: Record<string, Record<string, string>> = {
function extractServerErrors(err: unknown, displayType: string): Record<string, string> {
const errors: Record<string, string> = {};
const trpcErr = err as {
data?: { zodError?: { fieldErrors?: Record<string, string[]> } };
message?: string;
};
const trpcErr = err as { data?: { zodError?: { fieldErrors?: Record<string, string[]> } }; message?: string };
if (trpcErr?.data?.zodError?.fieldErrors) {
for (const [field, messages] of Object.entries(trpcErr.data.zodError.fieldErrors)) {
@@ -484,10 +427,7 @@ function extractServerErrors(err: unknown, displayType: string): Record<string,
}
try {
const issues = JSON.parse(trpcErr?.message ?? "[]") as Array<{
path: (string | number)[];
message: string;
}>;
const issues = JSON.parse(trpcErr?.message ?? "[]") as Array<{ path: (string | number)[]; message: string }>;
const fieldMap = serverToFormFieldMap[displayType] ?? {};
for (const issue of issues) {
@@ -511,13 +451,7 @@ const listItemDefaults = {
mapping: { label: "", jsonPath: "$", unit: "" },
column: { header: "", jsonPath: "$" },
statGridItem: { label: "", jsonPath: "$", unit: "", color: "blue" },
progressBar: {
label: "",
valuePath: "$",
maxPath: "",
unit: "",
color: "blue",
},
progressBar: { label: "", valuePath: "$", maxPath: "", unit: "", color: "blue" },
statusItem: { label: "", jsonPath: "$", goodValues: "online,true" },
countGridItem: { label: "", jsonPath: "$", unit: "" },
} as const;
@@ -587,10 +521,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
displayConfig: displayConfig as never,
secrets: values.secrets
.filter((s) => s.value)
.map((s) => ({
kind: s.kind as "apiKey" | "username" | "password",
value: s.value,
})),
.map((s) => ({ kind: s.kind as "apiKey" | "username" | "password", value: s.value })),
};
try {
@@ -604,10 +535,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
router.push(`/manage/custom-widgets/edit/${result.id}`);
} else if (definitionId) {
await updateMutation.mutateAsync({ id: definitionId, ...payload });
showSuccessNotification({
title: t("action.save"),
message: t("notification.updated", { name: values.name }),
});
showSuccessNotification({ title: t("action.save"), message: t("notification.updated", { name: values.name }) });
setPreviewRefreshSignal((n) => n + 1);
await utils.customWidget.all.invalidate();
await utils.customWidget.byId.invalidate({ id: definitionId });
@@ -619,10 +547,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
form.setErrors(serverErrors);
}
const errorKey = mode === "create" ? "notification.createError" : "notification.updateError";
showErrorNotification({
title: t("action.save"),
message: t(errorKey as never),
});
showErrorNotification({ title: t("action.save"), message: t(errorKey as never) });
}
});
@@ -641,10 +566,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
displayConfig: buildConfig?.(values) as z.infer<typeof displayConfigSchema>,
secrets: values.secrets
.filter((s) => s.value)
.map((s) => ({
kind: s.kind as CustomWidgetSecretKind,
value: s.value,
})),
.map((s) => ({ kind: s.kind as CustomWidgetSecretKind, value: s.value })),
definitionId,
};
}, [form.values, definitionId]);
@@ -827,18 +749,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi
</Group>
</Stack>
<Box
w={480}
style={{
flexShrink: 0,
position: "sticky",
top: 80,
alignSelf: "start",
maxHeight: "calc(100vh - 100px)",
overflow: "auto",
}}
visibleFrom="md"
>
<Box w={340} style={{ flexShrink: 0, position: "sticky", top: 100, alignSelf: "start" }} visibleFrom="md">
<Stack gap="sm">
<Button type="submit" fullWidth loading={createMutation.isPending || updateMutation.isPending}>
{mode === "create" ? t("action.create") : t("action.save")}
@@ -906,19 +817,13 @@ function DisplayTypeFields({
<Group grow>
<Select
label={t("field.valueSize")}
data={["sm", "md", "lg", "xl"].map((v) => ({
value: v,
label: t(`sizeOption.${v}` as never),
}))}
data={["sm", "md", "lg", "xl"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
{...form.getInputProps("valueSize")}
allowDeselect={false}
/>
<Select
label={t("field.labelPosition")}
data={["above", "below"].map((v) => ({
value: v,
label: t(`labelPositionOption.${v}` as never),
}))}
data={["above", "below"].map((v) => ({ value: v, label: t(`labelPositionOption.${v}` as never) }))}
{...form.getInputProps("labelPosition")}
allowDeselect={false}
/>
@@ -933,10 +838,7 @@ function DisplayTypeFields({
<Group grow>
<Select
label={t("field.kvLayout")}
data={["list", "grid"].map((v) => ({
value: v,
label: t(`layoutOption.${v}` as never),
}))}
data={["list", "grid"].map((v) => ({ value: v, label: t(`layoutOption.${v}` as never) }))}
{...form.getInputProps("kvLayout")}
allowDeselect={false}
/>
@@ -1056,10 +958,7 @@ function DisplayTypeFields({
<NumberInput label={t("field.gridColumns")} min={1} max={4} {...form.getInputProps("statGridColumns")} />
<Select
label={t("field.cardStyle")}
data={["filled", "outline", "subtle"].map((v) => ({
value: v,
label: t(`cardStyleOption.${v}` as never),
}))}
data={["filled", "outline", "subtle"].map((v) => ({ value: v, label: t(`cardStyleOption.${v}` as never) }))}
{...form.getInputProps("cardStyle")}
allowDeselect={false}
/>
@@ -1128,10 +1027,7 @@ function DisplayTypeFields({
<Switch label={t("field.showPercentage")} {...form.getInputProps("showPercentage", { type: "checkbox" })} />
<Select
label={t("field.barSize")}
data={["sm", "md", "lg"].map((v) => ({
value: v,
label: t(`sizeOption.${v}` as never),
}))}
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
{...form.getInputProps("barSize")}
allowDeselect={false}
/>
@@ -1207,19 +1103,13 @@ function DisplayTypeFields({
<Group grow>
<Select
label={t("field.kvLayout")}
data={["list", "grid"].map((v) => ({
value: v,
label: t(`layoutOption.${v}` as never),
}))}
data={["list", "grid"].map((v) => ({ value: v, label: t(`layoutOption.${v}` as never) }))}
{...form.getInputProps("statusLayout")}
allowDeselect={false}
/>
<Select
label={t("field.dotSize")}
data={["sm", "md", "lg"].map((v) => ({
value: v,
label: t(`sizeOption.${v}` as never),
}))}
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
{...form.getInputProps("dotSize")}
allowDeselect={false}
/>
@@ -1281,10 +1171,7 @@ function DisplayTypeFields({
<NumberInput label={t("field.gridColumns")} min={2} max={4} {...form.getInputProps("countGridColumns")} />
<Select
label={t("field.valueSize")}
data={["sm", "md", "lg"].map((v) => ({
value: v,
label: t(`sizeOption.${v}` as never),
}))}
data={["sm", "md", "lg"].map((v) => ({ value: v, label: t(`sizeOption.${v}` as never) }))}
{...form.getInputProps("countValueSize")}
allowDeselect={false}
/>
@@ -57,13 +57,13 @@ export const CustomWidgetList = ({ definitions }: CustomWidgetListProps) => {
{definitions.map((def) => (
<Table.Tr key={def.id} style={{ opacity: def.enabled ? undefined : 0.5 }}>
<Table.Td>
<Group gap="xs" wrap="nowrap">
<Group gap="xs">
{def.iconUrl ? (
<Avatar size={20} radius="sm" src={def.iconUrl} styles={{ image: { objectFit: "contain" } }} />
) : (
<IconApi size={16} />
)}
<Text size="sm" fw={500} lineClamp={1} c={def.enabled ? undefined : "dimmed"} style={{ minWidth: 0 }}>
<Text size="sm" fw={500} lineClamp={1} c={def.enabled ? undefined : "dimmed"}>
{def.name}
</Text>
</Group>
@@ -109,7 +109,7 @@ export function CustomWidgetPreview({
</Group>
)}
<Card withBorder p="xs" bg="var(--mantine-color-dark-7)" mah={320} style={{ overflow: "auto" }}>
<Card withBorder p="xs" bg="var(--mantine-color-dark-7)" mih={120}>
<PreviewDisplay data={displayData} />
</Card>
@@ -119,9 +119,7 @@ export function CustomWidgetPreview({
variant="subtle"
leftSection={<IconExternalLink size={14} />}
onClick={() => {
const blob = new Blob([fetchResult.rawResponse ?? ""], {
type: "application/json",
});
const blob = new Blob([fetchResult.rawResponse ?? ""], { type: "application/json" });
const blobUrl = URL.createObjectURL(blob);
window.open(blobUrl);
setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
Loaded 100 of 497 files, more files were not shown because too many files have changed in this diff. Show more