diff --git a/.env.example b/.env.example index cec6e8c36..997d60865 100644 --- a/.env.example +++ b/.env.example @@ -61,9 +61,26 @@ TURBO_TELEMETRY_DISABLED=1 # Enable mock integration UNSAFE_ENABLE_MOCK_INTEGRATION=true -# Public Workshop API origin used by Homarr's Workshop browser. The default is https://homarr.dev. -# This is read when the server starts, including in the production Docker image. -# WORKSHOP_API_URL=http://127.0.0.1:3003 +# Public URL contract. Values must be absolute HTTP(S) URLs without credentials, +# query strings, or fragments. Trailing slashes are normalized at startup. +HOMARR_WEBSITE_URL=https://homarr.dev +# PocketBase API used by Homarr and by the standalone documentation site. +# Defaults to HOMARR_WEBSITE_URL. +# WORKSHOP_API_URL=https://homarr.dev +# Public Workshop page opened from Homarr. Defaults to HOMARR_WEBSITE_URL/workshop. +# WORKSHOP_WEB_URL=https://homarr.dev/workshop + +# Documentation and PocketBase deployment settings. +# WORKSHOP_PUBLIC_ORIGIN=https://homarr.dev +# PB_ALLOWED_ORIGINS=* + +# Local Workshop stack: +# HOMARR_WEBSITE_URL=http://127.0.0.1:3003 +# WORKSHOP_API_URL=http://127.0.0.1:8090 +# WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop +# WORKSHOP_PUBLIC_ORIGIN=http://127.0.0.1:3003 +# Optional: GITHUB_CLIENT_ID= +# Optional: GITHUB_CLIENT_SECRET= # Enable demo mode (seeds a demo user "demo"/"demo", a sample board and a mock integration) # DEMO_MODE=true diff --git a/apps/docs/README.md b/apps/docs/README.md index f704c92f0..ede750aa7 100644 --- a/apps/docs/README.md +++ b/apps/docs/README.md @@ -21,8 +21,14 @@ pnpm dev:docs # Or from within this directory pnpm dev + +# Point the docs at a local PocketBase instance +WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs ``` +The standalone docs use `WORKSHOP_API_URL` for PocketBase and `HOMARR_WEBSITE_URL` for the public site origin. The old +`WORKSHOP_URL` name is a deprecated one-release alias. + ## Build ```bash @@ -40,7 +46,7 @@ Uses oxlint and oxfmt (same as the rest of the monorepo): ```bash pnpm lint # oxlint pnpm format # oxfmt (check) -pnpm format -- --write # oxfmt (fix) +pnpm format:fix # oxfmt (fix) ``` ## Typecheck @@ -93,7 +99,7 @@ pnpm verify:search | Layer | Technology | | -------------- | ------------------------------------------- | | Site generator | Docusaurus 3 (`@docusaurus/preset-classic`) | -| Styling | Tailwind CSS 3 + Docusaurus Infima | +| Styling | Tailwind CSS 4 + Docusaurus Infima | | Icons | `@tabler/icons-react` | | Diagrams | Mermaid (`@docusaurus/theme-mermaid`) | | Search | Algolia DocSearch | diff --git a/apps/docs/docs/advanced/development/getting-started.mdx b/apps/docs/docs/advanced/development/getting-started.mdx index fb2cb9560..3a8639f74 100644 --- a/apps/docs/docs/advanced/development/getting-started.mdx +++ b/apps/docs/docs/advanced/development/getting-started.mdx @@ -8,62 +8,107 @@ tags: sidebar_position: 1 --- -## Setting up your development environment - -This guide will help you set up your development environment to start contributing to homarr. +## Set up your development environment ### Prerequisites -- **Node.js (lts)**: You can download it from the [official website](https://nodejs.org/). -- **pnpm (latest)**: You can install it by running `npm install -g pnpm`. -- **Git**: You can download it from the [official website](https://git-scm.com/). -- **Docker or Docker Desktop**: You can download it from the [official website](https://www.docker.com/). -- **A GitHub account**: You can create one on the [official website](https://github.com/). +- Node.js 24.18 or newer +- Corepack and pnpm 11.11 +- Git +- Docker with Docker Compose -### Setting up the project +The versions in the root `package.json` are the supported baseline. Let Corepack select the repository version instead +of installing an unrelated global pnpm: -1. Fork the repository by clicking on the "Fork" button on the top right corner of the repository page. +```sh +corepack enable +corepack install +pnpm install --frozen-lockfile +``` -![Fork button](./img/fork.png) +### Run the Homarr application -2. Clone the repository to your local machine with `git clone`. You can find the url to use by clicking on the "Code" button on the top right corner of your forks repository page. +1. Copy `.env.example` to `.env`. +2. Set `DB_URL` to an absolute writable SQLite file path. +3. Keep Redis and the optional local databases running in one terminal. +4. Apply migrations and start Next.js in another terminal. -![Clone button](./img/clone.png) +```sh +cp .env.example .env +``` -3. Change directory to the project folder with `cd homarr`. +Terminal 1: -4. Install the dependencies with `pnpm install`. +```sh +pnpm docker:dev +``` -5. Start redis by running `pnpm run docker:dev`. This will run a docker-compose file with a redis service. +Terminal 2: -6. Copy the `.env.example` file to `.env` and replace `DB_URL` with the full path you want to use for the sqlite database file. For example `DB_URL='C:\\Dev\\homarr\\packages\\db\\db.sqlite'` +```sh +pnpm db:migration:sqlite:run +pnpm dev +``` -7. Run the migrations to create the sqlite db file with `pnpm run db:migration:sqlite:run`. +The infrastructure command remains in the foreground. Open `http://127.0.0.1:3000`; the first user created through +Homarr becomes the instance administrator. -8. Start the development server with `pnpm dev` and wait for a message that says `✓ Ready in 10.7s` +### Run PocketBase and documentation -9. Open your browser and navigate to [http://localhost:3000](http://localhost:3000). (Loading the page might take up to 2 minutes the first time) +Start PocketBase in another terminal: -### Useful npm scripts +```sh +cp apps/workshop/.env.example apps/workshop/.env +docker compose --env-file apps/workshop/.env -f apps/workshop/docker-compose.yml up --build workshop +``` -#### During development +Then start Docusaurus in a third terminal: -- `pnpm dev`: Start the development server. -- `pnpm cli`: Run cli commands. -- `pnpm db:migration:sqlite:run`: Run the migrations to create or update the sqlite db file. -- `pnpm db:migration:sqlite:generate`: Generate a new migration file for the sqlite db. Also works with `mysql`. -- `pnpm db:studio`: Open a visual studio for the database. -- `pnpm package:new`: Create a new package in the monorepo. +```sh +WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs +``` -#### Checking code quality +PocketBase runs on port 8090 and Docusaurus on port 3003. Stopping the services preserves the PocketBase volume. -- `pnpm lint`: Run the linter to check for code quality issues. -- `pnpm format:fix`: Run the formatter to fix code style issues. -- `pnpm test`: Run the tests. You can also use `pnpm test:ui` to get a UI for the tests. -- `pnpm typecheck`: Run the typescript type checker. +Open `http://127.0.0.1:8090/_/` and create the first PocketBase superuser. The Workshop UI is available at +`http://127.0.0.1:3003/workshop`. -#### Building the project +Point Homarr at the local services with: -- `pnpm build`: Build the project for production. -- `docker build -t homarr .`: Build a docker image for the project. -- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:latest`: Run the created docker image. +```dotenv +HOMARR_WEBSITE_URL=http://127.0.0.1:3003 +WORKSHOP_API_URL=http://127.0.0.1:8090 +WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop +``` + +Restart `pnpm dev` after changing `.env`. GitHub OAuth is optional locally. See +[`apps/workshop/README.md`](https://github.com/homarr-labs/homarr/blob/HEAD/apps/workshop/README.md) for OAuth, +Workshop moderator promotion, logs, tests, backup, restore, stopping, and intentional volume reset. + +### URL testing + +Docusaurus reads `HOMARR_WEBSITE_URL` and `WORKSHOP_API_URL` when it starts or builds. Homarr also reads +`WORKSHOP_WEB_URL` when its server starts, so these values do not rely on `NEXT_PUBLIC_*` build variables. Test a +separate API origin before release: + +```sh +HOMARR_WEBSITE_URL=http://127.0.0.1:3003 \ +WORKSHOP_API_URL=http://127.0.0.1:8090 \ +WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop \ +pnpm dev:docs +``` + +### Useful commands + +- `pnpm dev` — Next.js on port 3000. +- `pnpm dev:docs` — Docusaurus only on port 3003. +- `pnpm docker:workshop` — local PocketBase through Docker Compose. +- `pnpm cli` — Homarr operations CLI. +- `pnpm db:migration:sqlite:run` — create or update a SQLite database. +- `pnpm db:studio` — inspect the database. +- `pnpm format` / `pnpm format:fix` — check or apply oxfmt. +- `pnpm lint` / `pnpm lint:fix` — check or apply oxlint. +- `pnpm typecheck` — typecheck all workspaces. +- `pnpm test` — unit and contract suites. +- `pnpm test:workshop` — disposable Workshop integration suite. +- `pnpm build` — production monorepo build. diff --git a/apps/docs/docs/advanced/environment-variables/index.mdx b/apps/docs/docs/advanced/environment-variables/index.mdx index d45a19472..e740884fc 100644 --- a/apps/docs/docs/advanced/environment-variables/index.mdx +++ b/apps/docs/docs/advanced/environment-variables/index.mdx @@ -25,13 +25,16 @@ This is especially useful for [docker secrets](https://docs.docker.com/engine/sw Using the `PUID` and `PGID` will require you to set the correct permissions on the mounted volumes and if used the docker socket. See more in the [Running as a different user](/docs/advanced/running-as-different-user) documentation. ::: -| Environment Variable | Description | Possible values | Default | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ------- | -| `PUID` | User ID to run the container as | Any valid user ID | `0` | -| `PGID` | Group ID to run the container as | Any valid group ID | `0` | -| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` | -| `NO_EXTERNAL_CONNECTION` | Disables some requests that need internet connection | `true` or `false` | `false` | -| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` | +| Environment Variable | Description | Possible values | Default | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ----------------------------- | +| `PUID` | User ID to run the container as | Any valid user ID | `0` | +| `PGID` | Group ID to run the container as | Any valid group ID | `0` | +| `LOG_LEVEL` | Log level to use | `debug` / `info` / `warn` / `error` | `info` | +| `NO_EXTERNAL_CONNECTION` | Disables some requests that need internet connection | `true` or `false` | `false` | +| `ENABLE_DNS_CACHING` | Enables dns caching. Enabled by default, but if you experience issues with IPv6 or static IPs, see [#4006](https://github.com/homarr-labs/homarr/issues/4006) or disable this flag | `true` or `false` | `true` | +| `HOMARR_WEBSITE_URL` | Main Homarr documentation/site base used by runtime links. Must be an absolute HTTP(S) URL. | HTTP(S) URL | `https://homarr.dev` | +| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone documentation. | HTTP(S) URL | `HOMARR_WEBSITE_URL` | +| `WORKSHOP_WEB_URL` | Public Workshop page opened from Homarr. | HTTP(S) URL | `HOMARR_WEBSITE_URL/workshop` | ## Authentication @@ -105,7 +108,12 @@ External redis is currently not supported with Proxmox Community Scripts The advanced deployments environment variables should only be used if you know what you are doing. ::: -| Environment Variable | Description | -| ------------------------ | --------------------------------------------------------------------------------------------------------------- | -| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts | -| `WORKSHOP_API_URL` | Runtime public origin of the separate Workshop PocketBase service. Homarr uses `https://homarr.dev` by default. | +| Environment Variable | Description | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | +| `DB_MIGRATIONS_DISABLED` | Disable db migrations. For example for helm charts. | +| `WORKSHOP_PUBLIC_ORIGIN` | Origin used in PocketBase-generated links and email. It must be set explicitly in production. | +| `PB_ALLOWED_ORIGINS` | Browser origins accepted by PocketBase. The central public community defaults to `*` so self-hosted Homarr instances can use OAuth. | + +All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize +trailing slashes when Homarr or Docusaurus starts. The Docusaurus-only `WORKSHOP_URL` name is accepted as a deprecated alias for +`WORKSHOP_API_URL` for one release. `WORKSHOP_API_URL` takes precedence. diff --git a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx index 1f3129b4e..25afa3724 100644 --- a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx @@ -4,6 +4,9 @@ An external agent creates one widget at a time with the Homarr MCP server. The a Homarr does not create widget packs or drafts for multiple widgets. +The MCP API key must belong to a Homarr administrator. Non-administrator keys do not list Custom Widget tools, prompts, +or resources. The same administrator requirement applies to the matching HTTP authoring endpoints. + View the Custom Widget Skill on skills.sh @@ -67,7 +70,10 @@ These interfaces do not return an archive URL or archive hash. 12. Update the saved definition if the final verification finds a problem. 13. Repeat the procedure for the next widget. -Each source-configuration URL applies to one definition or preview and one source. The user confirms the server URL, network scope, and required credential fields directly in Homarr. The agent receives only request status and never receives plaintext credentials. Preview-scoped values expire with that preview; definition-scoped values persist. +Each source-configuration URL applies to one definition or preview and one source. It is short-lived, single-use, and +can only be opened and completed by an authenticated Homarr administrator. The administrator confirms the server URL, +network scope, and required credential fields directly in Homarr. The agent receives only request status and never +receives plaintext credentials. Preview-scoped values expire with that preview; definition-scoped values persist. Agents can install existing widgets without regenerating them: @@ -88,8 +94,11 @@ Homarr maintainers can run the same self-contained prompt that the workbench cop OPENROUTER_API_KEY=... pnpm --filter @homarr/custom-widgets test:ai ``` -The suite uses DeepSeek V4 Pro to author five complex widgets and DeepSeek V4 Flash to judge only widgets that pass Homarr's real parser and canonical schema. It saves every response, validated `widget.json`, `widget.jsx`, and detailed category scores under `packages/custom-widgets/.ai-evaluations/`. +The suite uses DeepSeek V4 Pro to author Pokédex, Portainer, football, Jellyfin, and Home Assistant widgets. Before DeepSeek V4 Flash judges a result, it must pass Homarr's real clipboard parser, canonical schema, and deterministic scenario checks for the documented API routes, authentication, bindings, actions, and essential runtime components. It saves every response, validated widget manifest, and detailed category scores under `packages/custom-widgets/.ai-evaluations/`. -The run has a hard budget of ten generation attempts across all scenarios. Override the models or budget with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`, and `CUSTOM_WIDGET_AI_MAX_LOOPS`. Generated artifacts are local and ignored by Git; promote a reviewed, validated result into the bundled-widget source deliberately. +Each scenario stops as soon as it passes and has a hard limit of ten generation attempts. Override the models or +per-scenario limit with `OPENROUTER_GENERATOR_MODEL`, `OPENROUTER_JUDGE_MODEL`, and +`CUSTOM_WIDGET_AI_MAX_LOOPS`. Generated artifacts are local and ignored by Git; promote a reviewed, validated result +into the bundled-widget source deliberately. The reference API check uses a real PokeAPI list/detail request and a read-only Portainer container query when `PORTAINER_BASE_URL` and `PORTAINER_API_KEY` are set. Use `pnpm --filter @homarr/custom-widgets test:reference-apis -- --pokeapi-only` for the public PokeAPI check alone. To exercise start, restart, and stop against a disposable container, also set `PORTAINER_TEST_CONTAINER_ID` and the explicit safeguard `PORTAINER_ALLOW_MUTATIONS=1`; the script restores the container's original running state. diff --git a/apps/docs/docs/management/custom-widgets/authoring.mdx b/apps/docs/docs/management/custom-widgets/authoring.mdx index 2fc8d2c1f..429827c55 100644 --- a/apps/docs/docs/management/custom-widgets/authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/authoring.mdx @@ -27,11 +27,12 @@ Every option has a default. Supported controls include text, textarea, number, s Enter the desired widget and optional API documentation URL, then select **Copy AI prompt**. The single self-contained prompt is below 12,000 characters, starts with your request, includes the lean contract and two polished examples, and never claims that an offline chat has MCP tools. -The model must return exactly: +The model must return one fenced `json` block containing the complete widget, including the JSX in its `template` +field. Copy that single block and select **Paste AI response**. Homarr fills the visual builders and the JSX editor +without any placeholder replacement or second paste. -1. One fenced `json` block with `template: "__HOMARR_TEMPLATE__"`. -2. One fenced `jsx` block with the readable template. - -Select **Paste AI response**. Homarr fills the visual builders and shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading, empty, failure, and success states. +Homarr shows all actionable schema or JSX errors inline. Enter credentials separately. The prompt deliberately +recommends a focused Mantine component set, visual hierarchy, responsive layout, theme compatibility, and loading, +empty, failure, and success states. Connected agents should use the skills.sh skill and retrieve the live schema and individual component metadata only as needed. diff --git a/apps/docs/docs/management/custom-widgets/custom-jsx.mdx b/apps/docs/docs/management/custom-widgets/custom-jsx.mdx index bb59f114d..a8bb3bd85 100644 --- a/apps/docs/docs/management/custom-widgets/custom-jsx.mdx +++ b/apps/docs/docs/management/custom-widgets/custom-jsx.mdx @@ -38,7 +38,8 @@ Bounded regex literals are supported for string matching, splitting, search, and - `SubFetch` runs a manual query with its `params`. Set `triggerContent` to a card or image to make that content the accessible click and keyboard trigger instead of showing the default load button. - `ActionButton` and `ToggleSwitch` run actions. -- `RefreshButton` renders a compact, accessible icon control that refreshes load queries. +- `RefreshButton` renders a compact, accessible icon control that refreshes load queries for authenticated viewers. It + is disabled for anonymous public-board viewers because refreshing invalidates shared server cache state. - `Icon`/`TablerIcon` resolves installed safe icons. Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile. diff --git a/apps/docs/docs/management/custom-widgets/index.mdx b/apps/docs/docs/management/custom-widgets/index.mdx index d58b73162..508dde70c 100644 --- a/apps/docs/docs/management/custom-widgets/index.mdx +++ b/apps/docs/docs/management/custom-widgets/index.mdx @@ -49,6 +49,21 @@ interface HomarrCustomWidgetV2 { The browser renders JSX. Homarr's server executes named requests, applies board permissions and network protections, and injects credentials. The browser never receives source credentials. +## Permissions + +Custom Widget management is an administrator-only surface. Only a Homarr administrator can create, import, migrate, +preview, publish, install, configure, or write secrets, including through HTTP authoring resources or MCP. An API key +only lists Custom Widget MCP tools, prompts, and resources when its owner is an administrator. + +Adding or configuring a Custom Widget placement requires both administrator access and permission to modify that +board. A non-administrator with board-modify access can move, resize, change presentation settings, or remove an +existing placement, but cannot add one, change its definition, refresh interval, or Custom Widget options. Duplicating +a board that contains Custom Widgets also requires an administrator. + +Once an administrator has placed a widget, ordinary board permissions apply to rendering and requests. Authorized +non-administrators can use it on private boards, and queries can render for anonymous viewers on public boards. +Mutations remain authenticated and enforce each request's view, modify, or full permission. + ## Authoring paths - [Create a Custom Widget](./authoring) visually or with one copied AI prompt. @@ -60,9 +75,26 @@ The browser renders JSX. Homarr's server executes named requests, applies board The workbench keeps one continuous layout with visual source, request, and option editors. Advanced raw JSON remains available for experts and AI paste. Invalid values stay inline and disable both preview and Save/Create until they are corrected. -Exports, imports, Workshop, MCP, and storage all validate the same schema. Widgets saved in the legacy format remain visible with a **Migration required** badge after upgrading. Use **Copy migration prompt**, give the redacted prompt to an LLM, then import its v2 JSON from that widget's action menu. Homarr keeps the legacy record until the replacement validates and is confirmed; credentials are never included in the prompt. +Exports, imports, the official Workshop client, and MCP validate the same schema. PocketBase stores community +submissions without interpreting their content, and Homarr validates them before installation. Widgets saved in the v1 +format remain visible to administrators with a **Migration required** badge after upgrading. Use **Copy migration +prompt**, give the redacted prompt to an LLM, copy its single complete JSON block, then select **Paste migrated widget** +from that widget's action menu. Homarr reads the `template` value into the JSX editor automatically. The prompt keeps safe +request metadata but deliberately omits the v1 display configuration because legacy free-text and JSX can contain +hard-coded credentials. Add a credential-free description of the widget's visible layout and behavior when sending the +prompt. -When adding widgets while editing a board, the Custom Widgets section always includes **Install from Workshop**. The Workshop and API source review open above the widget picker without closing it. After import, the new definition appears in the picker. Its tile waits for the board to be saved before server requests run and loads automatically after a successful save; no page reload is required. +Homarr keeps the original v1 definition and encrypted secret as a compatibility backup even after the validated v2 +replacement is created. The v2 record uses the same stable ID and takes precedence. The archived v1 copy stays hidden +while the replacement exists; deleting the v2 record surfaces the v1 entry as migration-required again. Automatic +legacy deletion is intentionally unavailable during v2 stabilization. Credentials are never included in prompts, +logs, or exports. + +For administrators adding widgets while editing a board, the Custom Widgets section includes **Install from +Workshop**. Non-administrators do not see the Custom Widget picker or Workshop actions. The Workshop and API source +review open above the widget picker without closing it. After import, the new definition appears in the picker. Its tile +waits for the board to be saved before server requests run and loads automatically after a successful save; no page +reload is required. ## Import and Workshop @@ -70,10 +102,25 @@ Import review shows origins, authentication modes, network scopes, methods, acti For example, installing **Tautulli Activity** asks for both the user's Tautulli URL and API key. A stable public source such as PokéAPI keeps its published URL without creating unnecessary setup work. -Workshop publishes technically valid widgets immediately; users can vote or report them, and administrators can delete submissions. Publishers should replace personal private-network hosts with a clear service placeholder before publishing. +Workshop publishes technically valid widgets immediately; users can vote or report them, and Workshop moderators can +delete submissions. Community moderation is separate from Homarr administration and is not a security boundary. +Publishers should replace personal private-network hosts with a clear service placeholder before publishing. Homarr +performs definitive validation again at install time. Connected agents can search, inspect, and install Workshop widgets with `customWidget_workshopSearch`, `customWidget_workshopGet`, and `customWidget_workshopInstall`. After installation they can set a source URL and credentials with `customWidget_sourceConfigure`, or create a secure user-facing setup URL with `customWidget_configurationRequestUser`. Secret values are never returned through MCP. +These tools are only exposed to API keys owned by Homarr administrators. Opening and completing a source-configuration +URL also requires the same authenticated administrator; the URL is short-lived, single-use, and redacted. + +## Reliability and rollback + +Custom Widget response caches and action invalidation use shared Redis state, so multiple Homarr replicas see the same +versioned cache generation. Deployments must point every replica at the same Redis service. + +Before upgrading, back up the database and `SECRET_ENCRYPTION_KEY`; a v1 binary can still read preserved v1 tables +during application rollback. Widgets created only in v2 may be unavailable to the rolled-back binary, but do not +replace the v1 backup. + ## Bundled examples Homarr seeds five disabled examples without credentials and never overwrites an edited seed. They use the same public contract. The Tautulli example is the highest-scoring widget preserved from Homarr's AI authoring evaluation: diff --git a/apps/docs/docs/management/custom-widgets/requests-and-security.mdx b/apps/docs/docs/management/custom-widgets/requests-and-security.mdx index cc784cbe9..1cae3ced1 100644 --- a/apps/docs/docs/management/custom-widgets/requests-and-security.mdx +++ b/apps/docs/docs/management/custom-widgets/requests-and-security.mdx @@ -12,7 +12,9 @@ Requests are keyed by ID. `source`, `kind`, `method`, `trigger`, `auth`, and `pe The request `path` is appended to the source `baseUrl`, including a base path such as `/api/v2`. -GET, POST, PUT, PATCH, and DELETE work for queries and actions. Kind—not method—controls behavior. Load queries run automatically, manual queries run through `SubFetch`, and actions run only from `ActionButton` or `ToggleSwitch`. +GET, POST, PUT, and PATCH work for queries and actions. DELETE is restricted to actions so it can never run while a +board loads. Kind controls the remaining behavior: load queries run automatically, manual queries run through +`SubFetch`, and actions run only from `ActionButton` or `ToggleSwitch`. Bind values directly where they are used: @@ -40,3 +42,12 @@ Paths and query-string values must resolve to text, numbers, or booleans. JSON b Every request uses Homarr's existing server executor: same-origin source enforcement, network scope, DNS and redirect validation, SSRF protection, board permissions, rate and request limits, body and response limits, timeouts, and encrypted credential injection. Static headers cannot override credentials, cookies, routing, proxy, forwarding, or hop-by-hop headers. Queries default to view permission and actions to modify. DELETE always requires full permission and confirmation. Actions are never cached, retried, or run on load. + +Management and runtime are intentionally separate. A Homarr administrator with board-modify permission places and +configures the widget. After placement, query and action authorization follows the request's board permission. Public +boards may execute queries for anonymous viewers, but mutations stay authenticated. This includes shared-cache +invalidation: `RefreshButton` is disabled for anonymous viewers, while ordinary scheduled query rendering remains +available. + +Responses use bounded, versioned shared-Redis cache keys. Action invalidation advances the shared version so every +Homarr replica stops serving the previous response without scanning or deleting a key prefix. diff --git a/apps/docs/docs/management/custom-widgets/troubleshooting.mdx b/apps/docs/docs/management/custom-widgets/troubleshooting.mdx index 544654718..068d8a21e 100644 --- a/apps/docs/docs/management/custom-widgets/troubleshooting.mdx +++ b/apps/docs/docs/management/custom-widgets/troubleshooting.mdx @@ -4,7 +4,16 @@ The definition does not exist. If it was deleted, remove the unavailable tile from the board. -If the Custom Widgets page shows the definition with a **Migration required** badge, open its action menu and select **Copy migration prompt**. Give that redacted prompt to an LLM, then import the resulting v2 JSON from the same menu. Homarr keeps the legacy definition until the replacement validates and you confirm the migration. +If the Custom Widgets page shows the definition with a **Migration required** badge, an administrator can open its +action menu and select **Copy migration prompt**. Give that redacted prompt to an LLM, then import the resulting v2 JSON +from the same menu. The LLM returns one complete JSON block; copy it once and select **Paste migrated widget**. Homarr +places its `template` value into the JSX editor automatically. Because arbitrary v1 display text or JSX may contain +credentials, the prompt omits that display configuration; describe the old widget's visible layout and behavior without +including secrets. Other viewers see **Contact an administrator** instead of a migration link. + +Homarr keeps the v1 definition and encrypted secret after migration. The validated v2 replacement uses the same ID and +takes precedence; the archived v1 record reappears if the v2 replacement is deleted. There is no v1 permanent-delete +action during the v2 stabilization period. Otherwise, install or create a Custom JSX v2 definition. @@ -34,6 +43,11 @@ An invalid definition disables widget preview and Save/Create. The Data, Options The Diagnostics tab shows the current schema errors. +## Workshop unavailable + +If Workshop is unavailable, check `WORKSHOP_API_URL` and PocketBase health. Widgets that are already installed continue +to work. + ## Correct request errors Check the API source network scope. Check the base URL and credentials. @@ -44,6 +58,9 @@ For “invalid placeholder,” use `{option:name}` or `{param:name}`. Load queri The journal shows the method, request ID, path template, status, and duration. +In a multi-instance deployment, all Homarr replicas must share Redis. Versioned cache invalidation is distributed +through Redis; replicas connected to different Redis databases can serve stale query responses after an action. + ## Correct a Mantine prop warning `UNKNOWN_MANTINE_PROP` is a warning. Homarr passes the prop to Mantine. @@ -57,3 +74,10 @@ Use safe widget options, temporary inputs, or named requests instead. The widget option definitions changed and the saved values are invalid. Open the item settings. Correct the marked configuration values. Homarr does not infer widget option migrations. + +## Roll back safely + +Before an application upgrade, back up the database and preserve the exact `SECRET_ENCRYPTION_KEY`. The v2 schema keeps +the physical v1 definition and secret tables readable by a v1 binary. Roll back the application without running +manual cleanup, verify legacy widgets, and restore the database backup if the migration itself must be reversed. +PocketBase Workshop data is a separate backup and restore operation. diff --git a/apps/docs/docs/management/mcp.mdx b/apps/docs/docs/management/mcp.mdx index 35a17b52b..34a5ac61a 100644 --- a/apps/docs/docs/management/mcp.mdx +++ b/apps/docs/docs/management/mcp.mdx @@ -92,6 +92,10 @@ MCP supports two authentication methods: 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. +Custom Widget and Workshop authoring are different: their tools, prompts, and resources are exposed only when the API +key owner is a Homarr administrator. Non-administrator keys cannot discover or invoke those capabilities. Creating an +API key never elevates the owner. + ## Testing the Connection Verify your MCP endpoint is working: @@ -168,6 +172,9 @@ The tools table on the MCP tab is dynamically generated - it always reflects the An agent creates one `homarr-custom-widget-v2` definition at a time. Start with the native prompt `homarr-custom-widget-author`. +The API key owner must be a Homarr administrator. The same rule applies to management, import, migration, preview, +secrets, publication, Workshop installation, and the HTTP authoring resources. + Then, read the live schema and component resources. These resources describe the installed Mantine components. 1. Call `customWidget_getAuthoringPrompt` and `customWidget_schema`. @@ -194,7 +201,8 @@ Discovery resources: - `homarr://custom-widgets/examples/{name}` - `homarr://custom-widgets/skill` -An HTTP-only harness can use the matching `/api/custom-widgets/authoring-prompt`, `/schema`, `/components`, and `/skill` endpoints instead of MCP resource URIs. +An authenticated administrator can use the matching `/api/custom-widgets/authoring-prompt`, `/schema`, `/components`, +and `/skill` endpoints instead of MCP resource URIs. These endpoints are not public schema downloads. The Custom Widget tools are: @@ -219,7 +227,13 @@ The Custom Widget tools are: - `customWidget_getAuthoringPrompt` - `customWidget_getSkill` -The `custom-widget-manage` permission permits authoring. A direct credential write also requires the `custom-widget-secret-write` permission. +Custom Widget permissions are not assignable group capabilities. Administrator status is required for the complete +authoring surface. A source-configuration URL is short-lived and single-use, requires the same logged-in administrator +to open and complete it, and never returns plaintext credentials to the agent. + +Adding a `customApi` item through `board_addItem`, changing its definition or options, and duplicating a board that +contains one also require an administrator with board-modify access. Existing placements continue to render and run +authorized requests according to normal board permissions. Homarr applies its request security controls during request execution. These controls include SSRF, DNS, redirect, size, timeout, rate, and permission controls. diff --git a/apps/docs/docs/management/users/index.mdx b/apps/docs/docs/management/users/index.mdx index 6170a02eb..a7181a1f5 100644 --- a/apps/docs/docs/management/users/index.mdx +++ b/apps/docs/docs/management/users/index.mdx @@ -177,6 +177,11 @@ Currently, the following permissions are available: - **Full integration access**: Allow members to manage, use and interact with any integration - **Administrator**: Members with this permission have full access to all features and settings +Custom Widget authoring, import, migration, preview, secrets, Workshop access, and placement configuration are reserved +for administrators and are not separate assignable group permissions. Board modifiers can still move, resize, change +presentation settings, or remove an existing Custom Widget placement, and board viewers can use placed widgets as +allowed by each request. + When you changed permissions, you must confirm your changes by clicking the banner at the bottom: ![](./img/changing-group-permissions.png) diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 887430e1e..4170d1681 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -17,9 +17,15 @@ The [Community Workshop](pathname:///workshop) is where Homarr users share and d **custom CSS themes** and **custom widgets**. You can browse submissions, upvote the ones you like, report abuse, and install entries directly into your own Homarr instance. +The central `homarr.dev` Workshop is a public community. Browsing it on the website is public. Browsing, installing, +publishing, or importing Workshop content **inside a Homarr instance is restricted to Homarr administrators**. + :::warning -The workshop contains community-made content. Widgets may contain unwanted or malicious content and it is your duty to check & verify safety of the content. While we try to remove malicious content as best as we can, we take no responsibility. Always review the widget JSON and all CSS before installing it on your instance and take frequent backups as prevention measures. +The Workshop contains community-made content. Moderation is not a security boundary, and PocketBase does not interpret +or validate submission content. Always review widget capabilities, network origins, actions, and CSS before installing. +Homarr's installation-time validator is definitive and can reject content that the public service accepted earlier. +Back up your instance before installing untrusted content. ::: @@ -29,12 +35,12 @@ The workshop contains community-made content. Widgets may contain unwanted or ma - Sign in with **GitHub** to upvote/downvote, submit your own creations, or report a submission. - Each submission shows its title, description, linked GitHub author, revision, optional screenshots, comments, and full content. Widget pages also summarize API sources, actions, options, network access, and required credentials before installation. - A new submission receives one real upvote owned by its author. Votes remain unique per user and submission. -- Report details and reporter identities are visible only to Workshop administrators. Reported submissions are highlighted in the administration page, where each open report can be dismissed or the submission can be deleted. Reports do not automatically remove a submission. +- Report details and reporter identities are visible only to Workshop moderators. Reported submissions are highlighted in the moderation page, where each open report can be dismissed or the submission can be deleted. Reports do not automatically remove a submission. - The manual **Outdated** badge warns visitors that a submission may need an update. ## Comments -Comments are publicly readable and appear in chronological conversation bubbles with the author's avatar and GitHub profile. Signed-in users can post; authors can edit or delete their own comments, and Workshop administrators can delete any comment. Failed posts keep the text you typed. When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, the submission author receives one email for a comment from another user. Self-comments, edits, and deletions do not send mail. +Comments are publicly readable and appear in chronological conversation bubbles with the author's avatar and GitHub profile. Signed-in users can post; authors can edit or delete their own comments, and Workshop moderators can delete any comment. Failed posts keep the text you typed. When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, the submission author receives one email for a comment from another user. Self-comments, edits, and deletions do not send mail. ## Submitting @@ -49,7 +55,14 @@ Comments are publicly readable and appear in chronological conversation bubbles Submissions are published immediately. Reported entries are reviewed and may be removed. Authors can edit a submission later to publish a new revision. The edit form can keep or remove existing screenshots and add new PNG, JPG, or WebP images without changing the Workshop URL. -When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, every current Workshop administrator receives an email when a report is submitted. If an administrator deletes the reported submission, its author receives a separate removal email. Author self-deletion does not send a moderation email. +When SMTP and `WORKSHOP_PUBLIC_ORIGIN` are configured, every current Workshop moderator receives an email when a report is submitted. If a moderator deletes the reported submission, its author receives a separate removal email. Author self-deletion does not send a moderation email. + +:::info Separate roles + +A Workshop moderator is a user with PocketBase's internal `isAdmin` field. This role moderates the central community +and is unrelated to a Homarr instance administrator. + +::: ## Submission guidelines @@ -61,9 +74,9 @@ that primarily promotes unrelated products or services. You can install workshop entries without leaving Homarr: -- **Custom widgets** — go to **Manage → Custom Widgets**, or use **Install from Workshop** in the +- **Custom widgets (Homarr administrators only)** — go to **Manage → Custom Widgets**, or use **Install from Workshop** in the board widget picker. Pick an entry, review its capabilities, and enter any required local secrets before import. -- **Custom CSS** — open a board's **Settings → Custom CSS**, click **Install from Workshop**, and +- **Custom CSS from Workshop (Homarr administrators only)** — open a board's **Settings → Custom CSS**, click **Install from Workshop**, and pick a theme to load it into the editor before saving. Alternatively, open a submission in the Workshop and use **Download widget JSON** or **Copy CSS**. The submission page shows the matching manual installation steps before the source code. @@ -74,13 +87,31 @@ Custom Widget imports detect required bearer, basic, header, or query credential Workshop is powered by [PocketBase](https://pocketbase.io). The production image compiles the current Docusaurus site and serves it together with the API from one origin. The deployment lives in `apps/workshop/` with a Dockerfile, Compose configuration, version-controlled migrations, hooks, a smoke test, and ACT-local workflow instructions. -Run `pnpm docker:workshop` for the PocketBase development service or `pnpm docker:docs` for the built documentation and Workshop site. Both use `apps/workshop/docker-compose.yml` and the persistent `pb_data` volume. Normal start and stop commands retain data; `docker compose -f apps/workshop/docker-compose.yml down --volumes` intentionally recreates the database. +Run `pnpm docker:workshop` for PocketBase on port 8090, then run +`WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs` for Docusaurus on port 3003. `pnpm docker:docs` builds the +combined production layout. Normal start and stop commands retain data; `down --volumes` is an intentional, +irreversible development reset. -Because Workshop is still unreleased, reset an older development volume if it predates the current schema and comments report that their collection context is missing. No compatibility migrations are maintained for these pre-release databases. +GitHub needs only the central callback `https:///api/oauth2-redirect`. Keep PocketBase origins unrestricted +unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and +`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated +links and email use the public host. Configure SMTP for comment, report, and removal notices. -GitHub needs only the central callback `https:///api/oauth2-redirect`. Keep PocketBase origins unrestricted unless every self-hosted Homarr caller is explicitly listed. Configure `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `WORKSHOP_PUBLIC_ORIGIN`, and PocketBase SMTP settings for authentication, comment email, report alerts, and submission-removal notices. +The public URL contract is: -Homarr instances use the server-side `WORKSHOP_API_URL` variable to reach the central API. The Docusaurus site uses `WORKSHOP_URL` when its development server runs separately; the combined production image always uses its own origin. Restart the corresponding process after changing either value because both are read at startup. +| Variable | Purpose | Default | +| ------------------------ | ------------------------------------------------- | ----------------------------- | +| `HOMARR_WEBSITE_URL` | Documentation/site base used by Homarr links | `https://homarr.dev` | +| `WORKSHOP_API_URL` | PocketBase API used by Homarr and standalone docs | `HOMARR_WEBSITE_URL` | +| `WORKSHOP_WEB_URL` | Public Workshop page linked from Homarr | `HOMARR_WEBSITE_URL/workshop` | +| `WORKSHOP_PUBLIC_ORIGIN` | PocketBase-generated links and email | Required in production | +| `PB_ALLOWED_ORIGINS` | PocketBase browser origins | `*` for the central community | + +The deprecated docs-only `WORKSHOP_URL` alias remains for one release; `WORKSHOP_API_URL` takes precedence. Restart +services after changing these values. + +Back up application and PocketBase data before deployment. Publish the combined image by immutable digest, test the +exact digest in staging, rehearse application rollback and PocketBase restore, then promote that same digest. ## Client architecture diff --git a/apps/docs/docs/widgets/custom-api/index.mdx b/apps/docs/docs/widgets/custom-api/index.mdx index b3d1e15ab..62d557b9a 100644 --- a/apps/docs/docs/widgets/custom-api/index.mdx +++ b/apps/docs/docs/widgets/custom-api/index.mdx @@ -38,7 +38,8 @@ Enable the definition before you use it. The definition must also be available t Each named request declares its minimum board permission. This permission does not depend on the HTTP method. -Queries and actions can use GET, POST, PUT, PATCH, or DELETE. The `kind` value controls runtime behavior. +Queries and actions can use GET, POST, PUT, or PATCH. DELETE is valid only for actions so it can never run +automatically while a board loads. The `kind` value controls the remaining runtime behavior. A DELETE request requires full board permission and confirmation. An anonymous viewer cannot run an action. diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index d4518fbaf..0d26722f6 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -1,19 +1,29 @@ import type * as Preset from "@docusaurus/preset-classic"; import type { Config } from "@docusaurus/types"; import { themes as prismThemes } from "prism-react-renderer"; +import { resolveHomarrUrlConfig } from "@homarr/workshop/schema"; const a11yEmoji = require("@fec/remark-a11y-emoji"); +const deprecatedWorkshopUrl = process.env.WORKSHOP_URL; +if (deprecatedWorkshopUrl && !process.env.WORKSHOP_API_URL) { + console.warn("WORKSHOP_URL is deprecated; use WORKSHOP_API_URL instead."); +} + +const publicUrls = resolveHomarrUrlConfig({ + homarrWebsiteUrl: process.env.HOMARR_WEBSITE_URL, + workshopApiUrl: process.env.WORKSHOP_API_URL ?? deprecatedWorkshopUrl, +}); const config: Config = { title: "Homarr documentation", tagline: "A simple yet powerful dashboard for your server.", - url: "https://homarr.dev", + url: publicUrls.homarrWebsiteUrl, baseUrl: "/", trailingSlash: undefined, favicon: "img/logo.png", organizationName: "homarr-labs", projectName: "homarr", customFields: { - workshopUrl: process.env.WORKSHOP_URL ?? "", + workshopUrl: publicUrls.workshopApiUrl, }, i18n: { defaultLocale: "en", diff --git a/apps/docs/src/components/custom-widget-code-client.tsx b/apps/docs/src/components/custom-widget-code-client.tsx index ce35fc60a..ff4829738 100644 --- a/apps/docs/src/components/custom-widget-code-client.tsx +++ b/apps/docs/src/components/custom-widget-code-client.tsx @@ -84,7 +84,6 @@ export function BundledCustomWidgetGalleryClient() { const [selectedId, setSelectedId] = useState(BUNDLED_CUSTOM_WIDGETS[0]?.id ?? null); const selected = BUNDLED_CUSTOM_WIDGETS.find(({ id }) => id === selectedId) ?? BUNDLED_CUSTOM_WIDGETS[0]; if (!selected) return null; - const manifest = { ...selected.widget, template: "__HOMARR_TEMPLATE__" }; return ( @@ -103,19 +102,11 @@ export function BundledCustomWidgetGalleryClient() { - diff --git a/apps/docs/src/components/workshop/DetailPage.tsx b/apps/docs/src/components/workshop/DetailPage.tsx index 452534ddc..7d5c3c360 100644 --- a/apps/docs/src/components/workshop/DetailPage.tsx +++ b/apps/docs/src/components/workshop/DetailPage.tsx @@ -4,6 +4,7 @@ import Link from "@docusaurus/Link"; import { useLocation } from "@docusaurus/router"; import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; + import { IconArrowBigDown, IconArrowBigUp, @@ -335,7 +336,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => { if (!submission) return; try { const updated = await backend.toggleOutdated(submission.id, !submission.outdated); - setSubmission({ ...submission, outdated: updated.outdated }); + setSubmission(updated); setError(null); toast.success(updated.outdated ? "Submission marked as outdated" : "Submission marked as current"); } catch (caught) { @@ -453,7 +454,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => { setReportOpen(false); setReportExplanation(""); setError(null); - toast.success("Report submitted", { description: "A Workshop administrator can now review it." }); + toast.success("Report submitted", { description: "A Workshop moderator can now review it." }); } catch (caught) { setReportError(errorMessage(caught, "Failed to submit report")); } finally { @@ -863,7 +864,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => { Report submission - Only Workshop administrators can review report details and reporter identities. + Only Workshop moderators can review report details and reporter identities. {reportError && ( diff --git a/apps/docs/src/components/workshop/WorkshopAdmin.tsx b/apps/docs/src/components/workshop/WorkshopAdmin.tsx index c5132e3fb..ec4428115 100644 --- a/apps/docs/src/components/workshop/WorkshopAdmin.tsx +++ b/apps/docs/src/components/workshop/WorkshopAdmin.tsx @@ -52,7 +52,7 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) { setSubmissions(nextSubmissions); setError(""); } catch (cause) { - setError(cause instanceof Error ? cause.message : "Unable to load Workshop administration"); + setError(cause instanceof Error ? cause.message : "Unable to load Workshop moderation"); } finally { setLoading(false); } @@ -118,7 +118,7 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) { Restricted area -

