diff --git a/apps/docs/docs/management/backup.mdx b/apps/docs/docs/management/backup.mdx index 1c86837a9..7219d7efc 100644 --- a/apps/docs/docs/management/backup.mdx +++ b/apps/docs/docs/management/backup.mdx @@ -22,6 +22,8 @@ Navigate to `Management` → `Tools` → `Backup`. You must have **admin** permi ## Exporting a Backup +![Export database card with the Download Backup action](/img/backup/export-database.png) + 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 | @@ -33,6 +35,8 @@ The backup contains the complete database. It includes Custom Widgets and all ot ## Restoring a Backup +![Restore database card with the backup ZIP drop zone](/img/backup/restore-database.png) + ### 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. diff --git a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx index 0adc7181d..16ba34f9e 100644 --- a/apps/docs/docs/management/custom-widgets/agent-authoring.mdx +++ b/apps/docs/docs/management/custom-widgets/agent-authoring.mdx @@ -130,12 +130,12 @@ The same suites can target the Homarr provider with a Workshop PocketBase token ```sh AI_PROVIDER_BASE_URL=https://homarr.dev/api/ai/v1 \ AI_PROVIDER_API_KEY= \ -AI_PROVIDER_MODEL=homarr/deepseek-v4-flash-latest \ +AI_PROVIDER_MODEL=homarr/model \ pnpm --filter @homarr/custom-widgets test:assistant-ai ``` `AI_PROVIDER_JUDGE_MODEL` can select a separate judge when the configured endpoint offers one. The Homarr provider -offers exactly one model, so generation and review both use `homarr/deepseek-v4-flash-latest`. +offers exactly one model, so generation and review both use `homarr/model`. The assistant test exposes the same lazy skill, live schema, component catalog, targeted component references, validation, preview-query, and create-from-preview tools as the product. It refuses to save a definition that was not @@ -143,8 +143,8 @@ validated exactly or whose preview queries were not all tested. Multi-request fi each documented path and require manual-query parameters. Use `--case=pokedex`, `--case=coinmarketcap-keyless`, `--case=bored-activity`, or `--case=agify-name` for a focused run. -Both suites default generation and harsh review to OpenRouter's `~deepseek/deepseek-v4-flash-latest` route. They save -tool traces, validated manifests, raw reviews, and detailed category scores under +Both suites use the configured provider models for generation and harsh review. They save tool traces, validated +manifests, raw reviews, and detailed category scores under `packages/custom-widgets/.ai-evaluations/`. These live tests are maintainer-invoked rather than CI-required because they consume a configured provider account and exercise a nondeterministic model. The repository's ordinary tests cover the lifecycle enforcement, fixture routing, score calculation, and failure behavior without network access. diff --git a/apps/docs/docs/workshop/homarr-provider.mdx b/apps/docs/docs/workshop/homarr-provider.mdx index c6ef59d43..7b770575f 100644 --- a/apps/docs/docs/workshop/homarr-provider.mdx +++ b/apps/docs/docs/workshop/homarr-provider.mdx @@ -8,15 +8,13 @@ tags: - Community --- -import { AssistantProviderActivity } from "@site/src/components/assistant-provider/assistant-provider-activity"; import { OpenCollectiveLink } from "@site/src/components/assistant-provider/open-collective-link"; # Homarr provider -The Homarr provider gives Workshop users access to **DeepSeek V4 Flash Latest** without an administrator API key. -Each user signs in with GitHub and receives a separate daily allowance. - -![Homarr provider configuration](/img/assistant/homarr-provider.png) +The Homarr provider gives Workshop users access to the **Homarr model** without an administrator API key. The Homarr +team selects a capable model based on quality, reliability, and cost, and can change the upstream model without +changing the user-facing provider. Each user signs in with GitHub and receives a separate daily allowance. ## Set it up @@ -29,25 +27,26 @@ Each user signs in with GitHub and receives a separate daily allowance. ## Daily allowance - Each user starts with **50 request units per UTC day**. -- A model request uses one unit. -- Each completed Homarr tool call uses one additional unit. +- Each model request uses one unit. Agent tool loops can make multiple model requests. - The allowance resets at 00:00 UTC. The battery beside the model selector shows the remaining allowance and reset time. The Workshop server enforces the limit before it sends a request to OpenRouter. - - ## Data and cost The Workshop keeps the OpenRouter API key on its server. It does not send that key to Homarr or the browser. -Prompts, attachments, tool definitions, and tool results are sent to OpenRouter to generate a response. The Workshop -does not store this content. It stores only request status, model, usage, duration, cost, and timestamps. +Prompts, attachments, tool definitions, and tool results are sent through OpenRouter to generate a response. The +Workshop proxy does not retain that content, responses, or per-request telemetry. It stores only the Workshop user, +UTC day, and counters required to enforce per-user and global daily allowances. Requests require OpenRouter routing +with zero data retention and data collection disabled. Homarr does not use this content to train language models. + +We would rather become millionaires in a more ethical manner. The Homarr community pays for these requests. Donations help cover the cost. -Workshop operators can change the upstream, default allowance, and security limits in the +Workshop operators can change the upstream model, per-user allowance, global allowance, and security limits in the [Workshop operator guide](./operator#homarr-provider-proxy). diff --git a/apps/docs/docs/workshop/operator.mdx b/apps/docs/docs/workshop/operator.mdx index 2b001fe23..da5f8b350 100644 --- a/apps/docs/docs/workshop/operator.mdx +++ b/apps/docs/docs/workshop/operator.mdx @@ -37,16 +37,18 @@ store duplicate profile metadata or avatar files. Configure SMTP for comment, re The public URL contract and Workshop-only server configuration are: -| 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 | -| `OPENROUTER_API_KEY` | Server-only key for the Homarr provider proxy | Provider disabled when empty | -| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Initial per-user daily provider allowance | `50` | -| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible proxy upstream | `https://openrouter.ai/api/v1` | +| 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 | +| `OPENROUTER_API_KEY` | Server-only key for the Homarr provider proxy | Provider disabled when empty | +| `HOMARR_AI_DAILY_REQUEST_LIMIT` | Per-user daily provider allowance | `50` | +| `HOMARR_AI_GLOBAL_DAILY_REQUEST_LIMIT` | Shared daily ceiling across all users | `10000` | +| `HOMARR_AI_OPENROUTER_BASE_URL` | OpenRouter-compatible proxy upstream | `https://openrouter.ai/api/v1` | +| `HOMARR_AI_OPENROUTER_MODEL` | Server-selected upstream model | Selected by the Homarr team | All public URL variables reject non-HTTP protocols, embedded credentials, query strings, and fragments, and normalize trailing slashes when Homarr or Docusaurus starts. The deprecated docs-only `WORKSHOP_URL` alias remains for one @@ -57,13 +59,18 @@ container after changing these values, without rebuilding it. ### Homarr provider proxy The [Homarr provider](./homarr-provider) proxy is part of the Workshop image. `OPENROUTER_API_KEY` enables the proxy, -`HOMARR_AI_OPENROUTER_BASE_URL` selects its OpenRouter-compatible upstream, and `HOMARR_AI_DAILY_REQUEST_LIMIT` sets the -initial limit stored for each new user. Existing users keep their assigned limits. The upstream URL cannot contain -embedded credentials, a query, or a fragment. Use HTTPS for any upstream outside a trusted private network. +`HOMARR_AI_OPENROUTER_BASE_URL` selects its OpenRouter-compatible upstream, `HOMARR_AI_OPENROUTER_MODEL` selects the +private upstream model behind the stable `homarr/model` alias, and the two allowance variables provide per-user and +shared cost ceilings. Lowering either allowance takes effect for existing users. The upstream URL cannot contain +embedded credentials, a query, or a fragment and must use HTTPS. Tests can explicitly opt into an insecure local mock +with `HOMARR_AI_ALLOW_INSECURE_UPSTREAM=true`; do not use that setting with a real API key. -Run `pnpm test:workshop` to test authentication, streaming, UTC reset, atomic quota enforcement, failed-request -accounting, input and output controls, tool-call accounting, private records, public activity, realtime delivery, and -migration rollback. +The proxy overwrites client routing and generation limits. It requires OpenRouter zero-data-retention routing, denies +data-collecting providers, limits responses to 32,768 completion tokens, and does not retain prompts, responses, or +per-request telemetry. Keep an OpenRouter account-level credit limit as the final operational backstop. + +Run `pnpm test:workshop` to test authentication, streaming, UTC reset, atomic per-user and global quota enforcement, +failed-request accounting, input and output controls, privacy routing, absence of request logs, and migration rollback. The Custom Widget AI tests can use this proxy. Set `AI_PROVIDER_BASE_URL` to the `/api/ai/v1` endpoint and `AI_PROVIDER_API_KEY` to a temporary Workshop user token. See diff --git a/apps/docs/docusaurus.config.ts b/apps/docs/docusaurus.config.ts index 6148de793..cf7dcc354 100644 --- a/apps/docs/docusaurus.config.ts +++ b/apps/docs/docusaurus.config.ts @@ -362,7 +362,7 @@ html[data-workshop-detail-loading] #workshop-detail-route-fallback { box-sizing: border-box; color: #4b5563; display: flex; - font: 500 0.875rem/1.5 Inter, system-ui, sans-serif; + font: 500 0.875rem/1.5 "Work Sans", system-ui, sans-serif; justify-content: center; min-height: 100vh; padding: 2rem; diff --git a/apps/docs/src/components/assistant-provider/assistant-provider-activity.module.css b/apps/docs/src/components/assistant-provider/assistant-provider-activity.module.css deleted file mode 100644 index 6b61c2f26..000000000 --- a/apps/docs/src/components/assistant-provider/assistant-provider-activity.module.css +++ /dev/null @@ -1,165 +0,0 @@ -.root { - min-width: 0; - overflow: hidden; - border: 1px solid var(--ifm-color-emphasis-300); - border-radius: 12px; - background: var(--ifm-background-surface-color); -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; - padding: 0.8rem 1rem; - border-bottom: 1px solid var(--ifm-color-emphasis-300); -} - -.title { - margin: 0; - font-size: 0.95rem; - font-weight: 700; -} - -.live { - display: inline-flex; - align-items: center; - gap: 0.38rem; - color: var(--ifm-color-emphasis-700); - font-size: 0.75rem; - font-weight: 650; -} - -.beep { - width: 0.5rem; - height: 0.5rem; - border-radius: 50%; - background: var(--ifm-color-emphasis-500); -} - -.live[data-connected] .beep { - background: #2f9e44; - box-shadow: 0 0 0 0 rgb(47 158 68 / 35%); - animation: live-beep 2.2s ease-out infinite; -} - -.tableWrap { - overflow-x: auto; -} - -.table { - width: 100%; - min-width: 36rem; - margin: 0; - border-collapse: collapse; - font-size: 0.8rem; -} - -.table th, -.table td { - padding: 0.58rem 0.75rem; - border: 0; - border-bottom: 1px solid var(--ifm-color-emphasis-200); - text-align: start; - white-space: nowrap; -} - -.table th { - color: var(--ifm-color-emphasis-700); - font-size: 0.72rem; - font-weight: 650; -} - -.table tbody tr:last-child td { - border-bottom: 0; -} - -.status { - display: inline-flex; - align-items: center; - gap: 0.35rem; - font-weight: 650; -} - -.status[data-status="processing"] { - color: #e67700; -} - -.status[data-status="completed"] { - color: #2f9e44; -} - -.status[data-status="failed"] { - color: #e03131; -} - -.empty { - padding: 1.2rem; - color: var(--ifm-color-emphasis-700); - font-size: 0.85rem; - text-align: center; -} - -.compact { - width: 100%; - height: 100%; - border: 0; - border-radius: inherit; - background: transparent; -} - -.compact .header { - padding: 0.38rem 0.55rem; -} - -.compact .title { - font-size: 0.75rem; -} - -.compact .live { - font-size: 0.65rem; -} - -.compact .table { - min-width: 0; - font-size: 0.66rem; -} - -.compact .table th, -.compact .table td { - padding: 0.3rem 0.5rem; -} - -.compact .table th:nth-child(n + 4), -.compact .table td:nth-child(n + 4) { - display: none; -} - -.compact .empty { - padding: 0.7rem; - font-size: 0.7rem; -} - -@keyframes live-beep { - 0% { - box-shadow: 0 0 0 0 rgb(47 158 68 / 35%); - } - - 55%, - 100% { - box-shadow: 0 0 0 0.35rem rgb(47 158 68 / 0%); - } -} - -@media (prefers-reduced-motion: reduce) { - .live[data-connected] .beep { - animation: none; - } -} - -@media (max-width: 40rem) { - .root:not(.compact) .table th:nth-child(5), - .root:not(.compact) .table td:nth-child(5) { - display: none; - } -} diff --git a/apps/docs/src/components/assistant-provider/assistant-provider-activity.tsx b/apps/docs/src/components/assistant-provider/assistant-provider-activity.tsx deleted file mode 100644 index b2ed38bec..000000000 --- a/apps/docs/src/components/assistant-provider/assistant-provider-activity.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { useEffect, useMemo, useState } from "react"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import { IconCheck, IconLoader2, IconX } from "@tabler/icons-react"; -import clsx from "clsx"; - -import type { WorkshopAssistantActivity } from "@homarr/workshop/schema"; -import { workshopAssistantActivitySchema } from "@homarr/workshop/schema"; - -import { getWorkshopBackend } from "@site/src/lib/pocketbase"; -import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config"; - -import styles from "./assistant-provider-activity.module.css"; - -const statusIcon = { - processing: IconLoader2, - completed: IconCheck, - failed: IconX, -}; - -const statusLabel = { - processing: "Processing", - completed: "Completed", - failed: "Failed", -}; - -const activityDate = (value: string) => new Date(value.includes("T") ? value : value.replace(" ", "T")); - -const mergeActivity = (current: WorkshopAssistantActivity[], next: WorkshopAssistantActivity) => - [next, ...current.filter((item) => item.id !== next.id)] - .toSorted((left, right) => activityDate(right.created).getTime() - activityDate(left.created).getTime()) - .slice(0, 10); - -const mergeActivities = (current: WorkshopAssistantActivity[], next: WorkshopAssistantActivity[]) => - next.reduce(mergeActivity, current); - -const getEmptyLabel = (loading: boolean, loadFailed: boolean, connected: boolean) => { - if (loading) return "Loading recent activity…"; - if (loadFailed) return connected ? "Waiting for live provider activity…" : "Recent provider activity is unavailable."; - return "No provider requests yet today."; -}; - -export const AssistantProviderActivity = ({ compact = false }: { compact?: boolean }) => { - const { siteConfig } = useDocusaurusContext(); - const configuredWorkshopUrl = (siteConfig.customFields?.workshopUrl as string | undefined) ?? ""; - const [activities, setActivities] = useState([]); - const [loading, setLoading] = useState(true); - const [loadFailed, setLoadFailed] = useState(false); - const [connected, setConnected] = useState(false); - const workshopUrl = useMemo(() => getRuntimeWorkshopApiUrl(configuredWorkshopUrl), [configuredWorkshopUrl]); - - useEffect(() => { - let active = true; - let unsubscribe: (() => void) | undefined; - const backend = getWorkshopBackend(workshopUrl); - const load = async () => { - try { - const nextUnsubscribe = await backend.pocketBase.collection("assistant_activity").subscribe("*", (event) => { - const parsed = workshopAssistantActivitySchema.safeParse(event.record); - if (!active || !parsed.success) return; - setConnected(true); - setActivities((current) => mergeActivity(current, parsed.data)); - }); - if (active) { - unsubscribe = nextUnsubscribe; - setConnected(true); - } else { - nextUnsubscribe(); - } - } catch { - if (active) setConnected(false); - } - try { - const initial = await backend.listAssistantActivity(10); - if (active) { - setActivities((current) => mergeActivities(current, initial)); - setLoadFailed(false); - } - } catch { - if (active) setLoadFailed(true); - } finally { - if (active) setLoading(false); - } - }; - void load(); - return () => { - active = false; - unsubscribe?.(); - }; - }, [workshopUrl]); - - const visibleActivities = compact ? activities.slice(0, 3) : activities; - - return ( -
-
-

