fix: address provider review feedback

This commit is contained in:
ajnart committed 2026-08-12 12:51:15 +02:00
1 parent bffa3e723c
commit c16e00f94c
15 files changed
+154 -95

No files matched your search

+6 -3
View File
@@ -50,7 +50,8 @@ Homarr includes presets for OpenRouter, OpenAI, Claude, Gemini, xAI, Groq, Mistr
LM Studio. Use **Custom endpoint** for another OpenAI-compatible Chat Completions service.
The [Homarr provider](../workshop/homarr-provider) does not require an administrator API key. Each user signs in to the
Community Workshop and receives a separate daily allowance.
Community Workshop and receives a separate daily allowance. A shared Workshop daily quota also applies, so requests
can be rejected after that shared quota is exhausted.
:::info Local model servers
@@ -78,5 +79,7 @@ and delete conversations.
- **A model stops during a tool call:** use a model that supports streaming and structured tool calls.
- **A local endpoint cannot be reached:** check container networking and the endpoint's listen address.
Homarr exposes the same tool set to external clients through the [MCP server](./mcp). Custom Widget authors can find
the full workflow in [Agent authoring](./custom-widgets/agent-authoring).
Homarr exposes its MCP-enabled procedures to external clients through the [MCP server](./mcp). MCP omits
`customWidget_*` tools for non-admin API keys, while the built-in assistant adds browser actions and optional
OpenRouter web search. Custom Widget authors can find the full workflow in
[Agent authoring](./custom-widgets/agent-authoring).
+2 -1
View File
@@ -41,7 +41,8 @@ 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
Restart `pnpm dev:docs` after changing the Docusaurus URL variables. If Homarr is also running, restart its `pnpm dev`
process after changing the Workshop URLs. 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.
-1
View File
@@ -51,7 +51,6 @@
"postcss": "^8.5.16",
"posthog-docusaurus": "^2.0.5",
"prism-react-renderer": "^2.4.1",
"random-word-slugs": "^0.1.7",
"react": "catalog:",
"react-day-picker": "catalog:",
"react-dom": "catalog:",
@@ -1,8 +1,10 @@
import Translate from "@docusaurus/Translate";
import styles from "./open-collective-link.module.css";
export const OpenCollectiveLink = () => (
<a className={styles.link} href="https://opencollective.com/homarr">
<img className={styles.logo} src="/img/opencollective.svg" alt="" aria-hidden />
Donate to Homarr on OpenCollective
<Translate id="workshop.provider.donate">Donate to Homarr on OpenCollective</Translate>
</a>
);
@@ -1,3 +1,4 @@
import { translate } from "@docusaurus/Translate";
import { IconBrandDocker, IconCpu, IconDeviceDesktopAnalytics } from "@tabler/icons-react";
import styles from "../../../../pages/index.module.css";
@@ -30,18 +31,31 @@ export const HeroCards = () => {
);
};
const heroLabels = {
systemResources: translate({ id: "homepage.preview.systemResources", message: "System resources" }),
online: translate({ id: "homepage.preview.online", message: "Online" }),
cpu: translate({ id: "homepage.preview.cpu", message: "CPU" }),
memory: translate({ id: "homepage.preview.memory", message: "Memory" }),
disk: translate({ id: "homepage.preview.disk", message: "Disk" }),
docker: translate({ id: "homepage.preview.docker", message: "Docker" }),
dockerSummary: translate({ id: "homepage.preview.dockerSummary", message: "2 running · 1 failed" }),
healthy: translate({ id: "homepage.preview.healthy", message: "Healthy" }),
running: translate({ id: "homepage.preview.running", message: "Running" }),
exited: translate({ id: "homepage.preview.exited", message: "Exited" }),
};
const SystemResourcesWidget = () => (
<WidgetCard width={2} className={styles.resourcesWidget}>
<div className={styles.widgetHeading}>
<span className={styles.widgetTitle}>
<IconDeviceDesktopAnalytics size={17} aria-hidden="true" />
System resources
{heroLabels.systemResources}
</span>
<span className={styles.onlineStatus}>Online</span>
<span className={styles.onlineStatus}>{heroLabels.online}</span>
</div>
<ResourceMeter icon={<IconCpu size={15} aria-hidden="true" />} label="CPU" value={34} />
<ResourceMeter label="Memory" value={62} />
<ResourceMeter label="Disk" value={98} />
<ResourceMeter icon={<IconCpu size={15} aria-hidden="true" />} label={heroLabels.cpu} value={34} />
<ResourceMeter label={heroLabels.memory} value={62} />
<ResourceMeter label={heroLabels.disk} value={98} />
</WidgetCard>
);
@@ -63,23 +77,20 @@ const DockerWidget = () => (
<div className={styles.widgetHeading}>
<span className={styles.widgetTitle}>
<IconBrandDocker size={18} aria-hidden="true" />
Docker
{heroLabels.docker}
</span>
<span className={styles.dockerCount}>2 running · 1 failed</span>
<span className={styles.dockerCount}>{heroLabels.dockerSummary}</span>
</div>
<div className={styles.containerList}>
{[
["immich", "Healthy"],
["jellyfin", "Running"],
["paperless", "Exited"],
].map(([name, status]) => (
<div className={`${styles.containerRow} ${status === "Exited" ? styles.containerRowFailed : ""}`} key={name}>
{ name: "immich", status: heroLabels.healthy, failed: false },
{ name: "jellyfin", status: heroLabels.running, failed: false },
{ name: "paperless", status: heroLabels.exited, failed: true },
].map(({ name, status, failed }) => (
<div className={`${styles.containerRow} ${failed ? styles.containerRowFailed : ""}`} key={name}>
<span className={styles.containerName}>{name}</span>
<span className={styles.containerStatus}>
<span
className={`${styles.statusDot} ${status === "Exited" ? styles.statusDotFailed : ""}`}
aria-hidden="true"
/>
<span className={`${styles.statusDot} ${failed ? styles.statusDotFailed : ""}`} aria-hidden="true" />
{status}
</span>
</div>
@@ -1,4 +1,5 @@
import Link from "@docusaurus/Link";
import { translate } from "@docusaurus/Translate";
import { IconDownload, IconExternalLink } from "@tabler/icons-react";
import clsx from "clsx";
import styles from "../../../../pages/index.module.css";
@@ -57,7 +58,10 @@ export default function HomeHero() {
</Link>
</div>
</div>
<section className={clsx(styles.heroPreview, "hover-animation")} aria-label="Example Homarr dashboard">
<section
className={clsx(styles.heroPreview, "hover-animation")}
aria-label={translate({ id: "homepage.preview.label", message: "Example Homarr dashboard" })}
>
<HeroCards />
</section>
</div>
@@ -14,7 +14,7 @@ export const AppWidget = ({ name, label, className }: AppWidgetProps) => {
return (
<WidgetCard width={1} className={clsx("app-card text-center", className)}>
<span className={"text-sm font-bold"}>{label ?? app.name}</span>
<img src={app.iconUrl} className="aspect-square scale-[0.6]" alt={`${label ?? app.name} icon`} />
<img src={app.iconUrl} className="aspect-square scale-[0.6]" alt="" />
<div className="absolute bottom-3 right-3 rounded-full bg-green-500 w-2 h-2" aria-hidden="true" />
</WidgetCard>
);
@@ -1,10 +1,15 @@
import { getRndInteger } from "@site/src/tools/math";
import { translate } from "@docusaurus/Translate";
import { useEffect, useState } from "react";
import { CommonWidgetProps, WidgetCard } from "./card";
import clsx from "clsx";
import { generateSlug } from "random-word-slugs";
import styles from "../../../../../pages/index.module.css";
const downloadQueue = ["family-photos.zip", "linux-isos.mkv", "server-backup.zip", "holiday-video.mp4"];
const downloadLabels = {
file: translate({ id: "homepage.preview.file", message: "File" }),
progress: translate({ id: "homepage.preview.progress", message: "Progress" }),
};
export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
const [downloads, setDownloads] = useState<Download[]>([
{ filename: "photos-backup.zip", progress: 72 },
@@ -18,11 +23,15 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
const progressedDownloads = filteredPrev.map((download, index) => ({
...download,
progress: Math.min(100, download.progress + getRndInteger(0, index < 2 ? 16 : 8)),
progress: Math.min(100, download.progress + (index < 2 ? 8 : 4)),
}));
if (progressedDownloads.length < 3 && progressedDownloads.length === prev.length) {
progressedDownloads.push(generateRandomDownload());
const availableDownloads = downloadQueue.filter(
(filename) => !progressedDownloads.some((download) => download.filename === filename),
);
const filename = availableDownloads[0];
if (filename) progressedDownloads.push({ filename, progress: 0 });
}
return progressedDownloads;
@@ -39,8 +48,8 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
<table className="mb-0 w-full h-full text-xs">
<thead className="inline-block w-full">
<tr className="inline-block w-full">
<th className="border-none px-2 py-1 text-start inline-block w-1/2">File</th>
<th className="border-none px-2 py-1 text-start inline-block w-1/2">Progress</th>
<th className="border-none px-2 py-1 text-start inline-block w-1/2">{downloadLabels.file}</th>
<th className="border-none px-2 py-1 text-start inline-block w-1/2">{downloadLabels.progress}</th>
</tr>
</thead>
<tbody>
@@ -48,12 +57,21 @@ export const DownloadsWidget = ({ className }: CommonWidgetProps) => {
return (
<tr key={file.filename}>
<td className="border-none text-nowrap p-2">{file.filename}</td>
<td className="border-none p-2 w-full" aria-label={`${file.progress}% complete`}>
<td
className="border-none p-2 w-full"
aria-label={translate(
{ id: "homepage.preview.downloadComplete", message: "{progress}% complete" },
{ progress: file.progress },
)}
>
<progress
className={styles.downloadProgress}
value={file.progress}
max={100}
aria-label={`${file.filename} download progress`}
aria-label={translate(
{ id: "homepage.preview.downloadProgress", message: "{filename} download progress" },
{ filename: file.filename },
)}
/>
</td>
</tr>
@@ -69,15 +87,3 @@ interface Download {
filename: string;
progress: number;
}
const formats = ["mkv", "mp4"];
const generateRandomDownload = (): Download => {
const randomMovie = generateSlug(2);
const randomFormat = formats[getRndInteger(0, formats.length - 1)];
return {
filename: `${randomMovie}.${randomFormat}`,
progress: 0,
};
};
+1 -1
View File
@@ -1,4 +1,4 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap");
@import "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Work+Sans:wght@400;500;600;700&display=swap";
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
+4 -4
View File
@@ -77,7 +77,7 @@ section {
flex: none;
min-width: 0;
border-radius: 0.75rem;
border: 1px solid color-mix(in srgb, currentColor 12%, transparent) !important;
border: 1px solid color-mix(in srgb, currentcolor 12%, transparent) !important;
color: inherit;
font: inherit;
background: color-mix(in srgb, var(--ifm-navbar-background-color) 76%, transparent) !important;
@@ -154,7 +154,7 @@ section {
height: 0.375rem;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, currentColor 16%, transparent);
background: color-mix(in srgb, currentcolor 16%, transparent);
}
.resourceTrack::-webkit-progress-bar {
@@ -181,7 +181,7 @@ section {
height: 0.5rem;
border: 0;
border-radius: 999px;
background: color-mix(in srgb, currentColor 18%, transparent);
background: color-mix(in srgb, currentcolor 18%, transparent);
}
.downloadProgress::-webkit-progress-bar {
@@ -215,7 +215,7 @@ section {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: color-mix(in srgb, currentColor 82%, transparent);
color: color-mix(in srgb, currentcolor 82%, transparent);
}
.statusDot {
-3
View File
@@ -1,3 +0,0 @@
export function getRndInteger(min: number, max: number) {
return Math.round(Math.random() * (max - min)) + min;
}