diff --git a/AppImage/components/lxc-app-panel.tsx b/AppImage/components/lxc-app-panel.tsx index f42eb317..55db57fa 100644 --- a/AppImage/components/lxc-app-panel.tsx +++ b/AppImage/components/lxc-app-panel.tsx @@ -233,9 +233,8 @@ interface Suggestions { docker_web_links?: DockerWebLinkSuggestion[] } -// An application proven to run inside Docker. Shown beneath the Docker -// detection, never as a registrable app: its update path is the Docker -// image it comes from, and offering a second one would contradict it. +// A registrable application inside Docker. Its image remains the updater; +// registration adds identity and version tracking, not a second update path. interface DockerWorkload { slug: string name: string @@ -636,10 +635,11 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop ) // Detections the user hid and could restore from the Register-a- // different-app panel. Not affected by registration state. - const hiddenDetections = detectedList.filter((d) => dismissedSlugs.has(d.slug)) + const hiddenDetections = [...detectedList, ...(suggestions?.docker_workloads || [])] + .filter((d, index, items) => dismissedSlugs.has(d.slug) && items.findIndex(item => item.slug === d.slug) === index) const searchInstalledApplications = async () => { - const before = new Set(visibleDetected.map((item) => item.slug)) + const before = new Set([...visibleDetected, ...visibleWorkloads].map((item) => item.slug)) setSearchingApplications(true) setDetectionNotice(null) setError(null) @@ -653,6 +653,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop const detected = new Set() if (result.helper_slug) detected.add(result.helper_slug) for (const item of result.extras || []) detected.add(item.slug) + for (const item of result.docker_workloads || []) detected.add(item.slug) const visible = [...detected].filter( (slug) => !registeredSlugs.has(slug) && !dismissedSlugs.has(slug), ) @@ -664,16 +665,6 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop found: true, text: t("vmLxc.appEditor.newApplicationsDetected", { count: newCount }), }) - } else if ((result.docker_workloads || []).length > 0) { - // Saying "nothing found" while the panel is listing containerised - // applications it just read versions from is the one answer that is - // certainly wrong. - setDetectionNotice({ - found: true, - text: t("vmLxc.appEditor.dockerDetectedWithWorkloads", { - count: (result.docker_workloads || []).length, - }), - }) } else { setDetectionNotice({ found: false, @@ -2361,7 +2352,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop size="sm" variant="ghost" onClick={() => dismissDetection(w.slug, w.name)} - aria-label={`Hide ${w.name} detection`} + aria-label={`${t("vmLxc.appEditor.hideButton")}: ${w.name}`} title={t("vmLxc.appEditor.hidePermanentlyTooltip")} className="flex-1 sm:flex-none bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-400 hover:text-red-300" > diff --git a/AppImage/components/virtual-machines.tsx b/AppImage/components/virtual-machines.tsx index cd1304ca..579b7ba8 100644 --- a/AppImage/components/virtual-machines.tsx +++ b/AppImage/components/virtual-machines.tsx @@ -100,6 +100,8 @@ interface LxcAppWatch { // once, through the image. docker_available_version?: string | null docker_update_available?: boolean | null + docker_image_reference?: string | null + docker_binding_error?: string | null ports?: LxcAppPort[] logo_url?: string | null health_path?: string | null @@ -263,7 +265,7 @@ function hasLxcPendingUpdates(vm: VMData): boolean { if (vm.type !== "lxc") return false const osUpdates = vm.update_check?.count ?? 0 const appUpdates = (vm.app_watches || []).filter( - (app) => app.update_available === true && !app.exclude_from_badge, + (app) => app.update_via !== "docker" && app.update_available === true && !app.exclude_from_badge, ).length const dockerRegistered = (vm.app_watches || []).some((app) => app.helper_slug === "docker") const dockerUpdates = dockerRegistered ? (vm.docker_inventory?.update_count ?? 0) : 0 @@ -273,9 +275,9 @@ function hasLxcPendingUpdates(vm: VMData): boolean { // supported choice, and it must not leave the CT looking up to date. // Counted only when the image is not already counted, so one release // stays one number. - const delegatedUpdates = dockerRegistered ? 0 : (vm.app_watches || []).filter( + const delegatedUpdates = dockerRegistered ? 0 : new Set((vm.app_watches || []).filter( (app) => app.update_via === "docker" && app.docker_update_available === true && !app.exclude_from_badge, - ).length + ).map(app => app.docker_image_reference || app.id)).size return osUpdates + appUpdates + dockerUpdates + delegatedUpdates > 0 } @@ -2572,15 +2574,15 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { const getAggregateUpdateCheck = (vm: VMData): LxcUpdateCheck | undefined => { const uc = vm.update_check const appCount = (vm.app_watches || []).filter( - (a) => a.update_available === true && !a.exclude_from_badge, + (a) => a.update_via !== "docker" && a.update_available === true && !a.exclude_from_badge, ).length const dockerRegistered = (vm.app_watches || []).some((a) => a.helper_slug === "docker") const dockerCount = dockerRegistered ? (vm.docker_inventory?.update_count ?? 0) : 0 // See hasLxcPendingUpdates: a delegated app counts only while its image // is not already being counted through the Docker section. - const delegatedCount = dockerRegistered ? 0 : (vm.app_watches || []).filter( + const delegatedCount = dockerRegistered ? 0 : new Set((vm.app_watches || []).filter( (a) => a.update_via === "docker" && a.docker_update_available === true && !a.exclude_from_badge, - ).length + ).map(app => app.docker_image_reference || app.id)).size const osCount = uc?.count ?? 0 const total = osCount + appCount + dockerCount + delegatedCount if (!uc && appCount === 0 && dockerCount === 0 && delegatedCount === 0) return undefined @@ -2590,10 +2592,17 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { // showed a number that explained nothing. Applications and images join // that list under the same names they carry everywhere else. const pendingNames: LxcPackageUpdate[] = [] + const namedImages = new Set() for (const a of vm.app_watches || []) { if (a.exclude_from_badge) continue const isDelegatedPending = a.update_via === "docker" && a.docker_update_available === true && !dockerRegistered + if (a.update_via === "docker" && !isDelegatedPending) continue if (a.update_available !== true && !isDelegatedPending) continue + if (isDelegatedPending) { + const reference = a.docker_image_reference || a.id + if (namedImages.has(reference)) continue + namedImages.add(reference) + } pendingNames.push({ name: a.name || "", current: a.installed_version || "", @@ -5171,6 +5180,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { const hasOsUpdates = osUpdateStatusKnown && !!uc.available const dockerAppWatch = (selectedVM.app_watches || []).find((a) => a.helper_slug === "docker") const dockerRegistered = !!dockerAppWatch + const dockerWorkloadsRegistered = dockerRegistered || (selectedVM.app_watches || []).some(a => a.update_via === "docker") const dockerEngineInstalledVersion = selectedVM.docker_inventory?.engine_version || dockerAppWatch?.installed_version || "" @@ -5187,7 +5197,11 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { || canonicalDockerEngineUpdateCommand const dockerInventoryRefreshing = selectedVM.docker_inventory?.refreshing === true const dockerInventoryAvailable = selectedVM.docker_inventory?.available === true - const dockerImages = dockerRegistered ? (selectedVM.docker_inventory?.images || []) : [] + const delegatedContainers = new Set((selectedVM.app_watches || []) + .filter(a => a.update_via === "docker").map(a => a.container_name)) + const dockerImages = (selectedVM.docker_inventory?.images || []).filter(image => + dockerRegistered || (image.used_by || []).some(name => delegatedContainers.has(name))) + const followedImageReferences = new Set(dockerImages.map(image => image.reference)) const dockerPending = dockerImages.filter((image) => image.update_available === true) const helperExists = !!uc?.app_updater_present && uc?.helper_slug_source === "update_wrapper" const helperName = uc?.helper_app_name || null @@ -5250,7 +5264,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { : scheduleTargets.some((target) => versionTrackedScheduleAppIds.has(target)) const composeProjects = new Map() for (const target of selectedVM.docker_inventory?.compose_projects || []) { - composeProjects.set(`docker-compose:${target.project}`, target) + if (dockerRegistered || dockerImages.some(image => (image.update_targets || []).some(item => item.project === target.project))) { + composeProjects.set(`docker-compose:${target.project}`, target) + } } const standaloneContainers = new Set() for (const image of dockerImages) { @@ -5315,8 +5331,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { logoUrl: webLinkLogo || app.logo_url?.trim() || "", } }) - const dockerUpdateUnits = dockerRegistered - ? (selectedVM.docker_inventory?.update_units || []) + const dockerUpdateUnits = dockerWorkloadsRegistered + ? (selectedVM.docker_inventory?.update_units || []).filter(unit => + dockerRegistered || (unit.references || []).some(ref => followedImageReferences.has(ref))) : [] const bulkActionChoices = [ ...bulkAppChoices, @@ -5513,8 +5530,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { standalone containers. Keep all three in one registered-app section while exposing a concrete action for each target. */} - {dockerRegistered && dockerAppWatch && ( + {dockerWorkloadsRegistered && (
+ {dockerAppWatch && (<>
@@ -5649,8 +5667,9 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => {
)} + )} {selectedVM.docker_inventory && (<> -
+
{t("vmLxc.updates.dockerImagesSubheading")}
@@ -5985,7 +6004,7 @@ const handleDownloadLogs = async (vmid: number, vmName: string) => { {aw.name}
- {!editing && ( + {!editing && aw.update_via !== "docker" && (