Homarr provider activity

- - - {connected ? "Live" : loading ? "Connecting" : "Offline"} - -
- {visibleActivities.length > 0 ? ( -
- - - - - - - - - - - - {visibleActivities.map((activity) => { - const StatusIcon = statusIcon[activity.status]; - return ( - - - - - - - - ); - })} - -
TimeStatusRequestsTokensDuration
- - - - - {statusLabel[activity.status]} - - {activity.requestUnits.toLocaleString()}{activity.totalTokens > 0 ? activity.totalTokens.toLocaleString() : "–"}{activity.durationMs > 0 ? `${(activity.durationMs / 1000).toFixed(1)} s` : "–"}
-
- ) : ( -
{getEmptyLabel(loading, loadFailed, connected)}
- )} -
- ); -}; diff --git a/apps/docs/src/components/pages/home/hero/hero-cards.tsx b/apps/docs/src/components/pages/home/hero/hero-cards.tsx index a22473e02..a61f8b54e 100644 --- a/apps/docs/src/components/pages/home/hero/hero-cards.tsx +++ b/apps/docs/src/components/pages/home/hero/hero-cards.tsx @@ -3,23 +3,29 @@ import { IconBrandDocker, IconCpu, IconDeviceDesktopAnalytics } from "@tabler/ic import styles from "../../../../pages/index.module.css"; import { AppWidget } from "./widgets/app-widget"; import { ClockWidget } from "./widgets/clock-widget"; -import { AssistantProviderWidget } from "./widgets/assistant-provider-widget"; import { DownloadsWidget } from "./widgets/downloads-widget"; -import { WeatherWidget } from "./widgets/weather-widget"; import { EntityStateWidget } from "./widgets/entity-state-widget"; +import { StockWidget } from "./widgets/stock-widget"; +import { WeatherWidget } from "./widgets/weather-widget"; import { WidgetCard } from "./widgets/card"; export const HeroCards = () => { return ( -
+
+ + + + + - - - - - + + + + + +
); }; @@ -35,11 +41,12 @@ const SystemResourcesWidget = () => (