From f83d25d155dfd43d6af5010669b9132bb99b0ef5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 20 May 2026 13:32:34 +0100 Subject: [PATCH] Add Discovery provenance markers --- .../subsystems/frontend-primitives.md | 9 ++++ .../subsystems/performance-and-scalability.md | 4 ++ .../internal/subsystems/unified-resources.md | 4 ++ .../src/components/Discovery/DiscoveryTab.tsx | 50 ++++++++++++------- .../Discovery/__tests__/DiscoveryTab.test.tsx | 2 + .../components/Workloads/GuestDrawer.test.tsx | 2 + .../Workloads/GuestDrawerOverview.tsx | 12 +++-- .../shared/DiscoveryProvenanceMarker.tsx | 37 ++++++++++++++ .../shared/WebInterfaceUrlField.tsx | 11 ++-- .../__tests__/WebInterfaceUrlField.test.tsx | 3 ++ .../__tests__/discoveryPresentation.test.ts | 8 +++ .../frontendResourceTypeBoundaries.test.ts | 7 +++ .../src/utils/discoveryPresentation.ts | 19 +++++++ 13 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index db7a50951..444261e80 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1400,6 +1400,11 @@ AI runtime. "Observed by Discovery" context and pass suggested URLs into the shared web-interface field. Persisted/manual web-interface metadata remains the only row-link source until the operator explicitly adopts a suggested URL. + Discovery-sourced values rendered outside the Discovery tab must carry the + shared compact provenance marker from + `frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx`, so + operators can distinguish opt-in Discovery context from API-owned resource + facts without reading a drawer-specific explanation. ## Current State @@ -3158,6 +3163,10 @@ surfacing "Unknown" rows or zero-confidence noise. Manual/persisted web-interface URLs still win: Discovery suggestions may be copied, opened, or adopted through the shared `WebInterfaceUrlField`, but they must not silently replace metadata or make row-name links active until the operator saves them. +The visible provenance marker for those values is the shared +`DiscoveryProvenanceMarker`; local surfaces may choose the labelled or +icon-only variant, but must not invent alternate Discovery badges or hide the +source on compact cards. That same settings-shell boundary now also owns the shared settings presentation helpers that those panels consume. `frontend-modern/src/utils/systemSettingsPresentation.ts` is the canonical owner for shared system-settings presets, summaries, and diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index cc7a95a62..2d49e50a7 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1581,3 +1581,7 @@ dependency, row-time endpoint probe, or auto-save mutation. Suggested URLs remain copy/open/adopt affordances only; persisting them as a workload link stays a user action through the existing Web Interface URL field. +The compact provenance marker for those values is presentation-only. It +may identify already-loaded Discovery fields, but must not introduce +additional discovery fetches, provider lookups, endpoint probes, or +per-row layout measurement on the Workloads hot path. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index acbf95613..d6fe3148a 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1696,6 +1696,10 @@ continue to use the operator-saved web-interface URL. Version, config-path, port, and endpoint facts surfaced outside the Discovery sub-tab must be labelled as Discovery-observed so API-owned resource facts and command-derived facts remain distinguishable. +That label must be visible through the shared Discovery provenance marker on +compact cards, suggested URL panels, and other out-of-tab Discovery values +rather than buried in helper text or inferred from the tab where the operator +found the value. Command-availability guidance inside that analysis surface must consume the shared `frontend-modern/src/utils/discoveryPresentation.ts` settings targets and scan-error copy instead of hard-coding legacy settings labels such as diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index f48c67405..5e4220a69 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -25,6 +25,7 @@ import { getDiscoverySuggestedURLHeadingClass, getDiscoverySuggestedURLTextClass, } from '@/utils/discoveryPresentation'; +import { DiscoveryProvenanceMarker } from '@/components/shared/DiscoveryProvenanceMarker'; import { DISCOVERY_ANALYSIS_EXPLANATION, DISCOVERY_ANALYSIS_REASONING_LABEL, @@ -661,11 +662,15 @@ export const DiscoveryTab: Component = (props) => {
-

- {d().service_name || 'Unknown Service'} -

+
+

+ {d().service_name || 'Unknown Service'} +

+ +
+ Version {d().service_version} = (props) => {

- - {getDiscoveryObservedSourceLabel()} - + Last observed {formatDiscoveryAge(d().updated_at)} @@ -706,7 +709,10 @@ export const DiscoveryTab: Component = (props) => {
-
Web Interface Suggestion
+
+ Web Interface Suggestion + +
= (props) => { {/* CLI Access */}
-
- CLI Access +
+ CLI Access +
= (props) => {
0}>
-
- Config Paths +
+ Config Paths +
@@ -798,8 +806,9 @@ export const DiscoveryTab: Component = (props) => { 0}>
-
- Data Paths +
+ Data Paths +
@@ -817,8 +826,9 @@ export const DiscoveryTab: Component = (props) => { 0}>
-
- Log Paths +
+ Log Paths +
@@ -840,8 +850,9 @@ export const DiscoveryTab: Component = (props) => { {/* Ports */} 0}>
-
- Listening Ports +
+ Listening Ports +
@@ -877,8 +888,9 @@ export const DiscoveryTab: Component = (props) => { {/* Key Facts */} 0}>
-
- Discovered Facts +
+ Discovered Facts +
diff --git a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx index 0b1f48475..bd00392d5 100644 --- a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx +++ b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx @@ -23,6 +23,7 @@ vi.mock('@/utils/clipboard', () => ({ import * as discoveryApi from '@/api/discovery'; import { DiscoveryTab } from '@/components/Discovery/DiscoveryTab'; import { copyToClipboard } from '@/utils/clipboard'; +import { getDiscoveryProvenanceTitle } from '@/utils/discoveryPresentation'; describe('DiscoveryTab', () => { afterEach(() => { @@ -176,6 +177,7 @@ describe('DiscoveryTab', () => { expect(await screen.findByText('Analysis: Cloud (Anthropic)')).toBeInTheDocument(); expect(await screen.findByText('Observed by Discovery')).toBeInTheDocument(); expect(await screen.findByText('Available to Pulse Assistant')).toBeInTheDocument(); + expect(screen.getAllByLabelText(getDiscoveryProvenanceTitle()).length).toBeGreaterThan(1); expect(await screen.findByText('Analysis Reasoning')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Open suggested URL' })).toHaveAttribute( 'href', diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index 81b1455a3..6c1aa5c92 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -5,6 +5,7 @@ import type { WorkloadGuest } from '@/types/workloads'; import type { Memory, Disk, GuestNetworkInterface } from '@/types/api'; import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery'; import { getCanonicalWorkloadId } from '@/utils/workloads'; +import { getDiscoveryProvenanceTitle } from '@/utils/discoveryPresentation'; // ── Mocks ────────────────────────────────────────────────────────────── @@ -226,6 +227,7 @@ describe('GuestDrawer', () => { expect(screen.getByText('web_server')).toBeInTheDocument(); expect(screen.getByText('0.9.0')).toBeInTheDocument(); expect(screen.getByText('95%')).toBeInTheDocument(); + expect(screen.getByLabelText(getDiscoveryProvenanceTitle())).toBeInTheDocument(); expect(screen.getAllByText('http://192.0.2.10:3000').length).toBeGreaterThan(0); expect(screen.getByText('docker exec -it homepage /bin/sh')).toBeInTheDocument(); expect(screen.getByTestId('url-suggested')).toHaveTextContent('http://192.0.2.10:3000'); diff --git a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx index a399b9a89..d875e3502 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx @@ -1,6 +1,7 @@ import { For, Show } from 'solid-js'; import { formatDiscoveryAge } from '@/api/discovery'; +import { DiscoveryProvenanceMarker } from '@/components/shared/DiscoveryProvenanceMarker'; import { WebInterfaceUrlField } from '@/components/shared/WebInterfaceUrlField'; import type { DiscoveryIdentifiedSummary } from '@/utils/discoveryPresentation'; import { formatBytes, formatUptime } from '@/utils/format'; @@ -45,10 +46,13 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { {(summary) => (
-

- Identified Service -

- +
+

+ Identified Service +

+ +
+ {summary().confidencePercent}
diff --git a/frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx b/frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx new file mode 100644 index 000000000..8a72153ae --- /dev/null +++ b/frontend-modern/src/components/shared/DiscoveryProvenanceMarker.tsx @@ -0,0 +1,37 @@ +import { Show, type Component } from 'solid-js'; +import ScanSearchIcon from 'lucide-solid/icons/scan-search'; + +import { + getDiscoveryProvenanceBadgeClass, + getDiscoveryProvenanceIconClass, + getDiscoveryProvenanceLabel, + getDiscoveryProvenanceTitle, +} from '@/utils/discoveryPresentation'; + +interface DiscoveryProvenanceMarkerProps { + showLabel?: boolean; + label?: string; + title?: string; + class?: string; + testId?: string; +} + +export const DiscoveryProvenanceMarker: Component = (props) => { + const showLabel = () => props.showLabel !== false; + const title = () => props.title || getDiscoveryProvenanceTitle(); + const label = () => props.label || getDiscoveryProvenanceLabel(); + const className = () => + props.class || + (showLabel() ? getDiscoveryProvenanceBadgeClass() : getDiscoveryProvenanceIconClass()); + + return ( + + + ); +}; + +export default DiscoveryProvenanceMarker; diff --git a/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx b/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx index fe5f335a2..b375d9fb9 100644 --- a/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx +++ b/frontend-modern/src/components/shared/WebInterfaceUrlField.tsx @@ -2,6 +2,7 @@ import { Component, Show } from 'solid-js'; import CheckIcon from 'lucide-solid/icons/check'; import CopyIcon from 'lucide-solid/icons/copy'; import ExternalLinkIcon from 'lucide-solid/icons/external-link'; +import { DiscoveryProvenanceMarker } from './DiscoveryProvenanceMarker'; import { useWebInterfaceUrlFieldState } from './useWebInterfaceUrlFieldState'; import type { WebInterfaceUrlFieldProps } from './webInterfaceUrlFieldModel'; @@ -105,15 +106,19 @@ export const WebInterfaceUrlField: Component = (props
-

{state.suggestedUrlFallback().title}

+
+ {state.suggestedUrlFallback().title} + +

{state.suggestedUrlFallback().description}

-
- {state.normalizedCurrentUrl() ? 'Discovered URL' : 'Suggested URL'} +
+ {state.normalizedCurrentUrl() ? 'Discovered URL' : 'Suggested URL'} +

({ import { AgentMetadataAPI } from '@/api/agentMetadata'; import { WebInterfaceUrlField } from '@/components/shared/WebInterfaceUrlField'; import { copyToClipboard } from '@/utils/clipboard'; +import { getDiscoveryProvenanceTitle } from '@/utils/discoveryPresentation'; describe('WebInterfaceUrlField', () => { afterEach(() => { @@ -119,6 +120,7 @@ describe('WebInterfaceUrlField', () => { expect(await screen.findByText('No suggested URL available')).toBeInTheDocument(); expect(screen.getByText('No management interface could be inferred.')).toBeInTheDocument(); + expect(screen.getByLabelText(getDiscoveryProvenanceTitle())).toBeInTheDocument(); }); it('offers discovered URL copy, open, and adopt actions without saving automatically', async () => { @@ -133,6 +135,7 @@ describe('WebInterfaceUrlField', () => { )); expect(await screen.findByText('Suggested URL')).toBeInTheDocument(); + expect(screen.getByLabelText(getDiscoveryProvenanceTitle())).toBeInTheDocument(); expect(screen.getByText('http://192.0.2.10:8123')).toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Open suggested URL' })).toHaveAttribute( 'href', diff --git a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts index 3800db734..61a416499 100644 --- a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts @@ -6,6 +6,10 @@ import { getDiscoveryInitialEmptyState, getDiscoveryLoadingState, getDiscoveryNoConnectedAgentMessage, + getDiscoveryProvenanceBadgeClass, + getDiscoveryProvenanceIconClass, + getDiscoveryProvenanceLabel, + getDiscoveryProvenanceTitle, getNetworkDiscoveryModePresentation, getDiscoveryNotesEmptyState, getDiscoveryObservedSourceLabel, @@ -60,6 +64,10 @@ describe('discoveryPresentation', () => { expect(getDiscoveryCategoryBadgeClass()).toContain('text-blue-700'); expect(getDiscoverySuggestedURLCardClass()).toContain('bg-blue-50'); expect(getDiscoverySuggestedURLActionClass()).toContain('text-blue-700'); + expect(getDiscoveryProvenanceLabel()).toBe('Discovery'); + expect(getDiscoveryProvenanceTitle()).toContain('opt-in Discovery'); + expect(getDiscoveryProvenanceBadgeClass()).toContain('border-cyan-200'); + expect(getDiscoveryProvenanceIconClass()).toContain('h-4'); }); it('returns canonical discovery empty-state copy', () => { diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 9f6c95bc5..aeb041b58 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -329,6 +329,7 @@ import useUnifiedResourcesSource from '@/hooks/useUnifiedResources.ts?raw'; import useWorkloadsSource from '@/hooks/useWorkloads.ts?raw'; import findingsPanelSource from '@/components/AI/FindingsPanel.tsx?raw'; import discoveryTabSource from '@/components/Discovery/DiscoveryTab.tsx?raw'; +import discoveryProvenanceMarkerSource from '@/components/shared/DiscoveryProvenanceMarker.tsx?raw'; import discoveryPresentationSource from '@/utils/discoveryPresentation.ts?raw'; import mailGatewaySource from '@/components/PMG/MailGateway.tsx?raw'; import pmgInstancePanelSource from '@/components/PMG/PMGInstancePanel.tsx?raw'; @@ -3170,6 +3171,12 @@ describe('frontend resource type boundaries', () => { expect(discoveryPresentationSource).toContain( 'export function getDiscoveryObservedSourceLabel', ); + expect(discoveryPresentationSource).toContain('export function getDiscoveryProvenanceTitle'); + expect(discoveryProvenanceMarkerSource).toContain('getDiscoveryProvenanceTitle'); + expect(discoveryProvenanceMarkerSource).toContain('getDiscoveryProvenanceBadgeClass'); + expect(discoveryTabSource).toContain('DiscoveryProvenanceMarker'); + expect(webInterfaceUrlFieldSource).toContain('DiscoveryProvenanceMarker'); + expect(guestDrawerOverviewSource).toContain('DiscoveryProvenanceMarker'); expect(discoveryPresentationSource).toContain( 'export function getDiscoverySuggestedURLCardClass', ); diff --git a/frontend-modern/src/utils/discoveryPresentation.ts b/frontend-modern/src/utils/discoveryPresentation.ts index a01cd26fc..abe99ee22 100644 --- a/frontend-modern/src/utils/discoveryPresentation.ts +++ b/frontend-modern/src/utils/discoveryPresentation.ts @@ -27,6 +27,9 @@ export interface DiscoveryIdentifiedSummary { } const OBSERVED_SOURCE_LABEL = 'Observed by Discovery'; +const DISCOVERY_PROVENANCE_LABEL = 'Discovery'; +const DISCOVERY_PROVENANCE_TITLE = + 'Surfaced by opt-in Discovery from agent-observed workload context.'; const toSentence = (text?: string | null): string => { const trimmed = (text || '').trim(); @@ -100,6 +103,22 @@ export function getDiscoveryObservedSourceLabel(): string { return OBSERVED_SOURCE_LABEL; } +export function getDiscoveryProvenanceLabel(): string { + return DISCOVERY_PROVENANCE_LABEL; +} + +export function getDiscoveryProvenanceTitle(): string { + return DISCOVERY_PROVENANCE_TITLE; +} + +export function getDiscoveryProvenanceBadgeClass(): string { + return 'inline-flex h-5 shrink-0 items-center gap-1 rounded border border-cyan-200 bg-cyan-50 px-1.5 text-[10px] font-medium leading-none text-cyan-700 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-200'; +} + +export function getDiscoveryProvenanceIconClass(): string { + return 'inline-flex h-4 w-4 shrink-0 items-center justify-center rounded border border-cyan-200 bg-cyan-50 text-cyan-700 dark:border-cyan-800 dark:bg-cyan-950 dark:text-cyan-200'; +} + export function normalizeDiscoverySuggestedUrl(value?: string | null): string | undefined { const trimmed = (value || '').trim(); return trimmed || undefined;