fix(onboarding): address PR review feedback on docker label discovery

Fix app-only continue flow, handle stale label sync, per-layout yOffset,
blank label normalization, updatedAt on source upsert, docs link, and
forceDisableStatus for ping-enabled apps.
This commit is contained in:
Thomas Camlong committed 2026-07-02 10:45:47 +02:00
1 parent cf655b8c8e
commit 2dcd81676c
6 files changed
+51 -20

No files matched your search

@@ -31,7 +31,7 @@ Homarr can read `homarr.*` labels on your containers to pre-populate apps during
## Homepage fallback
When `homarr.name` is absent, Homarr reads `homepage.name`, `homepage.group`, and `homepage.href` if homepage label reading is enabled (default: on).
When `homarr.name` is absent, Homarr reads `homepage.name`, `homepage.group`, `homepage.href`, `homepage.icon`, and `homepage.description` if homepage label reading is enabled (default: on).
## Example
+1 -1
View File
@@ -93,7 +93,7 @@ They are then available in the app list and can be used as normally added apps.
### Configuration
Homarr supports Docker labels for automatic app discovery during onboarding. See [Docker labels](./docker-labels/index.mdx) for the full reference.
Homarr supports Docker labels for automatic app discovery during onboarding. See [Docker labels](../docker-labels/index.mdx) for the full reference.
Currently Homarr supports one label for hiding containers:
@@ -159,14 +159,21 @@ export const InitIntegrations = () => {
const imageApps = selectedApps.filter((app) => app.source !== "label");
if (labelApps.length > 0) {
const labelAppsWithHost = labelApps.filter((app): app is typeof app & { host: string } => Boolean(app.host));
const labelAppsWithHost = labelApps.filter((app): app is (typeof app & { host: string }) => Boolean(app.host));
const missingHostCount = labelApps.length - labelAppsWithHost.length;
if (missingHostCount > 0) {
throw new Error("Some selected Docker label apps are missing host metadata");
}
if (labelAppsWithHost.length > 0) {
await syncLabelApps(
const syncResult = await syncLabelApps(
labelAppsWithHost.map((app) => ({
containerId: app.containerId,
host: app.host,
})),
);
if (syncResult.notFound > 0) {
throw new Error("Some selected Docker label apps were not found");
}
}
}
@@ -384,7 +391,13 @@ export const InitIntegrations = () => {
{t("action.skip")}
</Button>
<Button
onClick={handleContinueToConfig}
onClick={() => {
if (selectedKinds.length === 0) {
void finishSetupAsync();
return;
}
handleContinueToConfig();
}}
disabled={(selectedKinds.length === 0 && selectedAppCount === 0) || undefined}
rightSection={<IconArrowRight size={16} stroke={1.5} />}
suppressHydrationWarning
@@ -175,6 +175,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/deluge"
| "/docs/integrations/docker-hub"
| "/docs/integrations/docker"
| "/docs/integrations/docker-labels"
| "/docs/integrations/emby"
| "/docs/integrations/github-containerregistry"
| "/docs/integrations/github"
@@ -34,7 +34,13 @@ const homepageLabelKeys = {
const integrationKindSet = new Set<string>(integrationKinds);
const widgetKindSet = new Set<string>(widgetKinds);
const readLabel = (labels: Record<string, string>, key: string) => labels[key]?.trim();
const readLabel = (labels: Record<string, string>, key: string) => {
const value = labels[key]?.trim();
if (!value) {
return undefined;
}
return value;
};
const resolveLabelValue = (
labels: Record<string, string>,
@@ -133,21 +133,20 @@ export const syncDiscoveredServicesAsync = async (
if (!existingItem) {
const newItemId = createId();
const yOffset = getNextItemYOffset(
board,
categorySection.id,
firstLayout.id,
itemYOffsetBySection,
pendingItemLayouts,
itemHeight,
);
const newItemLayouts = board.layouts.map((layout) => ({
itemId: newItemId,
sectionId: categorySection.id,
layoutId: layout.id,
xOffset: 0,
yOffset,
yOffset: getNextItemYOffset(
board,
categorySection.id,
layout.id,
itemYOffsetBySection,
pendingItemLayouts,
itemHeight,
),
width: itemWidth,
height: itemHeight,
}));
@@ -232,7 +231,13 @@ export const syncDiscoveredServicesAsync = async (
if (existing) {
await innerDb
.update(table)
.set({ appId: source.appId, boardId: source.boardId, itemId: source.itemId, externalId: source.externalId })
.set({
appId: source.appId,
boardId: source.boardId,
itemId: source.itemId,
externalId: source.externalId,
updatedAt: new Date(),
})
.where(and(eq(dockerAppSources.host, source.host), eq(dockerAppSources.containerId, source.containerId)));
} else {
await innerDb.insert(table).values(source as never);
@@ -254,7 +259,13 @@ export const syncDiscoveredServicesAsync = async (
} catch {
innerDb
.update(dockerAppSources)
.set({ appId: source.appId, boardId: source.boardId, itemId: source.itemId, externalId: source.externalId })
.set({
appId: source.appId,
boardId: source.boardId,
itemId: source.itemId,
externalId: source.externalId,
updatedAt: new Date(),
})
.where(and(eq(dockerAppSources.host, source.host), eq(dockerAppSources.containerId, source.containerId)))
.run();
}
@@ -338,9 +349,9 @@ const buildAppItemOptions = (
showTitle: true,
descriptionDisplayMode: "hidden",
layout: "column",
pingEnabled: service.pingUrl
? true
: !(options.forceDisableStatus ?? false) && (options.enableStatusByDefault ?? true),
pingEnabled:
!(options.forceDisableStatus ?? false) &&
(Boolean(service.pingUrl) || (options.enableStatusByDefault ?? true)),
});
const findOrCreateCategorySection = (