Workshop administration

+

Workshop moderation

Review reports and remove unsafe or outdated submissions. PocketBase rules enforce every action.

@@ -145,14 +145,14 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) { {user && !user.isAdmin && ( - Administrator access required + Workshop moderator access required This GitHub account cannot access Workshop moderation. )} {error && ( - Workshop administration failed + Workshop moderation failed {error} )} diff --git a/apps/docs/src/pages/workshop/admin/index.tsx b/apps/docs/src/pages/workshop/admin/index.tsx index 5d05480b2..a8baf5bcf 100644 --- a/apps/docs/src/pages/workshop/admin/index.tsx +++ b/apps/docs/src/pages/workshop/admin/index.tsx @@ -3,12 +3,11 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import { WorkshopAdmin } from "@site/src/components/workshop/WorkshopAdmin"; - export default function WorkshopAdminRoute() { const { siteConfig } = useDocusaurusContext(); const workshopUrl = (siteConfig.customFields?.workshopUrl as string | undefined) ?? ""; return ( - + ); diff --git a/apps/nextjs/README.md b/apps/nextjs/README.md index 437b0b8e9..6cecb43d3 100644 --- a/apps/nextjs/README.md +++ b/apps/nextjs/README.md @@ -1,28 +1,48 @@ -# Create T3 App +# Homarr web application -This is a [T3 Stack](https://create.t3.gg/) project bootstrapped with `create-t3-app`. +This workspace contains Homarr's Next.js application. It consumes the shared API, authentication, database, board, +widget, translation, and UI packages from this monorepo. -## What's next? How do I make an app with this? +Run commands from the repository root: -We try to keep this project as simple as possible, so you can start with just the scaffolding we set up for you, and add additional things later when they become necessary. +```sh +pnpm install --frozen-lockfile +cp .env.example .env +``` -If you are not familiar with the different technologies used in this project, please refer to the respective docs. If you still are in the wind, please join our [Discord](https://t3.gg/discord) and ask for help. +Keep infrastructure running in the first terminal: -- [Next.js](https://nextjs.org) -- [NextAuth.js](https://next-auth.js.org) -- [Drizzle](https://orm.drizzle.team) -- [Tailwind CSS](https://tailwindcss.com) -- [tRPC](https://trpc.io) +```sh +pnpm docker:dev +``` -## Learn More +Apply migrations and start the application in a second terminal: -To learn more about the [T3 Stack](https://create.t3.gg/), take a look at the following resources: +```sh +pnpm db:migration:sqlite:run +pnpm dev +``` -- [Documentation](https://create.t3.gg/) -- [Learn the T3 Stack](https://create.t3.gg/en/faq#what-learning-resources-are-currently-available) — Check out these awesome tutorials +The app listens on `http://127.0.0.1:3000`. Configure an absolute writable `DB_URL`, `AUTH_SECRET`, and +`SECRET_ENCRYPTION_KEY` in `.env` before first startup. -You can check out the [create-t3-app GitHub repository](https://github.com/t3-oss/create-t3-app) — your feedback and contributions are welcome! +To test Custom Widgets against a local Community Workshop, start PocketBase and the docs in separate terminals: -## How do I deploy this? +```sh +pnpm docker:workshop +WORKSHOP_API_URL=http://127.0.0.1:8090 pnpm dev:docs +``` -Follow our deployment guides for [Vercel](https://create.t3.gg/en/deployment/vercel) and [Docker](https://create.t3.gg/en/deployment/docker) for more information. +Then set: + +```dotenv +HOMARR_WEBSITE_URL=http://127.0.0.1:3003 +WORKSHOP_API_URL=http://127.0.0.1:8090 +WORKSHOP_WEB_URL=http://127.0.0.1:3003/workshop +``` + +Server-rendered runtime configuration exposes those URLs to the browser, so changing a production container's +environment does not require rebuilding the Next.js bundle. + +See [`apps/docs/docs/advanced/development/getting-started.mdx`](../docs/docs/advanced/development/getting-started.mdx) +for the full contributor setup. diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_customCss.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_customCss.tsx index 415742176..42e64a3ab 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_customCss.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_customCss.tsx @@ -9,6 +9,7 @@ import "~/styles/prismjs.scss"; import { IconInfoCircle } from "@tabler/icons-react"; import { useForm } from "@homarr/form"; +import { useSession } from "@homarr/auth/client"; import { useI18n, useScopedI18n } from "@homarr/translation/client"; import { WorkshopCssImportButton } from "~/components/workshop/workshop-css-import-button"; @@ -22,6 +23,8 @@ interface Props { } export const CustomCssSettingsContent = ({ board }: Props) => { + const { data: session } = useSession(); + const isAdmin = session?.user.permissions.includes("admin") ?? false; const t = useI18n(); const customCssT = useScopedI18n("board.field.customCss"); const { mutate: savePartialSettings, isPending } = useSavePartialSettingsMutation(board); @@ -48,7 +51,7 @@ export const CustomCssSettingsContent = ({ board }: Props) => { - form.setFieldValue("customCss", css)} /> + {isAdmin ? form.setFieldValue("customCss", css)} /> : } diff --git a/apps/nextjs/src/app/[locale]/custom-widget-configuration/[token]/page.tsx b/apps/nextjs/src/app/[locale]/custom-widget-configuration/[token]/page.tsx index a6cdb5ee0..6485ec80e 100644 --- a/apps/nextjs/src/app/[locale]/custom-widget-configuration/[token]/page.tsx +++ b/apps/nextjs/src/app/[locale]/custom-widget-configuration/[token]/page.tsx @@ -1,4 +1,7 @@ import type { Metadata } from "next"; +import { redirect } from "next/navigation"; + +import { auth } from "@homarr/auth/next"; import { CustomWidgetConfigurationEntry } from "./configuration-entry"; @@ -8,6 +11,8 @@ export const metadata: Metadata = { }; export default async function CustomWidgetConfigurationPage({ params }: { params: Promise<{ token: string }> }) { + const session = await auth(); + if (!session?.user.permissions.includes("admin")) redirect(session ? "/" : "/auth/login"); const { token } = await params; return ; } diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index fba095ce4..d394d9c31 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -24,7 +24,7 @@ import { SettingsProvider } from "@homarr/settings"; import { SpotlightProvider } from "@homarr/spotlight"; import type { SupportedLanguage } from "@homarr/translation"; import { isLocaleRTL, isLocaleSupported } from "@homarr/translation"; -import { WORKSHOP_API_URL } from "@homarr/workshop/schema"; +import { resolveHomarrUrlConfig } from "@homarr/workshop/schema"; import { Analytics } from "~/components/layout/analytics"; import { CrowdinLiveTranslation } from "~/components/layout/crowdin-live-translation"; @@ -54,7 +54,7 @@ export const generateMetadata = async (): Promise => ({ title: "Homarr Dashboard", description: "A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.", - url: "https://homarr.dev", + url: env.HOMARR_WEBSITE_URL, siteName: "Homarr", }, icons: { @@ -89,6 +89,11 @@ export default async function Layout(props: { const serverSettings = await getServerSettingsAsync(db); const colorScheme = await getCurrentColorSchemeAsync(); const direction = isLocaleRTL((await props.params).locale) ? "rtl" : "ltr"; + const publicUrls = resolveHomarrUrlConfig({ + homarrWebsiteUrl: env.HOMARR_WEBSITE_URL, + workshopApiUrl: env.WORKSHOP_API_URL, + workshopWebUrl: env.WORKSHOP_WEB_URL, + }); const StackedProvider = composeWrappers([ (innerProps) => { @@ -141,7 +146,8 @@ export default async function Layout(props: { suppressHydrationWarning > - + + diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx index 2bb767724..5e50b6127 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.spec.tsx @@ -18,7 +18,7 @@ describe("Copy AI prompt", () => { const prompt = buildCustomWidgetAiPrompt(undefined, null, null, "Create a Pokédex"); expect(prompt.length).toBeLessThanOrEqual(12_000); expect(prompt).toContain("exactly one complete `json` fenced block"); - expect(prompt).toContain("one complete `jsx` fenced block"); + expect(prompt).toContain("Put the complete JSX source directly in the `template` string"); }); it("keeps AI actions and section navigation readable on narrow screens", () => { diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx index 66c6f6a77..441ccec03 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx @@ -5,6 +5,7 @@ import { ActionIcon, Menu } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { IconBuildingStore, + IconClipboard, IconCopy, IconDots, IconDownload, @@ -38,7 +39,6 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { const t = useScopedI18n("customWidget"); const { openConfirmModal } = useConfirmModal(); const deleteMutation = clientApi.customWidget.delete.useMutation(); - const deleteLegacyMutation = clientApi.customWidget.deleteLegacy.useMutation(); const duplicateMutation = clientApi.customWidget.duplicate.useMutation(); const utils = clientApi.useUtils(); const [publishOpened, publishControls] = useDisclosure(false); @@ -62,22 +62,37 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { } }; + const queueMigratedWidget = (text: string) => { + const result = parseCustomWidgetClipboardDetailed(text); + if (!result.success) { + showErrorNotification({ + title: t("action.migrate"), + message: formatCustomWidgetImportIssues(result.issues), + }); + return; + } + setMigratedWidget(result.widget); + migrationControls.open(); + }; + + const pasteMigratedWidget = async () => { + try { + queueMigratedWidget(await navigator.clipboard.readText()); + } catch { + showErrorNotification({ + title: t("action.migrate"), + message: t("notification.migrationError"), + }); + } + }; + const handleMigrationFile = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.addEventListener("load", (loadEvent) => { try { - const result = parseCustomWidgetClipboardDetailed(loadEvent.target?.result as string); - if (!result.success) { - showErrorNotification({ - title: t("action.migrate"), - message: formatCustomWidgetImportIssues(result.issues), - }); - return; - } - setMigratedWidget(result.widget); - migrationControls.open(); + queueMigratedWidget(loadEvent.target?.result as string); } catch { showErrorNotification({ title: t("action.migrate"), @@ -146,10 +161,6 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { showErrorNotification({ title: t("action.delete"), message: t("notification.deleteError") }); }, }; - if (widget.migrationRequired) { - deleteLegacyMutation.mutate({ id: widget.id }, callbacks); - return; - } deleteMutation.mutate({ id: widget.id }, callbacks); }, }); @@ -187,6 +198,9 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { void copyMigrationPrompt()} leftSection={}> {t("action.copyMigrationPrompt")} + void pasteMigratedWidget()} leftSection={}> + {t("action.pasteMigration")} + migrationFileInputRef.current?.click()} leftSection={} @@ -196,14 +210,16 @@ export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { )} - } - onClick={handleDelete} - disabled={deleteMutation.isPending || deleteLegacyMutation.isPending} - > - {t("action.delete")} - + {!widget.migrationRequired && ( + } + onClick={handleDelete} + disabled={deleteMutation.isPending} + > + {t("action.delete")} + + )} {widget.valid && } diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx index dd13ce1b4..86fa64951 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx @@ -26,6 +26,9 @@ export function CustomWidgetAiCard(props: AiCardProps) { {t("title")} + + {t("description")} +