mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Tighten Discovery drawer signal
This commit is contained in:
parent
59417a7da7
commit
ba374da2ee
14 changed files with 389 additions and 55 deletions
|
|
@ -1394,6 +1394,12 @@ AI runtime.
|
|||
`hasValidDiscovery` — so the same record either renders in all
|
||||
surfaces or hides in all surfaces, preventing "Unknown" rows or
|
||||
zero-confidence noise from drifting into peripheral UI.
|
||||
CLI access, confidence fields, and no-URL diagnostics are support
|
||||
metadata; they must not by themselves promote a record into the
|
||||
identified-service summary when the service name, category, version,
|
||||
paths, ports, facts, and suggested URL are all absent or placeholders,
|
||||
including generic workload types such as `service` or `container` and
|
||||
diagnostic facts such as metadata-only status or missing-config errors.
|
||||
Discovery is an opt-in observed-context layer, not an automatic row-link
|
||||
owner. The reducer must carry provenance, observed time, service version,
|
||||
endpoint candidates, and URL-source copy so drawer surfaces can show
|
||||
|
|
@ -2119,6 +2125,10 @@ owned title so feature drawers can place web-interface controls inside a larger
|
|||
access surface without forking the save/remove/runtime behavior. Future
|
||||
web-interface URL work should extend those owners instead of pushing metadata
|
||||
transport or validation back into the shared shell.
|
||||
Missing-suggested-URL diagnostics remain useful only when the operator has no
|
||||
saved or entered URL; once a custom web-interface URL is present, the shared
|
||||
field must suppress "no suggested URL" warnings so Discovery does not make a
|
||||
valid manual endpoint look broken.
|
||||
The shared help icon now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/HelpIcon.tsx` stays the render shell,
|
||||
`frontend-modern/src/components/shared/useHelpIconState.ts` owns open state,
|
||||
|
|
@ -3163,6 +3173,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.
|
||||
No-URL diagnostics and command access hints are not endpoint candidates; they
|
||||
can explain a Discovery result inside Discovery-owned surfaces, but they must
|
||||
not trigger out-of-tab identified-service cards or suggested-URL panels without
|
||||
another meaningful service signal.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -165,6 +165,11 @@ details such as detected version, config/data/log paths, Docker bind mounts,
|
|||
ports, and suggested web URLs may be authored in mock fixtures, but consumers
|
||||
must receive them through the normal Discovery API contract rather than through
|
||||
frontend-only demo data or a monitoring-only side channel.
|
||||
Demo Discovery fixtures must cover the authored estate broadly enough that the
|
||||
default drawer experience demonstrates meaningful service context instead of a
|
||||
majority of unknown placeholder records. Non-HTTP services may publish a clear
|
||||
no-web-interface diagnostic, but fixtures must still identify the service,
|
||||
version, category, and useful operator paths through the normal Discovery API.
|
||||
That same monitoring boundary also owns the escalation callback bridge into the
|
||||
alerts delivery layer. Monitor-owned escalation handling may still publish
|
||||
canonical escalation state to websocket consumers, but notification fan-out
|
||||
|
|
|
|||
|
|
@ -1696,6 +1696,13 @@ That access-side analysis surface still follows the same shell/runtime split as
|
|||
the rest of the drawer: `DiscoveryTab.tsx` owns presentation and disclosures,
|
||||
while `useDiscoveryTabState.ts` owns API fetches, websocket progress, and
|
||||
note/discovery mutations.
|
||||
Discovery validity in that runtime state must defer to the shared
|
||||
`discoveryPresentation.ts` meaningful-context gate. CLI access, confidence, and
|
||||
diagnostics may support an audit trail, but they must not turn placeholder
|
||||
records such as unknown service/category/version with no ports, paths, facts,
|
||||
or suggested URL into a valid identified result. Generic workload types such
|
||||
as `service`, `container`, or `lxc` remain placeholders until Discovery also
|
||||
provides a more specific service signal.
|
||||
Discovery endpoint candidates are support context, not authoritative resource
|
||||
links. Resource and workload drawers may pass observed Discovery URLs into the
|
||||
shared web-interface field with visible provenance, copy/open affordances, and
|
||||
|
|
|
|||
|
|
@ -39,6 +39,63 @@ describe('DiscoveryTab', () => {
|
|||
expect(await screen.findByRole('button', { name: 'Run Discovery Now' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('treats placeholder-only discovery records as unidentified instead of valid results', async () => {
|
||||
vi.mocked(discoveryApi.getDiscovery).mockResolvedValue({
|
||||
id: 'system-container:pve4:152',
|
||||
resource_type: 'system-container',
|
||||
resource_id: '152',
|
||||
target_id: 'pve4',
|
||||
hostname: 'smtp-relay-32',
|
||||
service_type: 'unknown',
|
||||
service_name: 'Unknown Container',
|
||||
service_version: 'unknown',
|
||||
category: 'unknown',
|
||||
cli_access: 'pct exec 152 -- /bin/bash',
|
||||
facts: [
|
||||
{
|
||||
category: 'service',
|
||||
key: 'status',
|
||||
value: 'online',
|
||||
source: 'metadata',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
{
|
||||
category: 'config',
|
||||
key: 'missing_config',
|
||||
value: 'nodes/delly/lxc/152.conf not found on host',
|
||||
source: 'all_commands',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
ports: [],
|
||||
user_notes: '',
|
||||
user_secrets: {},
|
||||
confidence: 0,
|
||||
ai_reasoning: '',
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
updated_at: '2026-05-19T00:00:00Z',
|
||||
scan_duration: 0,
|
||||
suggested_url_diagnostic: 'no host or IP candidate available',
|
||||
});
|
||||
|
||||
render(() => (
|
||||
<DiscoveryTab
|
||||
resourceType="system-container"
|
||||
agentId="pve4"
|
||||
resourceId="152"
|
||||
hostname="smtp-relay-32"
|
||||
/>
|
||||
));
|
||||
|
||||
expect(await screen.findByText('Unknown Service')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Unknown Container')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows a drawer run action and triggers discovery for the current resource', async () => {
|
||||
const discovered: ResourceDiscovery = {
|
||||
id: 'discovery-1',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import {
|
|||
} from '@/api/discovery';
|
||||
import { eventBus } from '@/stores/events';
|
||||
import type { DiscoveryProgress, ResourceType } from '@/types/discovery';
|
||||
import { getDiscoveryNoConnectedAgentMessage } from '@/utils/discoveryPresentation';
|
||||
import {
|
||||
getDiscoveryNoConnectedAgentMessage,
|
||||
hasMeaningfulDiscoveryContext,
|
||||
} from '@/utils/discoveryPresentation';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { toDiscoveryAPIResourceType } from '@/utils/discoveryTarget';
|
||||
|
||||
|
|
@ -262,29 +265,7 @@ export function useDiscoveryTabState(props: DiscoveryTabStateProps) {
|
|||
});
|
||||
|
||||
const hasValidDiscovery = createMemo(() => {
|
||||
const current = discovery();
|
||||
if (!current) return false;
|
||||
|
||||
const hasServiceName = current.service_name && current.service_name.toLowerCase() !== 'unknown';
|
||||
const hasConfidence = typeof current.confidence === 'number' && current.confidence > 0;
|
||||
const hasPorts = current.ports && current.ports.length > 0;
|
||||
const hasFacts = current.facts && current.facts.length > 0;
|
||||
const hasPaths =
|
||||
(current.config_paths && current.config_paths.length > 0) ||
|
||||
(current.data_paths && current.data_paths.length > 0) ||
|
||||
(current.log_paths && current.log_paths.length > 0);
|
||||
const hasCliAccess = Boolean(current.cli_access);
|
||||
const hasSuggestedUrl = Boolean(current.suggested_url || current.suggested_url_diagnostic);
|
||||
|
||||
return Boolean(
|
||||
hasServiceName ||
|
||||
hasConfidence ||
|
||||
hasPorts ||
|
||||
hasFacts ||
|
||||
hasPaths ||
|
||||
hasCliAccess ||
|
||||
hasSuggestedUrl,
|
||||
);
|
||||
return hasMeaningfulDiscoveryContext(discovery());
|
||||
});
|
||||
|
||||
const validDiscovery = createMemo(() =>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { ResourceDiscovery } from '@/types/discovery';
|
||||
import { toDiscoveryConfig } from '@/components/Infrastructure/resourceDetailDiscoveryModel';
|
||||
import { hasMeaningfulDiscoveryContext } from '@/utils/discoveryPresentation';
|
||||
|
||||
const baseResource = (): Resource => ({
|
||||
id: 'host-abcd',
|
||||
|
|
@ -290,3 +292,52 @@ describe('toDiscoveryConfig', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resource drawer discovery promotion', () => {
|
||||
it('does not treat command-only diagnostic records as meaningful resource context', () => {
|
||||
const discovery: ResourceDiscovery = {
|
||||
id: 'system-container:pve4:152',
|
||||
resource_type: 'system-container',
|
||||
resource_id: '152',
|
||||
target_id: 'pve4',
|
||||
hostname: 'smtp-relay-32',
|
||||
service_type: 'unknown',
|
||||
service_name: 'Unknown Container',
|
||||
service_version: 'unknown',
|
||||
category: 'unknown',
|
||||
cli_access: 'pct exec 152 -- /bin/bash',
|
||||
facts: [
|
||||
{
|
||||
category: 'service',
|
||||
key: 'status',
|
||||
value: 'online',
|
||||
source: 'metadata',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
{
|
||||
category: 'config',
|
||||
key: 'missing_config',
|
||||
value: 'nodes/delly/lxc/152.conf not found on host',
|
||||
source: 'all_commands',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
ports: [],
|
||||
user_notes: '',
|
||||
user_secrets: {},
|
||||
confidence: 0,
|
||||
ai_reasoning: 'Discovery commands could not inspect the workload.',
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
updated_at: '2026-05-19T00:00:00Z',
|
||||
scan_duration: 2816,
|
||||
suggested_url_diagnostic: 'no host or IP candidate available',
|
||||
};
|
||||
|
||||
expect(hasMeaningfulDiscoveryContext(discovery)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -243,6 +243,53 @@ describe('GuestDrawer', () => {
|
|||
expect(screen.queryByText('Identified Service')).toBeNull();
|
||||
});
|
||||
|
||||
it('hides low-signal placeholder discovery from the Overview card and URL field', async () => {
|
||||
discoveryApiMocks.getDiscovery.mockResolvedValueOnce({
|
||||
id: 'system-container:pve4:152',
|
||||
resource_type: 'system-container',
|
||||
resource_id: '152',
|
||||
target_id: 'pve4',
|
||||
service_name: '',
|
||||
service_type: 'service',
|
||||
service_version: '',
|
||||
category: 'unknown',
|
||||
confidence: 0,
|
||||
cli_access: 'pct exec 152 -- /bin/bash',
|
||||
ports: [],
|
||||
facts: [
|
||||
{
|
||||
category: 'service',
|
||||
key: 'status',
|
||||
value: 'online',
|
||||
source: 'metadata',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
{
|
||||
category: 'config',
|
||||
key: 'missing_config',
|
||||
value: 'nodes/delly/lxc/152.conf not found on host',
|
||||
source: 'all_commands',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
updated_at: '2026-05-18T09:49:19.049058+01:00',
|
||||
suggested_url_diagnostic: 'no host or IP candidate available',
|
||||
} as unknown as import('@/types/discovery').ResourceDiscovery);
|
||||
|
||||
render(() => <GuestDrawer guest={makeGuest()} onClose={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(discoveryApiMocks.getDiscovery).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.queryByText('Identified Service')).toBeNull();
|
||||
expect(screen.getByTestId('url-suggested-diagnostic')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
it('keeps the passive discovery lookup out of the parent Suspense fallback', () => {
|
||||
discoveryApiMocks.getDiscovery.mockImplementationOnce(
|
||||
() => new Promise<import('@/types/discovery').ResourceDiscovery | null>(() => undefined),
|
||||
|
|
|
|||
|
|
@ -123,6 +123,20 @@ describe('WebInterfaceUrlField', () => {
|
|||
expect(screen.getByLabelText(getDiscoveryProvenanceTitle())).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show missing suggested URL diagnostics when a custom URL already exists', async () => {
|
||||
render(() => (
|
||||
<WebInterfaceUrlField
|
||||
metadataKind="guest"
|
||||
metadataId="guest-1"
|
||||
customUrl="https://198.51.100.100:8080"
|
||||
suggestedUrlDiagnostic="No management interface could be inferred."
|
||||
/>
|
||||
));
|
||||
|
||||
expect(await screen.findByDisplayValue('https://198.51.100.100:8080')).toBeInTheDocument();
|
||||
expect(screen.queryByText('No suggested URL available')).toBeNull();
|
||||
});
|
||||
|
||||
it('offers discovered URL copy, open, and adopt actions without saving automatically', async () => {
|
||||
render(() => (
|
||||
<WebInterfaceUrlField
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export function useWebInterfaceUrlFieldState(props: WebInterfaceUrlFieldProps) {
|
|||
const normalizedSuggestedUrl = createMemo(() => normalizeWebInterfaceUrl(props.suggestedUrl));
|
||||
const showSuggestedDiagnostic = createMemo(() =>
|
||||
shouldShowWebInterfaceSuggestedDiagnostic({
|
||||
currentUrl: currentCustomUrl(),
|
||||
discoveryLoading: props.discoveryLoading,
|
||||
suggestedUrl: props.suggestedUrl,
|
||||
suggestedUrlDiagnostic: props.suggestedUrlDiagnostic,
|
||||
|
|
|
|||
|
|
@ -52,11 +52,13 @@ export function getWebInterfaceTargetLabel(
|
|||
|
||||
export function shouldShowWebInterfaceSuggestedDiagnostic(options: {
|
||||
discoveryLoading?: boolean;
|
||||
currentUrl?: string;
|
||||
suggestedUrl?: string;
|
||||
suggestedUrlDiagnostic?: string;
|
||||
}): boolean {
|
||||
return (
|
||||
!options.discoveryLoading &&
|
||||
!normalizeWebInterfaceUrl(options.currentUrl) &&
|
||||
!normalizeWebInterfaceUrl(options.suggestedUrl) &&
|
||||
Boolean(options.suggestedUrlDiagnostic)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -235,7 +235,24 @@ describe('discoveryPresentation', () => {
|
|||
service_name: 'Unknown',
|
||||
confidence: 0,
|
||||
ports: [],
|
||||
facts: [],
|
||||
facts: [
|
||||
{
|
||||
category: 'service',
|
||||
key: 'status',
|
||||
value: 'online',
|
||||
source: 'metadata',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
{
|
||||
category: 'config',
|
||||
key: 'missing_config',
|
||||
value: 'nodes/delly/lxc/152.conf not found on host',
|
||||
source: 'all_commands',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
|
|
@ -261,11 +278,60 @@ describe('discoveryPresentation', () => {
|
|||
service_name: 'Unknown',
|
||||
confidence: 0,
|
||||
ports: [],
|
||||
facts: [],
|
||||
facts: [
|
||||
{
|
||||
category: 'service',
|
||||
key: 'status',
|
||||
value: 'online',
|
||||
source: 'metadata',
|
||||
confidence: 1,
|
||||
discovered_at: '2026-05-19T00:00:00Z',
|
||||
},
|
||||
],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
} as unknown as ResourceDiscovery),
|
||||
).toBeNull();
|
||||
expect(
|
||||
getDiscoveryIdentifiedSummary({
|
||||
id: 'system-container:pve4:152',
|
||||
resource_type: 'system-container',
|
||||
resource_id: '152',
|
||||
target_id: 'pve4',
|
||||
service_name: 'Unknown Container',
|
||||
service_type: 'unknown',
|
||||
service_version: 'unknown',
|
||||
category: 'unknown',
|
||||
confidence: 0,
|
||||
cli_access: 'pct exec 152 -- /bin/bash',
|
||||
ports: [],
|
||||
facts: [],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
suggested_url_diagnostic: 'no host or IP candidate available',
|
||||
} as unknown as ResourceDiscovery),
|
||||
).toBeNull();
|
||||
expect(
|
||||
getDiscoveryIdentifiedSummary({
|
||||
id: 'system-container:pve4:152',
|
||||
resource_type: 'system-container',
|
||||
resource_id: '152',
|
||||
target_id: 'pve4',
|
||||
service_name: '',
|
||||
service_type: 'service',
|
||||
service_version: '',
|
||||
category: 'unknown',
|
||||
confidence: 0,
|
||||
cli_access: 'pct exec 152 -- /bin/bash',
|
||||
ports: [],
|
||||
facts: [],
|
||||
config_paths: [],
|
||||
data_paths: [],
|
||||
log_paths: [],
|
||||
suggested_url_diagnostic: 'no host or IP candidate available',
|
||||
} as unknown as ResourceDiscovery),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ResourceDiscovery } from '@/types/discovery';
|
||||
import type { DiscoveryFact, ResourceDiscovery } from '@/types/discovery';
|
||||
import {
|
||||
getInfrastructureSettingsLocationLabel,
|
||||
getInfrastructureSettingsTarget,
|
||||
|
|
@ -38,6 +38,87 @@ const toSentence = (text?: string | null): string => {
|
|||
return first.toUpperCase() + trimmed.slice(1);
|
||||
};
|
||||
|
||||
const normalizeDiscoveryToken = (value?: string | null): string => {
|
||||
return (value || '').trim().toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
const isMeaningfulDiscoveryText = (value?: string | null): boolean => {
|
||||
const normalized = normalizeDiscoveryToken(value);
|
||||
if (!normalized) return false;
|
||||
return ![
|
||||
'detected',
|
||||
'n a',
|
||||
'none',
|
||||
'app',
|
||||
'application',
|
||||
'container',
|
||||
'host',
|
||||
'linux',
|
||||
'lxc',
|
||||
'service',
|
||||
'system container',
|
||||
'unknown',
|
||||
'unknown app',
|
||||
'unknown application',
|
||||
'unknown container',
|
||||
'unknown host',
|
||||
'unknown service',
|
||||
'unknown system container',
|
||||
'unknown virtual machine',
|
||||
'unknown vm',
|
||||
'unknown workload',
|
||||
'virtual machine',
|
||||
'vm',
|
||||
'workload',
|
||||
].includes(normalized);
|
||||
};
|
||||
|
||||
const isMeaningfulDiscoveryFact = (fact: DiscoveryFact): boolean => {
|
||||
const key = normalizeDiscoveryToken(fact.key);
|
||||
const value = normalizeDiscoveryToken(fact.value);
|
||||
const source = normalizeDiscoveryToken(fact.source);
|
||||
if (!isMeaningfulDiscoveryText(fact.value)) return false;
|
||||
if (key === 'status' && source === 'metadata') return false;
|
||||
if (key.startsWith('missing') || key.endsWith('missing')) return false;
|
||||
if (
|
||||
value.includes('does not exist') ||
|
||||
value.includes('not found') ||
|
||||
value.includes('failed') ||
|
||||
value.includes('error')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return ['config', 'dependency', 'network', 'port', 'security', 'service', 'version'].includes(
|
||||
normalizeDiscoveryToken(fact.category),
|
||||
);
|
||||
};
|
||||
|
||||
export function hasMeaningfulDiscoveryContext(
|
||||
discovery: ResourceDiscovery | null | undefined,
|
||||
): boolean {
|
||||
if (!discovery) return false;
|
||||
const portCount = Array.isArray(discovery.ports) ? discovery.ports.length : 0;
|
||||
const configPathCount = Array.isArray(discovery.config_paths) ? discovery.config_paths.length : 0;
|
||||
const dataPathCount = Array.isArray(discovery.data_paths) ? discovery.data_paths.length : 0;
|
||||
const logPathCount = Array.isArray(discovery.log_paths) ? discovery.log_paths.length : 0;
|
||||
const hasFacts =
|
||||
Array.isArray(discovery.facts) &&
|
||||
discovery.facts.some((fact) => isMeaningfulDiscoveryFact(fact));
|
||||
const hasPaths = configPathCount + dataPathCount + logPathCount > 0;
|
||||
const hasSuggestedUrl = Boolean(normalizeDiscoverySuggestedUrl(discovery.suggested_url));
|
||||
|
||||
return Boolean(
|
||||
isMeaningfulDiscoveryText(discovery.service_name) ||
|
||||
isMeaningfulDiscoveryText(discovery.service_type) ||
|
||||
isMeaningfulDiscoveryText(discovery.service_version) ||
|
||||
isMeaningfulDiscoveryText(discovery.category) ||
|
||||
portCount > 0 ||
|
||||
hasFacts ||
|
||||
hasPaths ||
|
||||
hasSuggestedUrl,
|
||||
);
|
||||
}
|
||||
|
||||
// getDiscoveryIdentifiedSummary returns a compact presentation object for
|
||||
// surfaces outside the Discovery sub-tab (e.g. the workload drawer overview)
|
||||
// to label a resource with its identified service and endpoint candidates.
|
||||
|
|
@ -48,39 +129,28 @@ export function getDiscoveryIdentifiedSummary(
|
|||
discovery: ResourceDiscovery | null | undefined,
|
||||
): DiscoveryIdentifiedSummary | null {
|
||||
if (!discovery) return null;
|
||||
if (!hasMeaningfulDiscoveryContext(discovery)) return null;
|
||||
const serviceName = (discovery.service_name || '').trim();
|
||||
const hasName = serviceName.length > 0 && serviceName.toLowerCase() !== 'unknown';
|
||||
const hasName = isMeaningfulDiscoveryText(serviceName);
|
||||
const confidence = typeof discovery.confidence === 'number' ? discovery.confidence : 0;
|
||||
const hasConfidence = confidence > 0;
|
||||
const portCount = Array.isArray(discovery.ports) ? discovery.ports.length : 0;
|
||||
const configPathCount = Array.isArray(discovery.config_paths) ? discovery.config_paths.length : 0;
|
||||
const dataPathCount = Array.isArray(discovery.data_paths) ? discovery.data_paths.length : 0;
|
||||
const logPathCount = Array.isArray(discovery.log_paths) ? discovery.log_paths.length : 0;
|
||||
const hasFacts = Array.isArray(discovery.facts) && discovery.facts.length > 0;
|
||||
const hasCli = typeof discovery.cli_access === 'string' && discovery.cli_access.trim().length > 0;
|
||||
const hasPaths = configPathCount + dataPathCount + logPathCount > 0;
|
||||
const suggestedUrl = normalizeDiscoverySuggestedUrl(discovery.suggested_url);
|
||||
const hasEndpointCandidate =
|
||||
Boolean(suggestedUrl) ||
|
||||
(typeof discovery.suggested_url_diagnostic === 'string' &&
|
||||
discovery.suggested_url_diagnostic.trim().length > 0);
|
||||
if (
|
||||
!hasName &&
|
||||
!hasConfidence &&
|
||||
portCount === 0 &&
|
||||
!hasFacts &&
|
||||
!hasPaths &&
|
||||
!hasCli &&
|
||||
!hasEndpointCandidate
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const suggestedUrlReason = getDiscoverySuggestedURLReason(discovery);
|
||||
return {
|
||||
serviceName: hasName ? serviceName : 'Unidentified service',
|
||||
serviceType: discovery.service_type?.trim() || undefined,
|
||||
serviceVersion: discovery.service_version?.trim() || undefined,
|
||||
category: discovery.category?.trim() || undefined,
|
||||
serviceType: isMeaningfulDiscoveryText(discovery.service_type)
|
||||
? discovery.service_type?.trim()
|
||||
: undefined,
|
||||
serviceVersion: isMeaningfulDiscoveryText(discovery.service_version)
|
||||
? discovery.service_version?.trim()
|
||||
: undefined,
|
||||
category: isMeaningfulDiscoveryText(discovery.category)
|
||||
? discovery.category?.trim()
|
||||
: undefined,
|
||||
confidence,
|
||||
confidencePercent: `${Math.round(confidence * 100)}%`,
|
||||
cliAccess: hasCli ? discovery.cli_access?.trim() : undefined,
|
||||
|
|
@ -95,7 +165,7 @@ export function getDiscoveryIdentifiedSummary(
|
|||
suggestedUrlReasonText: suggestedUrlReason.text || undefined,
|
||||
suggestedUrlReasonTitle: suggestedUrlReason.title || undefined,
|
||||
suggestedUrlDiagnostic: discovery.suggested_url_diagnostic?.trim() || undefined,
|
||||
hasEndpointCandidate,
|
||||
hasEndpointCandidate: Boolean(suggestedUrl),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ func TestBuildFixtureGraphIncludesDiscoveryContextFixtures(t *testing.T) {
|
|||
var hasDockerURL bool
|
||||
var hasVM bool
|
||||
var hasAgent bool
|
||||
var hasSMTPRelay bool
|
||||
for _, discovery := range graph.DiscoveryFixtures {
|
||||
if discovery == nil {
|
||||
t.Fatal("discovery fixture must not be nil")
|
||||
|
|
@ -101,6 +102,9 @@ func TestBuildFixtureGraphIncludesDiscoveryContextFixtures(t *testing.T) {
|
|||
if discovery.ServiceName == "" || discovery.ServiceVersion == "" || len(discovery.ConfigPaths) == 0 {
|
||||
t.Fatalf("discovery fixture missing operator context: %+v", discovery)
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(discovery.ServiceName)), "unknown") {
|
||||
t.Fatalf("mock discovery fixture must not publish placeholder service names: %+v", discovery)
|
||||
}
|
||||
switch discovery.ResourceType {
|
||||
case discoveryResourceTypeDocker:
|
||||
hasDockerURL = hasDockerURL || discovery.SuggestedURL != "" && len(discovery.DockerMounts) > 0
|
||||
|
|
@ -108,6 +112,8 @@ func TestBuildFixtureGraphIncludesDiscoveryContextFixtures(t *testing.T) {
|
|||
hasVM = true
|
||||
case discoveryResourceTypeAgent:
|
||||
hasAgent = true
|
||||
case discoveryResourceTypeSystemContainer:
|
||||
hasSMTPRelay = hasSMTPRelay || strings.Contains(discovery.ServiceName, "SMTP")
|
||||
}
|
||||
}
|
||||
if !hasDockerURL {
|
||||
|
|
@ -116,6 +122,9 @@ func TestBuildFixtureGraphIncludesDiscoveryContextFixtures(t *testing.T) {
|
|||
if !hasVM || !hasAgent {
|
||||
t.Fatalf("expected guest and host discovery fixtures, hasVM=%t hasAgent=%t", hasVM, hasAgent)
|
||||
}
|
||||
if !hasSMTPRelay {
|
||||
t.Fatal("expected system-container discovery fixtures to identify SMTP relay workloads")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDemoScenarioStorageNamingHandlesScaledNodeCount(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
maxMockVMDiscoveryFixtures = 4
|
||||
maxMockContainerDiscoveryFixtures = 4
|
||||
maxMockDockerDiscoveryFixtures = 10
|
||||
maxMockHostDiscoveryFixtures = 4
|
||||
maxMockK8sDiscoveryFixtures = 4
|
||||
maxMockVMDiscoveryFixtures = 48
|
||||
maxMockContainerDiscoveryFixtures = 64
|
||||
maxMockDockerDiscoveryFixtures = 96
|
||||
maxMockHostDiscoveryFixtures = 6
|
||||
maxMockK8sDiscoveryFixtures = 80
|
||||
|
||||
discoveryResourceTypeVM = "vm"
|
||||
discoveryResourceTypeSystemContainer = "system-container"
|
||||
|
|
@ -498,6 +498,16 @@ func mockServiceDiscoveryProfile(name, image, runtime string) mockDiscoveryProfi
|
|||
return mockProfile("redis", "Redis", firstNonEmpty(version, "7.2"), discoveryCategoryCache, 6379, "redis-server", "", "", "No web interface was suggested because Redis exposes a cache port, not an HTTP UI.", []string{"/usr/local/etc/redis/redis.conf", "/etc/redis/redis.conf"}, []string{"/data"}, []string{"/var/log/redis/redis-server.log"})
|
||||
case strings.Contains(token, "traefik") || strings.Contains(token, "edge-proxy"):
|
||||
return mockProfile("traefik", "Traefik", firstNonEmpty(version, "3.1"), discoveryCategoryWebServer, 443, "traefik", "https", "/", "", []string{"/etc/traefik/traefik.yml", "/etc/traefik/dynamic"}, []string{"/letsencrypt"}, []string{"/var/log/traefik/traefik.log"})
|
||||
case strings.Contains(token, "smtp") || strings.Contains(token, "postfix") || strings.Contains(token, "mail-relay"):
|
||||
return mockProfile("postfix", "Postfix SMTP Relay", firstNonEmpty(version, "3.8"), discoveryCategoryNetwork, 25, "master", "", "", "No web interface was suggested because the detected service exposes SMTP, not an HTTP UI.", []string{"/etc/postfix/main.cf", "/etc/postfix/master.cf"}, []string{"/var/spool/postfix"}, []string{"/var/log/mail.log"})
|
||||
case strings.Contains(token, "auth-service"):
|
||||
return mockProfile("auth-service", "Auth Service", firstNonEmpty(version, "2026.04"), discoveryCategorySecurity, 8080, "auth-service", "http", "/", "", []string{"/etc/auth-service/config.yaml"}, []string{"/var/lib/auth-service"}, []string{"/var/log/auth-service.log"})
|
||||
case strings.Contains(token, "billing-worker") || strings.Contains(token, "payments-worker"):
|
||||
return mockProfile("queue-worker", "Queue Worker", firstNonEmpty(version, "2026.04"), discoveryCategoryBackup, 0, "queue-worker", "", "", "No web interface was suggested because this workload is a background worker.", []string{"/etc/pulse-demo/worker.yaml"}, []string{"/var/lib/pulse-demo/worker"}, []string{"/var/log/pulse-demo/worker.log"})
|
||||
case strings.Contains(token, "reporting-api") || strings.Contains(token, "inventory-api") || strings.Contains(token, "checkout-api"):
|
||||
return mockProfile("api-service", "API Service", firstNonEmpty(version, "2026.04"), discoveryCategoryWebServer, 8080, "api-service", "http", "/", "", []string{"/etc/pulse-demo/api.yaml"}, []string{"/var/lib/pulse-demo/api"}, []string{"/var/log/pulse-demo/api.log"})
|
||||
case strings.Contains(token, "docs-wiki") || strings.Contains(token, "docs-portal") || strings.Contains(token, "dev-portal") || strings.Contains(token, "customer-portal"):
|
||||
return mockProfile("web-portal", "Web Portal", firstNonEmpty(version, "2026.04"), discoveryCategoryWebServer, 8080, "web-portal", "http", "/", "", []string{"/etc/pulse-demo/portal.yaml"}, []string{"/var/lib/pulse-demo/portal"}, []string{"/var/log/pulse-demo/portal.log"})
|
||||
case strings.Contains(token, "vaultwarden"):
|
||||
return mockProfile("vaultwarden", "Vaultwarden", firstNonEmpty(version, "1.32.7"), discoveryCategorySecurity, 80, "vaultwarden", "http", "/", "", []string{"/data/config.json"}, []string{"/data"}, []string{"/data/vaultwarden.log"})
|
||||
case strings.Contains(token, "uptime-kuma"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue