fix: remove unnecessary demo and docker handling

This commit is contained in:
ajnart committed 2026-08-27 11:21:48 +02:00
1 parent 809eed8ef9
commit a935fd78fb
12 files changed
+16 -159

No files matched your search

@@ -36,7 +36,7 @@ pnpm cli harness setup feature-board
The command builds `homarr:feature-board`, starts a container with a persistent data volume, enables demo mode, and keeps demo mutations writable (`DEMO_READ_ONLY=false`). Docker assigns a free host port by default; use the URL printed by the command or query it later:
The demo board includes every widget kind. Integration-backed widgets use the bundled mock integration; media widgets use bundled same-origin demo assets.
The demo board includes every widget kind. Integration-backed widgets use the bundled mock integration; media widgets use public stock assets.
```sh
pnpm cli harness port feature-board
-4
View File
@@ -144,10 +144,6 @@ Docker or Podman endpoints from targeting the wrong service.
Docker environment, or pass one or more IDs returned by `docker.getEndpoints` to query only those Dockerode clients. The
Docker widget uses the same behavior for its environment selector.
`docker.refreshInventory` always refreshes the current replica and returns `scope: "published"` when Redis accepts the
non-persistent refresh event, or `scope: "local"` when publication fails. Published scope does not confirm that every replica
processed the event. A local result is partial success: retry after restoring Redis to notify the other replicas.
## Partial integration failures
Multi-integration widget queries keep data from integrations that remain available. Calendar and media-server results include
@@ -41,7 +41,7 @@ import type { UrlTemplateMode } from "@homarr/definitions";
import { getIntegrationName, invariantTechnicalLabels } from "@homarr/definitions";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr } from "@homarr/modals-collection";
import { showErrorNotification, showWarningNotification } from "@homarr/notifications";
import { showErrorNotification } from "@homarr/notifications";
import { normalizeServiceUrl, ServiceUrlTemplate } from "@homarr/onboarding";
import { useI18n } from "@homarr/translation/client";
import { Link } from "@homarr/ui";
@@ -69,18 +69,12 @@ export const DockerReconciliation = ({ defaultServerOrigin }: { defaultServerOri
const reconciliation = clientApi.docker.reconcileServices.useQuery();
const health = clientApi.docker.getServiceHealth.useQuery(undefined, { enabled: isOpen });
const refreshInventory = clientApi.docker.refreshInventory.useMutation({
async onSuccess(result) {
async onSuccess() {
await Promise.all([
utils.docker.getContainers.invalidate(),
utils.docker.reconcileServices.invalidate(),
utils.docker.getServiceHealth.invalidate(),
]);
if (result.scope === "local") {
showWarningNotification({
title: tDockerAction("refresh.notification.warning.title"),
message: tDockerAction("refresh.notification.warning.message"),
});
}
},
onError() {
showErrorNotification({
@@ -25,7 +25,7 @@ import type { ContainerState, DockerEndpointCapability } from "@homarr/docker";
import { containerStateColorMap, cpuUsageColor, memoryUsageColor, safeValue } from "@homarr/docker/shared";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr, useDockerContainerRemovalConfirmation } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification, showWarningNotification } from "@homarr/notifications";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { ScopedTranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { TablerIcon } from "@homarr/ui";
@@ -199,19 +199,12 @@ export function DockerTable({ initialData }: DockerTableProps) {
refetchOnMount: false,
});
const refreshInventory = clientApi.docker.refreshInventory.useMutation({
async onSuccess(result) {
async onSuccess() {
await Promise.all([
utils.docker.getContainers.invalidate(),
utils.docker.reconcileServices.invalidate(),
utils.docker.getServiceHealth.invalidate(),
]);
if (result.scope === "local") {
showWarningNotification({
title: tDocker("action.refresh.notification.warning.title"),
message: tDocker("action.refresh.notification.warning.message"),
});
return;
}
showSuccessNotification({
title: tDocker("action.refresh.notification.success.title"),
message: tDocker("action.refresh.notification.success.message"),
@@ -1,56 +0,0 @@
const overview = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
overflow: hidden;
color: #f8f9fa;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background:
radial-gradient(circle at 18% 15%, rgba(116, 143, 252, .32), transparent 36%),
radial-gradient(circle at 82% 80%, rgba(59, 201, 219, .22), transparent 42%),
#15121f;
}
main { width: min(86%, 34rem); text-align: center; }
.mark {
width: 4rem;
height: 4rem;
margin: 0 auto 1rem;
display: grid;
place-items: center;
border-radius: 1.25rem;
font-size: 1.75rem;
font-weight: 900;
background: linear-gradient(135deg, #748ffc, #3bc9db);
box-shadow: 0 1rem 3rem rgba(116, 143, 252, .34);
}
h1 { margin: 0; font-size: clamp(1.5rem, 8vw, 2.5rem); letter-spacing: -.04em; }
p { margin: .65rem auto 0; max-width: 28rem; color: #c1c2c5; line-height: 1.55; }
.signals { display: flex; justify-content: center; gap: .55rem; margin-top: 1.25rem; flex-wrap: wrap; }
span { padding: .38rem .7rem; border: 1px solid rgba(255,255,255,.13); border-radius: 999px; background: rgba(255,255,255,.06); font-size: .75rem; }
</style>
</head>
<body>
<main>
<div class="mark">H</div>
<h1>Your home, at a glance.</h1>
<p>Bring services, media, infrastructure, notes and everyday tools together on one practical dashboard.</p>
<div class="signals"><span>59 widgets</span><span>Mock data included</span><span>Built for self-hosters</span></div>
</main>
</body>
</html>`;
export const GET = () =>
new Response(overview, {
headers: {
"Cache-Control": "public, max-age=3600",
"Content-Type": "text/html; charset=utf-8",
},
});
@@ -1 +0,0 @@
export { GET } from "../route";
@@ -1,10 +0,0 @@
const videoBase64 =
"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 Line truncated
export const GET = () =>
new Response(Buffer.from(videoBase64, "base64"), {
headers: {
"Cache-Control": "public, max-age=3600",
"Content-Type": "video/mp4",
},
});
@@ -4,8 +4,6 @@ import { z } from "zod/v4";
import type { ContainerState, DockerContainerTarget, Port } from "@homarr/docker";
import { DockerSingleton } from "@homarr/docker";
import { createLogger } from "@homarr/core/infrastructure/logs";
import { createSubPubChannel } from "@homarr/redis";
import {
dockerContainersRequestHandler,
findDockerContainerAsync,
@@ -24,34 +22,6 @@ const dockerContainerTargetSchema = z.object({
id: z.string().min(1),
});
const dockerContainerTargetsSchema = z.array(dockerContainerTargetSchema).min(1).max(100);
const logger = createLogger({ module: "dockerRouter" });
const resetDockerInventory = () => {
DockerSingleton.reset();
dockerContainersRequestHandler.invalidateCache();
};
const dockerInventoryRefreshChannel = createSubPubChannel<{ requestedAt: number }>("docker-inventory-refresh", {
persist: false,
});
const isRedisDisabled = () => Boolean(process.env.CI) || Boolean(process.env.DISABLE_REDIS_LOGS);
try {
dockerInventoryRefreshChannel.subscribe(resetDockerInventory);
} catch (error) {
if (!isRedisDisabled()) throw error;
}
const publishDockerInventoryRefreshAsync = async () => {
try {
await dockerInventoryRefreshChannel.publishAsync({ requestedAt: Date.now() });
return true;
} catch (error) {
if (!isRedisDisabled()) {
logger.warn(new Error("Failed to publish Docker inventory refresh", { cause: error }));
}
return false;
}
};
const normalizeGetContainersInput = (input: { endpointIds?: string[] } | undefined) => {
if (!input?.endpointIds || input.endpointIds.length === 0) return {};
@@ -87,15 +57,13 @@ export const dockerRouter = createTRPCRouter({
mcp: {
enabled: true,
description:
"Invalidate the local Docker inventory and publish a non-persistent Redis refresh event. Returns scope 'published' when Redis accepts the event or 'local' when only this replica is refreshed. Requires admin permission.",
"Invalidate the cached Docker inventory so the next Docker query performs a fresh user-requested discovery. Requires admin permission.",
},
})
.concat(dockerMiddleware())
.mutation(async () => {
resetDockerInventory();
const propagated = await publishDockerInventoryRefreshAsync();
if (propagated) return { scope: "published" as const };
return { scope: "local" as const };
.mutation(() => {
DockerSingleton.reset();
dockerContainersRequestHandler.invalidateCache();
}),
getContainers: permissionRequiredProcedure
.requiresPermission("admin")
@@ -15,17 +15,6 @@ const findDockerContainerAsyncMock = vi.hoisted(() => vi.fn());
const hasDockerEndpointCapabilityMock = vi.hoisted(() => vi.fn(() => true));
const invalidateDockerCacheMock = vi.hoisted(() => vi.fn());
const resetDockerSingletonMock = vi.hoisted(() => vi.fn());
const dockerInventoryRefreshChannelMock = vi.hoisted(() => {
let subscriber: (() => void) | undefined;
return {
publishAsync: vi.fn(async () => undefined),
subscribe: vi.fn((callback: () => void) => {
subscriber = callback;
return () => undefined;
}),
trigger: () => subscriber?.(),
};
});
const getDockerDataAsyncMock = vi.hoisted(() =>
vi.fn<
() => Promise<{
@@ -77,9 +66,6 @@ vi.mock("@homarr/docker", () => ({
reset: resetDockerSingletonMock,
},
}));
vi.mock("@homarr/redis", () => ({
createSubPubChannel: () => dockerInventoryRefreshChannelMock,
}));
vi.mock("@homarr/request-handler/docker", () => ({
findDockerContainerAsync: findDockerContainerAsyncMock,
hasDockerEndpointCapability: hasDockerEndpointCapabilityMock,
@@ -517,12 +503,6 @@ test("invalidates the shared Docker inventory on explicit refresh", async () =>
expect(invalidateDockerCacheMock).toHaveBeenCalledOnce();
expect(resetDockerSingletonMock).toHaveBeenCalledOnce();
expect(dockerInventoryRefreshChannelMock.publishAsync).toHaveBeenCalledOnce();
dockerInventoryRefreshChannelMock.trigger();
expect(invalidateDockerCacheMock).toHaveBeenCalledTimes(2);
expect(resetDockerSingletonMock).toHaveBeenCalledTimes(2);
});
test("projects persisted service layers without inventing runtime health", async () => {
+5 -2
View File
@@ -805,7 +805,10 @@ const buildDemoWidgets = (appIds: string[], customWidgetDefinitionId: string): D
width: 4,
height: 3,
needsIntegration: false,
options: { embedUrl: "/api/demo-assets/overview", allowScrolling: true },
options: {
embedUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg",
allowScrolling: false,
},
},
{
kind: "video",
@@ -815,7 +818,7 @@ const buildDemoWidgets = (appIds: string[], customWidgetDefinitionId: string): D
height: 3,
needsIntegration: false,
options: {
feedUrl: "/api/demo-assets/video/file.mp4",
feedUrl: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
hasAutoPlay: false,
isMuted: true,
hasControls: true,
-4
View File
@@ -6644,10 +6644,6 @@
"title": "Containers refreshed",
"message": "You are now viewing the most recent data"
},
"warning": {
"title": "Only this Homarr instance was refreshed",
"message": "Redis did not accept the refresh event, so other Homarr instances may still show older Docker data"
},
"error": {
"title": "Containers not refreshed",
"message": "Something went wrong while refreshing the containers"
+2 -8
View File
@@ -42,7 +42,7 @@ import type { ContainerState, DockerEndpointCapability } from "@homarr/docker";
import { containerStateColorMap, cpuUsageColor, memoryUsageColor, safeValue } from "@homarr/docker/shared";
import { useModalAction } from "@homarr/modals";
import { AddDockerAppToHomarr, useDockerContainerRemovalConfirmation } from "@homarr/modals-collection";
import { showErrorNotification, showSuccessNotification, showWarningNotification } from "@homarr/notifications";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import { useI18n } from "@homarr/translation/client";
import { zoomCompensatedSize } from "@homarr/ui";
@@ -285,18 +285,12 @@ export default function DockerWidget({
const data = getUsableWidgetQueryData(containersQuery);
const { isFetching } = containersQuery;
const refreshInventory = clientApi.docker.refreshInventory.useMutation({
async onSuccess(result) {
async onSuccess() {
await Promise.all([
utils.docker.getContainers.invalidate(),
utils.docker.reconcileServices.invalidate(),
utils.docker.getServiceHealth.invalidate(),
]);
if (result.scope === "local") {
showWarningNotification({
title: t("action.refresh.notification.warning.title"),
message: t("action.refresh.notification.warning.message"),
});
}
},
onError() {
showErrorNotification({