diff --git a/src/frontend/package.json b/src/frontend/package.json index b628105..1c4da67 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "test": "bun test tests", "preview": "vite preview" }, "dependencies": { diff --git a/src/frontend/src/components/infra/database-chip.tsx b/src/frontend/src/components/infra/database-chip.tsx index 103a3cb..2b135e2 100644 --- a/src/frontend/src/components/infra/database-chip.tsx +++ b/src/frontend/src/components/infra/database-chip.tsx @@ -39,6 +39,7 @@ function dbService(item: RegistryItem): Service { image: item.image, registryImage: item.image, source: item.source, + sourceHref: item.source_href, registryUrl: item.url, latestVersion: item.version, hasUpdate: false, diff --git a/src/frontend/src/components/service/service-card.tsx b/src/frontend/src/components/service/service-card.tsx index d46c3da..3a212fb 100644 --- a/src/frontend/src/components/service/service-card.tsx +++ b/src/frontend/src/components/service/service-card.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { ExternalLink, Power, Trash2, ArrowUp, LoaderCircle, Copy, Download, SlidersHorizontal } from 'lucide-react' +import { ExternalLink, Power, Trash2, ArrowUp, LoaderCircle, Copy, Download, SlidersHorizontal, Globe } from 'lucide-react' import { Button } from '@components/ui/button' import { Card, CardContent, CardFooter } from '@components/ui/card' import { statusMeta } from '@components/container/status' @@ -25,6 +25,7 @@ import type { Service } from '@lib/types' import { cn } from '@lib/utils' import { useAsyncAction } from '@lib/hooks/useAsyncAction' import { DatabaseChip } from '@components/infra/database-chip' +import { sourceLink } from '@lib/domain/source-link' import type { DatabaseState } from './service-grid' interface ServiceCardProps { @@ -47,9 +48,36 @@ function errorMessage(error: unknown): string { return error instanceof Error ? error.message : 'Unknown error' } +/** + * The "Web app" chip (contract rev 8.2 §16.5). `href` is resolved SERVER-SIDE + * and is the only thing this component may open — the console must never build + * a URL from service strings. The caller renders it only when `href` is + * non-empty, so a service without a usable web app shows no chip at all. + */ +function WebAppChip({ href, name }: { href: string; name: string }) { + return ( + + + ) +} + export function ServiceCard({ service, db, onOpenConfig }: ServiceCardProps) { const meta = statusMeta(service.status) const installed = service.origin !== 'remote' + // Externally managed containers (e.g. a repo's own debug compose) are not + // Maestro's to run: the console may stop them but must not start, uninstall, + // update or reconfigure them. + const managed = installed && !service.external const running = service.status.toLowerCase() === 'running' || service.status.toLowerCase() === 'starting' || @@ -60,6 +88,9 @@ export function ServiceCard({ service, db, onOpenConfig }: ServiceCardProps) { const shortContainerId = service.id.slice(0, 12) const shortImage = service.image ? service.image.split('/').slice(-2).join('/') : service.name + // Registry `source` is only linked through its server-validated href; an + // unvalidated value renders as inert text (audit N7). + const source = sourceLink(service) const copyId = useAsyncAction(async () => { try { @@ -193,6 +224,15 @@ export function ServiceCard({ service, db, onOpenConfig }: ServiceCardProps) { {latest} )} + {service.external && ( + + external + + )} {running && installed && ( )} - {service.source && ( + {source?.kind === 'link' ? ( Source - )} + ) : source?.kind === 'text' ? ( + + {source.text} + + ) : null} - {service.capabilities.includes('database') && } +
+ {service.capabilities.includes('database') && } + {service.web?.href ? : null} +
- {installed && ( + {managed && ( - ) : installed ? ( + ) : managed ? ( - ) : ( + ) : installed ? null : (