From 86abd901f6d663cdd456632921ccb1b2ad614fcd Mon Sep 17 00:00:00 2001 From: Anso Date: Thu, 7 May 2026 23:52:08 -0400 Subject: [PATCH] feat(app-store): show inline port-conflict message on deploy sheet (#984) * feat(app-store): show inline port-conflict message on deploy sheet Replace the hover-only cursor tooltip on the Advanced tab with an inline "in use by " message rendered next to the port input. The pulsating warning dot stays as the live-data indicator. Add a Port-conflict warning rail to the Essentials tab that lists each conflicting default port and the application using it. The rail replaces the "Deploy with defaults" hint while conflicts exist, and its left accent pulses to echo the live-data signal. Update docs/features/app-store.mdx to describe the new visible behavior on both tabs. * docs(app-store): drop docs hunk from this PR The deploy-sheet docs are being rewritten on the docs/v1-refresh branch (PR #966). To avoid a merge conflict between the two PRs, the prose update for the inline port-conflict messaging now lives there as a follow-up commit on top of f0161e3. Keep this PR scoped to the UI change only. --- frontend/src/components/AppStoreView.tsx | 76 +++++++++++++++--------- 1 file changed, 49 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/AppStoreView.tsx b/frontend/src/components/AppStoreView.tsx index 7b037774..a9279dd8 100644 --- a/frontend/src/components/AppStoreView.tsx +++ b/frontend/src/components/AppStoreView.tsx @@ -13,7 +13,6 @@ import { apiFetch } from '@/lib/api'; import { useDeployFeedback } from '@/context/DeployFeedbackContext'; import { useNodes } from '@/context/NodeContext'; import { useAuth } from '@/context/AuthContext'; -import { CursorProvider, CursorContainer, Cursor, CursorFollow } from '@/components/animate-ui/primitives/animate/cursor'; import { CategorySidebar } from '@/components/appstore/CategorySidebar'; import { FeaturedHero } from '@/components/appstore/FeaturedHero'; import { TemplateTile } from '@/components/appstore/TemplateTile'; @@ -252,6 +251,22 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { return [...base].sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0)); }, [filtered, featuredTemplate]); + const essentialsConflicts = useMemo(() => { + if (!selectedTemplate?.ports) return []; + const seen = new Set(); + const conflicts: Array<{ host: string; info: PortInUseInfo }> = []; + for (const p of selectedTemplate.ports) { + const parts = p.split(':'); + if (parts.length < 2) continue; + const host = portVars[p] || parts[0]; + if (!host || seen.has(host)) continue; + seen.add(host); + const info = portsInUse[host]; + if (info) conflicts.push({ host, info }); + } + return conflicts; + }, [selectedTemplate, portVars, portsInUse]); + return (
@@ -427,10 +442,30 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) {

-
- Deploy with defaults: the template's recommended ports, volumes, and environment variables. - Use the Advanced tab to customize. -
+ {essentialsConflicts.length > 0 ? ( +
+ +
+ Port conflict +
+
    + {essentialsConflicts.map(({ host, info }) => ( +
  • + Port {host} is in use by{' '} + {info.stack ?? 'an external app'}. +
  • + ))} +
+

+ Switch to the Advanced tab to remap. +

+
+ ) : ( +
+ Deploy with defaults: the template's recommended ports, volumes, and environment variables. + Use the Advanced tab to customize. +
+ )} )} @@ -454,28 +489,15 @@ export function AppStoreView({ onDeploySuccess }: AppStoreViewProps) { }} className={cn('w-24 text-center font-mono', hostPort && !isValidPort(hostPort) && 'border-destructive')} /> - {conflict ? ( - - - : {parts[1]} - - - -
- - -
- - {conflict.stack !== null - ? <>Port {hostPort} used by {conflict.stack} - : <>Port {hostPort} used by an external app - } - -
-
- - ) : ( - : {parts[1]} + : {parts[1]} + {conflict && ( + <> + + in use by{' '} + {conflict.stack ?? 'an external app'} + + + )}
);