Files
ServiceMap-Public/frontend/src/App.tsx
T

3740 lines
134 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
FormEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Background,
Controls,
MarkerType,
MiniMap,
ReactFlow,
type Connection,
type Edge,
type ReactFlowInstance,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import {
api,
type AvailabilitySummary,
type ComponentRole,
type Criticality,
type DiscoveryResult,
type DiscoveryRun,
type DiscoveryStatus,
type DiscoveryObservation,
type RelationEvidence,
type ProbeRepairReport,
type TopologySuggestion,
type TopologyDiff,
type TopologySnapshot,
type ProviderStatus,
type DemoState,
type DockerCandidate,
type EventRecord,
type HealthStatus,
type IncidentDetail,
type IncidentOverview,
type IncidentRecord,
type MonitoringState,
type ServiceInput,
type ServiceKind,
type ServiceSnapshot,
type SemanticTopology,
} from "./api";
import { LivingEdge } from "./LivingEdge";
import { DemoWalkthrough } from "./demo/DemoWalkthrough";
import { ServiceNodeView, type ServiceNode, type ServiceNodeData } from "./topology/ServiceNode";
import {
applicationEdges,
applicationName,
applicationTopologyLayout,
topologyLayout,
type LegacyTopologyMode,
} from "./topology/legacyLayout";
export {
applicationEdges,
applicationTopologyLayout,
landscapeGroups,
topologyLayout,
} from "./topology/legacyLayout";
import { semanticTopologyGraph, type SemanticView } from "./topology/semanticGraph";
import { TopologyCommandBar } from "./topology/TopologyCommandBar";
import { arrangeTopology, topologyStructureKey } from "./topologyLayout";
import { useRealtimeEvents } from "./realtime/useRealtimeEvents";
import { changedFields, filterSuggestions } from "./auditViewModel";
import {
applicationLocation,
domainLocation,
isTopologyRoot,
topologyRoot,
topologyUp,
type TopologyLocation,
} from "./topologyNavigation";
type Panel =
| "overview"
| "topology"
| "incidents"
| "services"
| "discovery"
| "changes"
| "config";
type FilterStatus = "all" | "unresolved" | HealthStatus;
type TopologyMode = LegacyTopologyMode;
type ServiceForm = Omit<ServiceInput, "tags"> & {
depends_on: string[];
tags: string;
};
type MonitoringFilter = "all" | MonitoringState;
const emptyForm = (): ServiceForm => ({
name: "",
kind: "http",
url: "",
host: "",
port: undefined,
depends_on: [],
tags: "",
group: "",
environment: "production",
application: "",
domain: "",
component_role: "unknown",
application_locked: false,
domain_locked: false,
component_role_locked: false,
criticality: "medium",
description: "",
category: "unknown",
visibility: "visible",
lifecycle: "active",
enabled: true,
monitoring_enabled: true,
interval_seconds: undefined,
timeout_seconds: undefined,
retries: undefined,
failure_threshold: 1,
recovery_threshold: 1,
availability_target: undefined,
maintenance_enabled: false,
});
const effectiveStatus = (snapshot: ServiceSnapshot): HealthStatus =>
snapshot.effective_status ?? snapshot.latest_probe?.status ?? "unknown";
const statusLabel = (status: HealthStatus) =>
status === "unknown"
? "Health unknown"
: status === "unmonitored"
? "Unmonitored"
: status;
const statusIcon = (status: HealthStatus) =>
({
healthy: "✓",
degraded: "△",
down: "!",
recovering: "↗",
maintenance: "Ⅱ",
unknown: "?",
unmonitored: "○",
})[status];
const formatDate = (value: string | null | undefined) =>
value ? new Date(value).toLocaleString() : "—";
const monitoringLabel = (state: MonitoringState) =>
({
active_probe: "Active probe",
docker_health: "Docker health",
appops_observation: "AppOps observed",
observed: "Runtime observed",
unmonitored: "Not monitored",
disabled: "Monitoring disabled",
})[state];
const hasCurrentNetworkProbe = (snapshot: ServiceSnapshot) =>
snapshot.service.monitoring_state === "active_probe";
const nodeTypes = { service: ServiceNodeView };
const edgeTypes = { living: LivingEdge };
function downloadText(filename: string, content: string, type: string) {
const url = URL.createObjectURL(new Blob([content], { type }));
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}
export default function App() {
const [services, setServices] = useState<ServiceSnapshot[]>([]);
const [loading, setLoading] = useState(true);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [busyId, setBusyId] = useState<string | null>(null);
const [busyAll, setBusyAll] = useState(false);
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [incidentSearch, setIncidentSearch] = useState("");
const [incidentStatusFilter, setIncidentStatusFilter] = useState("active");
const [suggestionSearch, setSuggestionSearch] = useState("");
const [suggestionConfidence, setSuggestionConfidence] = useState("actionable");
const [suggestionType, setSuggestionType] = useState("all");
const [suggestionLimit, setSuggestionLimit] = useState(30);
const [changeLimit, setChangeLimit] = useState(30);
const [reconcileConfirm, setReconcileConfirm] = useState(false);
const [panel, setPanel] = useState<Panel>("topology");
const [form, setForm] = useState<ServiceForm>(emptyForm);
const [editingId, setEditingId] = useState<string | null>(null);
const [formOpen, setFormOpen] = useState(false);
const [history, setHistory] = useState<import("./api").ProbeResult[]>([]);
const [historyServiceId, setHistoryServiceId] = useState<string | null>(null);
const [availability, setAvailability] = useState<
Record<number, AvailabilitySummary | null>
>({ 24: null, 168: null, 720: null });
const [incident, setIncident] = useState<IncidentOverview | null>(null);
const [overview, setOverview] = useState<
import("./api").OverviewSummary | null
>(null);
const [semanticTopology, setSemanticTopology] =
useState<SemanticTopology | null>(null);
const [events, setEvents] = useState<EventRecord[]>([]);
const [demo, setDemo] = useState<DemoState | null>(null);
const [demoConfirm, setDemoConfirm] = useState(false);
const [demoBusy, setDemoBusy] = useState(false);
const [demoStep, setDemoStep] = useState(0);
const [configText, setConfigText] = useState("version: 1\nservices: []\n");
const [configFormat, setConfigFormat] = useState<"yaml" | "json">("yaml");
const [configMode, setConfigMode] = useState<"merge" | "replace">("merge");
const [dockerHost, setDockerHost] = useState("unix:///var/run/docker.sock");
const [dockerCandidates, setDockerCandidates] = useState<DockerCandidate[]>(
[],
);
const [dockerBusy, setDockerBusy] = useState(false);
const [appopsUrl, setAppopsUrl] = useState("");
const [discoveryRun, setDiscoveryRun] = useState<DiscoveryRun | null>(null);
const [discoveryStatus, setDiscoveryStatus] =
useState<DiscoveryStatus | null>(null);
const [discoveryItems, setDiscoveryItems] = useState<DiscoveryObservation[]>(
[],
);
const [discoverySuggestions, setDiscoverySuggestions] = useState<
TopologySuggestion[]
>([]);
const [discoveryBusy, setDiscoveryBusy] = useState(false);
const [probeRepair, setProbeRepair] = useState<ProbeRepairReport | null>(
null,
);
const [editMode, setEditMode] = useState(false);
const [showMiniMap, setShowMiniMap] = useState(false);
const [edgeEvidence, setEdgeEvidence] = useState<RelationEvidence[]>([]);
const [providerStatuses, setProviderStatuses] = useState<ProviderStatus[]>([]);
const [topologySnapshots, setTopologySnapshots] = useState<TopologySnapshot[]>([]);
const [topologyDiff, setTopologyDiff] = useState<TopologyDiff | null>(null);
const [selectedSnapshotId, setSelectedSnapshotId] = useState<string | null>(null);
const [snapshotCheckedAt, setSnapshotCheckedAt] = useState(0);
const [selectedEdgeId, setSelectedEdgeId] = useState<string | null>(null);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<FilterStatus>("all");
const [monitoringFilter, setMonitoringFilter] =
useState<MonitoringFilter>("all");
const [groupFilter, setGroupFilter] = useState("all");
const [environmentFilter, setEnvironmentFilter] = useState("all");
const [criticalityFilter, setCriticalityFilter] = useState("all");
const [domainFilter, setDomainFilter] = useState("all");
const [serviceApplicationFilter, setServiceApplicationFilter] =
useState("all");
const [lifecycleFilter, setLifecycleFilter] = useState("all");
const [focusMode, setFocusMode] = useState(false);
const [blastRadius, setBlastRadius] = useState(false);
const [topologyMode, setTopologyMode] = useState<TopologyMode>("landscape");
const [applicationFilter, setApplicationFilter] = useState("all");
const [componentScope, setComponentScope] = useState<{
label: string;
memberIds: string[];
} | null>(null);
const [topologyLocation, setTopologyLocation] = useState<TopologyLocation>(
topologyRoot,
);
// Compatibility adapter for legacy non-semantic controls. Navigation state
// itself remains the single TopologyLocation value above.
const semanticScope = topologyLocation;
const setSemanticScope = (
update:
| Pick<TopologyLocation, "domainId" | "applicationId">
| ((scope: TopologyLocation) => Pick<TopologyLocation, "domainId" | "applicationId">),
) => {
setTopologyLocation((current) => {
const next = typeof update === "function" ? update(current) : update;
if (!next.domainId) return topologyRoot();
return next.applicationId
? applicationLocation(next.domainId, next.applicationId)
: domainLocation(next.domainId);
});
};
const [inspectorOpen, setInspectorOpen] = useState(true);
const [filtersOpen, setFiltersOpen] = useState(false);
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
const [selectedIncident, setSelectedIncident] =
useState<IncidentRecord | null>(null);
const [incidentDetail, setIncidentDetail] = useState<IncidentDetail | null>(
null,
);
const flowInstance = useRef<ReactFlowInstance<ServiceNode, Edge> | null>(
null,
);
const sidePanelRef = useRef<HTMLElement | null>(null);
const demoFocusApplied = useRef(false);
const [layoutPositions, setLayoutPositions] = useState<
Record<string, { x: number; y: number }>
>({});
const [layoutRoutes, setLayoutRoutes] = useState<
Record<string, { x: number; y: number }[]>
>({});
const refreshDiscovery = useCallback(async () => {
try {
const [status, items, suggestions, evidence, providers, snapshots] = await Promise.all([
api.discoveryStatus(),
api.discoveryItems(),
api.discoverySuggestions("open"),
api.topologyEdges(),
api.providers(),
api.topologySnapshots(),
]);
setDiscoveryStatus(status);
setDiscoveryRun(status.current ?? status.last_completed);
setDiscoveryItems(items);
setDiscoverySuggestions(suggestions);
setEdgeEvidence(evidence);
setProviderStatuses(providers);
setTopologySnapshots(snapshots);
setSnapshotCheckedAt(Date.now());
if (!snapshots.length) setTopologyDiff(null);
setSelectedSnapshotId((current) =>
current && snapshots.some((item) => item.id === current)
? current
: snapshots[0]?.id ?? null,
);
} catch (err) {
// Discovery is an optional enrichment path. Keep the primary topology usable
// when an older API or an unavailable source is encountered during rollout.
setError(
err instanceof Error ? err.message : "Failed to load discovery state",
);
}
}, []);
useEffect(() => {
if (!selectedSnapshotId) return;
const index = topologySnapshots.findIndex((item) => item.id === selectedSnapshotId);
if (index < 0) return;
let active = true;
void api.topologyDiff(selectedSnapshotId, topologySnapshots[index + 1]?.id)
.then((diff) => { if (active) setTopologyDiff(diff); })
.catch(() => { if (active) setTopologyDiff(null); });
return () => { active = false; };
}, [selectedSnapshotId, topologySnapshots]);
const refresh = useCallback(async () => {
try {
const [nextServices, nextIncident, nextDemo, nextOverview, nextEvents] =
await Promise.all([
api.listServices(),
api.incidents(),
api.demoState(),
api.overview(),
api.events(30),
]);
setServices(nextServices);
setSelectedId((current) =>
current && !nextServices.some((item) => item.service.id === current)
? null
: current,
);
setIncident(nextIncident);
setDemo(nextDemo);
if (!nextDemo.active) {
demoFocusApplied.current = false;
} else if (!demoFocusApplied.current && nextDemo.root_cause_service_id) {
setSelectedId(nextDemo.root_cause_service_id);
setPanel("topology");
demoFocusApplied.current = true;
}
if (!nextDemo.active && nextIncident.candidates[0]) {
setSelectedId((current) => current ?? nextIncident.candidates[0].service_id);
}
setOverview(nextOverview);
setEvents(nextEvents);
setError(null);
void refreshDiscovery();
void api
.semanticTopology()
.then(setSemanticTopology)
.catch(() => {
// The component-level graph remains available while a rolling deploy
// brings older API instances forward.
setSemanticTopology(null);
});
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to refresh ServiceMap",
);
} finally {
setLoading(false);
}
}, [refreshDiscovery]);
const refreshTimerRef = useRef<number | undefined>(undefined);
const scheduleRefresh = useCallback(() => {
if (refreshTimerRef.current !== undefined) {
window.clearTimeout(refreshTimerRef.current);
}
refreshTimerRef.current = window.setTimeout(() => void refresh(), 180);
}, [refresh]);
useRealtimeEvents({ onEvent: scheduleRefresh, onFallback: scheduleRefresh });
useEffect(() => {
// This starts async API reads; refresh applies its state only after those reads settle.
// eslint-disable-next-line react-hooks/set-state-in-effect
void refresh();
return () => {
if (refreshTimerRef.current !== undefined) {
window.clearTimeout(refreshTimerRef.current);
}
};
}, [refresh]);
useEffect(() => {
if (selectedId || selectedEdgeId)
sidePanelRef.current?.scrollTo({ top: 0 });
}, [selectedId, selectedEdgeId, demo?.active]);
useEffect(() => {
if (!selectedId || !services.some((item) => item.service.id === selectedId)) return;
let active = true;
void Promise.all([
api.history(selectedId, 48),
api.availability(selectedId, 24),
api.availability(selectedId, 168),
api.availability(selectedId, 720),
])
.then(([nextHistory, day, week, month]) => {
if (!active) return;
setHistory(nextHistory);
setAvailability({ 24: day, 168: week, 720: month });
setHistoryServiceId(selectedId);
})
.catch((err) => {
if (active)
setError(
err instanceof Error
? err.message
: "Failed to load service history",
);
});
return () => {
active = false;
};
}, [selectedId, services]);
const groups = useMemo(
() =>
[
...new Set(
services
.map((item) => item.service.group)
.filter(Boolean) as string[],
),
].sort(),
[services],
);
const applications = useMemo(
() => [...new Set(services.map(applicationName))].sort(),
[services],
);
const semanticDomainByService = useMemo(() => {
const names = new Map(
(semanticTopology?.domains ?? []).map((item) => [item.id, item.name]),
);
return new Map(
(semanticTopology?.components ?? []).map((item) => [
item.service_id,
names.get(item.domain_id) ?? null,
]),
);
}, [semanticTopology]);
const domains = useMemo(
() =>
[
...new Set(
services
.map(
(item) =>
item.service.domain ?? semanticDomainByService.get(item.service.id),
)
.filter(Boolean) as string[],
),
].sort(),
[semanticDomainByService, services],
);
const environments = useMemo(
() =>
[
...new Set(
services
.map((item) => item.service.environment)
.filter(Boolean) as string[],
),
].sort(),
[services],
);
const filteredServices = useMemo(() => {
const matchesFilter = (snapshot: ServiceSnapshot) => {
const name =
`${snapshot.service.name} ${snapshot.service.description ?? ""} ${snapshot.service.tags.join(" ")}`.toLowerCase();
return (
(!search || name.includes(search.toLowerCase())) &&
(statusFilter === "all" ||
(statusFilter === "unresolved"
? ["unknown", "unmonitored"].includes(effectiveStatus(snapshot))
: effectiveStatus(snapshot) === statusFilter)) &&
(monitoringFilter === "all" ||
snapshot.service.monitoring_state === monitoringFilter) &&
(groupFilter === "all" || snapshot.service.group === groupFilter) &&
(environmentFilter === "all" ||
snapshot.service.environment === environmentFilter) &&
(criticalityFilter === "all" ||
snapshot.service.criticality === criticalityFilter) &&
(domainFilter === "all" ||
(snapshot.service.domain ?? semanticDomainByService.get(snapshot.service.id)) ===
domainFilter) &&
(serviceApplicationFilter === "all" ||
applicationName(snapshot) === serviceApplicationFilter) &&
(lifecycleFilter === "all" ||
snapshot.service.lifecycle === lifecycleFilter)
);
};
return services.filter(matchesFilter);
}, [
services,
search,
statusFilter,
monitoringFilter,
groupFilter,
environmentFilter,
criticalityFilter,
domainFilter,
serviceApplicationFilter,
lifecycleFilter,
semanticDomainByService,
]);
const directDependents = useMemo(() => {
const result = new Map<string, string[]>();
for (const snapshot of services)
for (const dependencyId of snapshot.service.depends_on)
result.set(dependencyId, [
...(result.get(dependencyId) ?? []),
snapshot.service.id,
]);
return result;
}, [services]);
const relatedIds = useMemo(() => {
if (!selectedId || !focusMode) return null;
const result = new Set([selectedId]);
const visit = (id: string, depth: number) => {
if (depth <= 0) return;
const service = services.find((item) => item.service.id === id)?.service;
for (const dependencyId of service?.depends_on ?? []) {
result.add(dependencyId);
visit(dependencyId, depth - 1);
}
for (const dependentId of directDependents.get(id) ?? []) {
result.add(dependentId);
visit(dependentId, depth - 1);
}
};
visit(selectedId, 2);
return result;
}, [selectedId, focusMode, services, directDependents]);
const impactIds = useMemo(
() =>
new Set(
demo?.affected_service_ids ??
selectedIncident?.affected_service_ids ??
[],
),
[demo?.affected_service_ids, selectedIncident?.affected_service_ids],
);
const graphServices = useMemo(() => {
const base = filteredServices.filter(
(item) => !relatedIds || relatedIds.has(item.service.id),
);
if (!blastRadius) return base;
const allowed = new Set(
[
demo?.root_cause_service_id ?? selectedIncident?.root_service_id,
...impactIds,
].filter(Boolean),
);
return base.filter((item) => allowed.has(item.service.id));
}, [
filteredServices,
relatedIds,
blastRadius,
demo?.root_cause_service_id,
selectedIncident?.root_service_id,
impactIds,
]);
const componentServices = useMemo(() => {
if (topologyLocation.level !== "components") return graphServices;
if (componentScope) {
const members = new Set(componentScope.memberIds);
return graphServices.filter((item) => members.has(item.service.id));
}
return applicationFilter !== "all"
? graphServices.filter(
(item) => applicationName(item) === applicationFilter,
)
: graphServices;
}, [topologyLocation.level, applicationFilter, componentScope, graphServices]);
const activeGraphServices =
topologyLocation.level === "components" ? componentServices : graphServices;
const rootCauseId =
demo?.root_cause_service_id ?? selectedIncident?.root_service_id ?? null;
const legacyNodes = useMemo(
() =>
topologyLocation.level === "components"
? topologyLayout(activeGraphServices, rootCauseId, impactIds)
: applicationTopologyLayout(
activeGraphServices,
rootCauseId,
impactIds,
),
[topologyLocation.level, activeGraphServices, rootCauseId, impactIds],
);
const legacyEdges = useMemo<Edge[]>(
() =>
topologyLocation.level !== "components"
? applicationEdges(
activeGraphServices,
topologyMode,
impactIds,
rootCauseId,
)
: activeGraphServices.flatMap((snapshot) =>
snapshot.service.depends_on
.filter((dependencyId) =>
activeGraphServices.some(
(item) => item.service.id === dependencyId,
),
)
.map((dependencyId) => ({
id: `${dependencyId}-${snapshot.service.id}`,
source: dependencyId,
target: snapshot.service.id,
animated: effectiveStatus(snapshot) !== "healthy",
className: `edge-${effectiveStatus(snapshot)}${blastRadius && !impactIds.has(snapshot.service.id) ? " edge-dimmed" : ""}`,
markerEnd: { type: MarkerType.ArrowClosed },
})),
),
[
topologyLocation.level,
activeGraphServices,
impactIds,
blastRadius,
rootCauseId,
topologyMode,
],
);
const semanticView: SemanticView = topologyLocation.level;
const semanticGraph = useMemo(
() =>
semanticTopology
? semanticTopologyGraph(
semanticTopology,
semanticView,
semanticView === "components"
? topologyLocation.applicationId
: topologyLocation.domainId,
topologyMode === "impact" || blastRadius,
search,
statusFilter,
)
: null,
[
semanticTopology,
semanticView,
topologyLocation.domainId,
topologyLocation.applicationId,
topologyMode,
blastRadius,
search,
statusFilter,
],
);
const graphNodes = semanticGraph?.nodes ?? legacyNodes;
const baseEdges = semanticGraph?.edges ?? legacyEdges;
const edges = useMemo<Edge[]>(
() =>
baseEdges.map((edge) => {
const touchesSelected = Boolean(
selectedId && (edge.source === selectedId || edge.target === selectedId),
);
const touchesHovered = Boolean(
hoveredNodeId &&
(edge.source === hoveredNodeId || edge.target === hoveredNodeId),
);
const incidentPath = Boolean(
rootCauseId &&
(edge.source === rootCauseId || edge.target === rootCauseId) &&
(impactIds.has(edge.source) || impactIds.has(edge.target)),
);
return {
...edge,
type: "living",
data: {
relation: edge.className?.includes("routes")
? "routes_to"
: edge.className?.includes("review") || edge.className?.includes("communicates")
? "review"
: "depends_on",
selected: edge.id === selectedEdgeId,
route: layoutRoutes[edge.id],
signal:
edge.id === selectedEdgeId ||
touchesSelected ||
touchesHovered ||
incidentPath,
},
};
}),
[
baseEdges,
hoveredNodeId,
impactIds,
layoutRoutes,
rootCauseId,
selectedEdgeId,
selectedId,
],
);
const graphLayoutKey = useMemo(
() => topologyStructureKey(graphNodes, edges),
[graphNodes, edges],
);
const layoutPositionKey = useMemo(
() =>
graphNodes
.map((node) => {
const position = layoutPositions[node.id] ?? node.position;
return `${node.id}:${position.x}:${position.y}`;
})
.join("|"),
[graphNodes, layoutPositions],
);
const autoArrangeGraph = async () => {
const layout = await arrangeTopology(graphNodes, edges);
setLayoutPositions(layout.positions);
setLayoutRoutes(layout.routes);
};
useEffect(() => {
if (!graphNodes.length) return;
let active = true;
void arrangeTopology(graphNodes, edges).then((layout) => {
if (!active) return;
setLayoutPositions(layout.positions);
setLayoutRoutes(layout.routes);
});
return () => {
active = false;
};
// Layout work is keyed only by structure; health updates retain positions.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [graphLayoutKey]);
const nodes = useMemo(
() =>
graphNodes.map((node) => ({
...node,
position: layoutPositions[node.id] ?? node.position,
})),
[graphNodes, layoutPositions],
);
const selected =
services.find((item) => item.service.id === selectedId) ?? null;
const hasCurrentHistory = Boolean(
selectedId && historyServiceId === selectedId,
);
const selectedEdge =
edgeEvidence.find((item) => item.id === selectedEdgeId) ?? null;
const selectedIsRootCause = Boolean(
selected &&
(demo?.root_cause_service_id === selected.service.id ||
selectedIncident?.root_service_id === selected.service.id),
);
const counts = useMemo(
() =>
services.reduce(
(result, item) => {
const key = effectiveStatus(item);
result[key] = (result[key] ?? 0) + 1;
return result;
},
{} as Record<string, number>,
),
[services],
);
const monitoredCount = useMemo(
() =>
services.filter((item) =>
["active_probe", "docker_health", "appops_observation", "observed"].includes(
item.service.monitoring_state,
),
).length,
[services],
);
const resolvedComponentCount = useMemo(
() =>
semanticTopology?.components.filter(
(item) => !["unknown", "unmonitored"].includes(item.direct_status),
).length ?? monitoredCount,
[monitoredCount, semanticTopology],
);
const unresolvedComponentCount = useMemo(
() =>
semanticTopology?.components.filter((item) =>
["unknown", "unmonitored"].includes(item.direct_status),
).length ?? Math.max(0, services.length - monitoredCount),
[monitoredCount, semanticTopology, services.length],
);
const coverageCounts = useMemo(
() =>
services.reduce<Record<string, number>>((result, item) => {
const state = item.service.monitoring_state;
result[state] = (result[state] ?? 0) + 1;
return result;
}, {}),
[services],
);
const visibleSuggestions = useMemo(
() => filterSuggestions(discoverySuggestions, {
query: suggestionSearch,
confidence: suggestionConfidence,
type: suggestionType,
}),
[discoverySuggestions, suggestionSearch, suggestionConfidence, suggestionType],
);
const visibleIncidents = useMemo(
() => (incident?.incidents ?? []).filter((item) =>
(incidentStatusFilter === "all" ||
(incidentStatusFilter === "active" ? item.status !== "resolved" : item.status === incidentStatusFilter)) &&
(!incidentSearch.trim() || `${item.title} ${item.root_service_name} ${item.root_cause_reason}`
.toLowerCase().includes(incidentSearch.trim().toLowerCase())),
),
[incident, incidentSearch, incidentStatusFilter],
);
const selectedDomain = semanticTopology?.domains.find(
(item) => item.id === topologyLocation.domainId,
);
const selectedApplication = semanticTopology?.applications.find(
(item) => item.id === topologyLocation.applicationId,
);
const activeTopologyFilters: Array<{
label: string;
value: string;
clear: () => void;
}> = [
{ label: "Status", value: statusFilter, clear: () => setStatusFilter("all") },
{ label: "Monitoring", value: monitoringFilter, clear: () => setMonitoringFilter("all") },
{ label: "Domain", value: domainFilter, clear: () => setDomainFilter("all") },
{ label: "Application", value: serviceApplicationFilter, clear: () => setServiceApplicationFilter("all") },
{ label: "Environment", value: environmentFilter, clear: () => setEnvironmentFilter("all") },
{ label: "Criticality", value: criticalityFilter, clear: () => setCriticalityFilter("all") },
{ label: "Lifecycle", value: lifecycleFilter, clear: () => setLifecycleFilter("all") },
];
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
const target = event.target as HTMLElement | null;
if (
!target ||
panel !== "topology" ||
["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName) ||
target.isContentEditable ||
isTopologyRoot(topologyLocation)
)
return;
if (event.key === "Escape" || event.key === "Backspace") {
event.preventDefault();
setTopologyLocation((location) => topologyUp(location));
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [panel, topologyLocation]);
useEffect(() => {
if (!nodes.length || !flowInstance.current || panel !== "topology") return;
const frame = window.requestAnimationFrame(() =>
flowInstance.current?.fitView({
padding: 0.16,
duration: demo?.active ? 240 : 0,
}),
);
return () => window.cancelAnimationFrame(frame);
}, [
nodes.length,
demo?.active,
panel,
graphLayoutKey,
layoutPositionKey,
topologyMode,
focusMode,
blastRadius,
]);
const clearMessages = () => {
setError(null);
setNotice(null);
};
const resetForm = () => {
setEditingId(null);
setForm(emptyForm());
setFormOpen(false);
};
const payloadFromForm = (): ServiceInput => {
const common = {
name: form.name,
kind: form.kind,
depends_on: form.depends_on,
description: form.description,
tags: form.tags
.split(",")
.map((item) => item.trim())
.filter(Boolean),
group: form.group,
environment: form.environment,
criticality: form.criticality,
interval_seconds: form.interval_seconds,
timeout_seconds: form.timeout_seconds,
retries: form.retries,
failure_threshold: form.failure_threshold,
recovery_threshold: form.recovery_threshold,
availability_target: form.availability_target,
enabled: form.enabled,
monitoring_enabled: form.monitoring_enabled,
application: form.application || undefined,
domain: form.domain || undefined,
component_role: form.component_role,
application_locked: form.application_locked,
domain_locked: form.domain_locked,
component_role_locked: form.component_role_locked,
source_type: form.source_type,
source_ref: form.source_ref,
category: form.category,
visibility: form.visibility,
lifecycle: form.lifecycle,
first_seen_at: form.first_seen_at,
last_seen_at: form.last_seen_at,
last_discovery_status: form.last_discovery_status,
container_image: form.container_image,
compose_project: form.compose_project,
layout_x: form.layout_x,
layout_y: form.layout_y,
layout_pinned: form.layout_pinned,
maintenance_enabled: form.maintenance_enabled,
maintenance_reason: form.maintenance_reason,
};
if (form.kind === "http") return { ...common, kind: "http", url: form.url };
if (form.kind === "dns") return { ...common, kind: "dns", host: form.host };
return {
...common,
kind: form.kind,
host: form.host,
port: Number(form.port),
};
};
const payloadFromService = (
service: ServiceSnapshot["service"],
overrides: Partial<ServiceInput> = {},
): ServiceInput => ({
name: service.name,
kind: service.kind,
url: service.url ?? undefined,
host: service.host ?? undefined,
port: service.port ?? undefined,
depends_on: [...service.depends_on],
description: service.description ?? undefined,
tags: [...service.tags],
group: service.group ?? undefined,
environment: service.environment ?? undefined,
owner: service.owner ?? undefined,
enabled: service.enabled,
monitoring_enabled: service.monitoring_enabled,
application: service.application ?? undefined,
domain: service.domain ?? undefined,
component_role: service.component_role,
application_locked: service.application_locked,
domain_locked: service.domain_locked,
component_role_locked: service.component_role_locked,
criticality: service.criticality,
category: service.category,
visibility: service.visibility,
lifecycle: service.lifecycle,
source_type: service.source_type ?? undefined,
source_ref: service.source_ref ?? undefined,
first_seen_at: service.first_seen_at ?? undefined,
last_seen_at: service.last_seen_at ?? undefined,
last_discovery_status: service.last_discovery_status ?? undefined,
container_image: service.container_image ?? undefined,
compose_project: service.compose_project ?? undefined,
layout_x: service.layout_x ?? undefined,
layout_y: service.layout_y ?? undefined,
layout_pinned: service.layout_pinned,
interval_seconds: service.interval_seconds ?? undefined,
timeout_seconds: service.timeout_seconds ?? undefined,
retries: service.retries ?? undefined,
failure_threshold: service.failure_threshold,
recovery_threshold: service.recovery_threshold,
expected_status_min: service.expected_status_min ?? undefined,
expected_status_max: service.expected_status_max ?? undefined,
availability_target: service.availability_target ?? undefined,
maintenance_enabled: service.maintenance_enabled,
maintenance_reason: service.maintenance_reason ?? undefined,
...overrides,
});
const submit = async (event: FormEvent) => {
event.preventDefault();
clearMessages();
try {
const service = editingId
? await api.updateService(editingId, payloadFromForm())
: await api.createService(payloadFromForm());
setSelectedId(service.id);
setNotice(editingId ? "Service updated." : "Service added.");
resetForm();
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save service");
}
};
const beginEdit = (snapshot: ServiceSnapshot) => {
const service = snapshot.service;
setEditingId(service.id);
setForm({
name: service.name,
kind: service.kind,
url: service.url ?? "",
host: service.host ?? "",
port: service.port ?? undefined,
depends_on: [...service.depends_on],
tags: service.tags.join(", "),
group: service.group ?? "",
environment: service.environment ?? "",
application: service.application ?? "",
domain: service.domain ?? "",
component_role: service.component_role,
application_locked: service.application_locked,
domain_locked: service.domain_locked,
component_role_locked: service.component_role_locked,
criticality: service.criticality,
category: service.category,
visibility: service.visibility,
lifecycle: service.lifecycle,
description: service.description ?? "",
interval_seconds: service.interval_seconds ?? undefined,
timeout_seconds: service.timeout_seconds ?? undefined,
retries: service.retries ?? undefined,
failure_threshold: service.failure_threshold,
recovery_threshold: service.recovery_threshold,
availability_target: service.availability_target ?? undefined,
enabled: service.enabled,
monitoring_enabled: service.monitoring_enabled,
source_type: service.source_type ?? undefined,
source_ref: service.source_ref ?? undefined,
first_seen_at: service.first_seen_at ?? undefined,
last_seen_at: service.last_seen_at ?? undefined,
last_discovery_status: service.last_discovery_status ?? undefined,
container_image: service.container_image ?? undefined,
compose_project: service.compose_project ?? undefined,
maintenance_enabled: service.maintenance_enabled,
maintenance_reason: service.maintenance_reason ?? undefined,
layout_x: service.layout_x ?? undefined,
layout_y: service.layout_y ?? undefined,
layout_pinned: service.layout_pinned,
});
setFormOpen(true);
setInspectorOpen(true);
setPanel("services");
};
const probe = async (serviceId: string) => {
clearMessages();
setBusyId(serviceId);
try {
await api.probeService(serviceId);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Probe failed");
} finally {
setBusyId(null);
}
};
const probeAll = async () => {
clearMessages();
setBusyAll(true);
try {
const result = await api.probeAll();
setNotice(`Completed ${result.completed}/${result.requested} probes.`);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Bulk probe failed");
} finally {
setBusyAll(false);
}
};
const removeSelected = async () => {
if (!selected) return;
clearMessages();
try {
await api.deleteService(selected.service.id);
setSelectedId(null);
setNotice(`${selected.service.name} deleted.`);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Delete failed");
}
};
const focusDemo = (result: DemoState) => {
setSelectedId(result.root_cause_service_id);
setDemoStep(0);
setTopologyLocation(topologyRoot());
setTopologyMode("landscape");
setBlastRadius(false);
setPanel("topology");
demoFocusApplied.current = true;
};
const startDemo = async () => {
clearMessages();
setDemoBusy(true);
try {
const result = await api.loadDemo();
setDemoConfirm(false);
await refresh();
focusDemo(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load demo");
} finally {
setDemoBusy(false);
}
};
const loadDemo = async () => {
clearMessages();
if (demo?.active) {
await resetDemo();
return;
}
if (services.length) {
setDemoConfirm(true);
return;
}
await startDemo();
};
const resetDemo = async () => {
clearMessages();
setDemoBusy(true);
try {
const result = await api.resetDemo();
await refresh();
focusDemo(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to reset demo");
} finally {
setDemoBusy(false);
}
};
const exitDemo = async () => {
clearMessages();
setDemoBusy(true);
try {
const result = await api.exitDemo();
demoFocusApplied.current = false;
setDemoStep(0);
setSelectedId(null);
setNotice(
result.restored
? "Demo closed. Your previous topology was restored."
: "No active demo session.",
);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to exit demo");
} finally {
setDemoBusy(false);
}
};
const setDemoWalkthroughStep = (step: number) => {
setDemoStep(step);
setPanel("topology");
setInspectorOpen(true);
if (step === 0) {
setTopologyLocation(topologyRoot());
setTopologyMode("landscape");
setBlastRadius(false);
return;
}
if (step === 1) {
const dataDomain = semanticTopology?.domains.find(
(item) => item.name === "Data",
);
if (dataDomain) setTopologyLocation(domainLocation(dataDomain.id));
setTopologyMode("landscape");
setBlastRadius(false);
return;
}
if (step === 2) {
setTopologyLocation(topologyRoot());
setTopologyMode("impact");
setBlastRadius(true);
return;
}
setSelectedId(demo?.root_cause_service_id ?? null);
setTopologyLocation(topologyRoot());
setTopologyMode("impact");
setBlastRadius(true);
};
const exportConfig = async (format: "yaml" | "json") => {
clearMessages();
try {
const content = await api.exportConfig(format);
downloadText(
`servicemap.${format}`,
content,
format === "json" ? "application/json" : "application/yaml",
);
} catch (err) {
setError(err instanceof Error ? err.message : "Export failed");
}
};
const importConfig = async () => {
clearMessages();
try {
const result = await api.importConfig(
configText,
configFormat,
configMode,
);
setNotice(
`Configuration applied: ${result.created} created, ${result.updated} updated.`,
);
setSelectedId(null);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : "Import failed");
}
};
const discoverDocker = async () => {
clearMessages();
setDockerBusy(true);
try {
const result = await api.discoverDocker(dockerHost);
setDockerCandidates(result.candidates);
setNotice(
`Discovered ${result.candidates.length} Docker containers for review.`,
);
} catch (err) {
setError(err instanceof Error ? err.message : "Docker discovery failed");
} finally {
setDockerBusy(false);
}
};
const runProductionDiscovery = async () => {
clearMessages();
setDiscoveryBusy(true);
try {
const result: DiscoveryResult = await api.runDiscovery(
dockerHost,
appopsUrl || undefined,
false,
);
setDiscoveryRun(result.run);
setDiscoveryItems(result.observations);
setDiscoverySuggestions(
result.suggestions.filter((item) => item.status === "open"),
);
setDockerCandidates(result.candidates);
setDiscoveryStatus({
current: null,
last_completed: result.run,
active: false,
});
setNotice(
`Discovery complete: ${result.run.diff.new} new, ${result.run.diff.changed} changed, ${result.run.diff.missing} missing, ${result.run.diff.new_suggestions} new suggestions.`,
);
await refreshDiscovery();
} catch (err) {
setError(
err instanceof Error ? err.message : "Production discovery failed",
);
} finally {
setDiscoveryBusy(false);
}
};
const reconcileProduction = async () => {
clearMessages();
setDiscoveryBusy(true);
try {
const result = await api.reconcileDiscovery(discoveryRun?.id, true);
setReconcileConfirm(false);
setDiscoveryRun(result.run);
setNotice(
`Canonical topology updated: ${result.created} services created, ${result.updated} changed, ${result.accepted_relations} high-confidence relationships accepted.`,
);
await refresh();
await refreshDiscovery();
} catch (err) {
setError(
err instanceof Error ? err.message : "Discovery reconciliation failed",
);
} finally {
setDiscoveryBusy(false);
}
};
const repairDiscoveredProbes = async (dryRun: boolean) => {
clearMessages();
setDiscoveryBusy(true);
try {
const report = await api.repairDiscoveredProbes(dryRun, discoveryRun?.id);
setProbeRepair(report);
setNotice(
dryRun
? `Probe safety preview: ${report.changed} services would change; ${report.disabled_unsafe} unsafe probes would be disabled.`
: `Probe safety applied: ${report.changed} services updated; ${report.resolved_incidents} false incidents resolved.`,
);
if (!dryRun) await refresh();
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to repair discovered probes",
);
} finally {
setDiscoveryBusy(false);
}
};
const reviewSuggestion = async (
suggestionId: string,
action: "accept" | "reject",
) => {
clearMessages();
setDiscoveryBusy(true);
try {
if (action === "accept") await api.acceptSuggestion(suggestionId);
else await api.rejectSuggestion(suggestionId);
await refresh();
await refreshDiscovery();
setNotice(
action === "accept"
? "Discovery suggestion accepted."
: "Discovery suggestion rejected.",
);
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to update discovery suggestion",
);
} finally {
setDiscoveryBusy(false);
}
};
const prefillDockerCandidate = (
candidate: DockerCandidate,
suggestion = candidate.suggestions[0],
) => {
if (!suggestion) return;
setEditingId(null);
setForm({
...emptyForm(),
name: suggestion.name,
kind: suggestion.kind,
url: suggestion.url ?? "",
host: suggestion.host ?? "",
port: suggestion.port ?? undefined,
group: suggestion.group ?? "",
category: suggestion.category,
depends_on: [],
tags: "docker-discovery",
source_type: suggestion.source_type ?? "docker",
source_ref:
suggestion.stable_source_ref ??
suggestion.source_ref ??
`docker:${candidate.container_id}`,
});
setFormOpen(true);
setPanel("services");
setNotice(
"Docker suggestion copied into the service form. Review before saving.",
);
};
const persistNodePosition = async (node: ServiceNode) => {
const snapshot = services.find((item) => item.service.id === node.id);
if (!snapshot || !editMode) return;
try {
await api.updateService(
node.id,
payloadFromService(snapshot.service, {
layout_x: node.position.x,
layout_y: node.position.y,
layout_pinned: true,
}),
);
setNotice(`${snapshot.service.name} position saved.`);
await refresh();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to save node position",
);
}
};
const createManualEdge = async (connection: Connection) => {
if (
!editMode ||
!connection.source ||
!connection.target ||
connection.source === connection.target
)
return;
clearMessages();
try {
// A React Flow connection starts at the dependent node and points to
// the dependency. Canonical graph edges render in the opposite direction.
const dependent = services.find(
(item) => item.service.id === connection.source,
)?.service;
const dependency = services.find(
(item) => item.service.id === connection.target,
)?.service;
if (!dependent || !dependency) return;
await api.createEdge({
source_service_id: dependent.id,
target_service_id: dependency.id,
relation_type: "depends_on",
lock: true,
explanation: "Manual dependency created in Edit topology mode",
});
setNotice(`${dependent.name} now depends on ${dependency.name}.`);
await refresh();
await refreshDiscovery();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to create dependency",
);
}
};
const handleEdgeAction = async (
action: "confirm" | "reject" | "lock" | "remove",
) => {
if (!selectedEdgeId) return;
clearMessages();
try {
await api.edgeAction(selectedEdgeId, action);
setSelectedEdgeId(null);
await refresh();
await refreshDiscovery();
const label = {
confirm: "confirmed",
reject: "rejected",
lock: "locked",
remove: "removed",
}[action];
setNotice(`Edge ${label}.`);
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to update relationship",
);
}
};
const toggleDependency = (dependencyId: string) =>
setForm((current) => ({
...current,
depends_on: current.depends_on.includes(dependencyId)
? current.depends_on.filter((item) => item !== dependencyId)
: [...current.depends_on, dependencyId],
}));
const openIncident = async (item: IncidentRecord) => {
setSelectedIncident(item);
setSelectedId(item.root_service_id);
setPanel("incidents");
try {
setIncidentDetail(await api.incident(item.id));
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to load incident timeline",
);
}
};
const acknowledge = async () => {
if (!selectedIncident) return;
try {
const detail = await api.acknowledgeIncident(selectedIncident.id);
setSelectedIncident(detail.incident);
setIncidentDetail(detail);
await refresh();
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to acknowledge incident",
);
}
};
const toggleMaintenance = async () => {
if (!selected) return;
try {
await api.maintenance(
selected.service.id,
!selected.service.maintenance_enabled,
"Manual maintenance window",
);
await refresh();
} catch (err) {
setError(
err instanceof Error
? err.message
: "Failed to change maintenance state",
);
}
};
const renderEdgeInspector = () => {
if (!selectedEdge) return null;
const source = services.find(
(item) => item.service.id === selectedEdge.source_service_id,
)?.service;
const target = selectedEdge.target_service_id
? services.find(
(item) => item.service.id === selectedEdge.target_service_id,
)?.service
: null;
return (
<section
className="panel-section edge-inspector"
aria-label="Relationship inspector"
>
<div className="inspector-title">
<div>
<span className="eyebrow">Relationship inspector</span>
<h3>
{source?.name ?? "Unknown"} →{" "}
{target?.name ?? selectedEdge.target_ref ?? "Unknown"}
</h3>
</div>
<span
className={`health-pill status-${selectedEdge.active ? "healthy" : "unknown"}`}
>
{selectedEdge.active ? "active" : "inactive"}
</span>
</div>
<dl className="detail-grid">
<div>
<dt>Direction</dt>
<dd>
{source?.name ?? selectedEdge.source_service_id} depends on{" "}
{target?.name ?? selectedEdge.target_ref ?? "unknown"}
</dd>
</div>
<div>
<dt>Confidence</dt>
<dd>{selectedEdge.confidence}</dd>
</div>
<div>
<dt>Evidence</dt>
<dd>{selectedEdge.evidence_type}</dd>
</div>
<div>
<dt>Locked</dt>
<dd>{selectedEdge.locked ? "Yes" : "No"}</dd>
</div>
<div>
<dt>First seen</dt>
<dd>{formatDate(selectedEdge.first_seen_at)}</dd>
</div>
<div>
<dt>Last seen</dt>
<dd>{formatDate(selectedEdge.last_seen_at)}</dd>
</div>
</dl>
<p className="probe-detail">{selectedEdge.explanation}</p>
<div className="button-row wrap">
<button
onClick={() => void handleEdgeAction("confirm")}
disabled={!selectedEdge.active}
>
Confirm
</button>
<button
className="ghost"
onClick={() => void handleEdgeAction("lock")}
disabled={!selectedEdge.active || selectedEdge.locked}
>
Lock
</button>
<button
className="ghost"
onClick={() => void handleEdgeAction("reject")}
disabled={!selectedEdge.active}
>
Reject
</button>
<button
className="danger"
onClick={() => void handleEdgeAction("remove")}
disabled={!selectedEdge.active}
>
Remove
</button>
</div>
</section>
);
};
const renderInspector = () => (
<section className="panel-section inspector-section">
<div className="inspector-title">
<div>
<span className="eyebrow">Inspector</span>
<h3>{selected?.service.name ?? "Select a service"}</h3>
</div>
{selected && (
<span className={`health-pill status-${effectiveStatus(selected)}`}>
<i aria-hidden="true">{statusIcon(effectiveStatus(selected))}</i>{" "}
{statusLabel(effectiveStatus(selected))}
</span>
)}
</div>
{!selected ? (
<p className="muted compact">
Select a node to inspect health, history, dependencies and incident
context.
</p>
) : (
<>
<div className="inspector-callout current-evidence-callout">
<strong>Why {statusLabel(effectiveStatus(selected))} now?</strong>
<span>{selected.health_resolution.explanation || "No current health explanation is available."}</span>
<small>
{selected.health_resolution.selected_provider?.replaceAll("_", " ") ?? "No selected evidence"}
{selected.health_resolution.selected_evidence_id
? ` · ${formatDate(selected.health_evidence.find((item) => item.id === selected.health_resolution.selected_evidence_id)?.observed_at)}`
: ""}
</small>
</div>
{panel === "topology" && !nodes.some((node) => node.id === selected.service.id) && (
<p className="muted compact">The selected service is outside this graph level. Open its application or use Services to inspect the component in context.</p>
)}
{selectedIsRootCause && (
<div className="inspector-callout root-cause-callout">
<strong>Likely root cause</strong>
<span>
{demo?.affected_count ??
selectedIncident?.affected_service_ids.length ??
0}{" "}
downstream services are affected.
</span>
<small>
{(
demo?.affected_service_names ??
selectedIncident?.affected_service_names ??
[]
).join(" · ")}
</small>
</div>
)}
{!selectedIsRootCause && impactIds.has(selected.service.id) && (
<div className="inspector-callout impacted-callout">
<strong>Dependency impact</strong>
<span>
This service is affected through the current root cause.
</span>
</div>
)}
{!hasCurrentNetworkProbe(selected) && (
<div className="inspector-callout observation-callout">
<strong>{monitoringLabel(selected.service.monitoring_state)}</strong>
<span>
{selected.service.monitoring_state === "docker_health"
? "Docker supplies the current health signal; no network probe is configured."
: selected.service.monitoring_state === "appops_observation"
? "AppOps supplies a read-only application health observation; no network probe is configured."
: selected.service.monitoring_state === "observed"
? "Docker runtime is observed, but no verified externally reachable endpoint was detected."
: "No current monitoring signal is configured for this service."}
</span>
{selected.service.probe_reachability === "internal_only" && (
<small>
Internal Docker endpoint evidence is retained for discovery and is never used as an active probe.
</small>
)}
</div>
)}
<dl className="detail-grid">
<div>
<dt>Effective health</dt>
<dd className={`status-${effectiveStatus(selected)}`}>
{statusIcon(effectiveStatus(selected))}{" "}
{statusLabel(effectiveStatus(selected))}
</dd>
</div>
<div>
<dt>Direct health</dt>
<dd>
{statusIcon(selected.direct_status)}{" "}
{statusLabel(selected.direct_status)}
</dd>
</div>
<div>
<dt>Monitoring</dt>
<dd>
{selected.service.monitoring_state.replace("_", " ")} ·{" "}
{selected.service.probe_source.replace("_", " ")}
</dd>
</div>
<div>
<dt>Reachability</dt>
<dd>
{selected.service.probe_reachability.replace("_", " ")} ·{" "}
{selected.service.probe_confidence}
</dd>
</div>
<div>
<dt>Runtime</dt>
<dd>
{selected.service.runtime_state ?? "—"}
{selected.service.runtime_health
? ` · ${selected.service.monitoring_state === "appops_observation" ? "AppOps" : "Docker"} ${selected.service.runtime_health}`
: ""}
</dd>
</div>
<div>
<dt>Application</dt>
<dd>
{selected.service.application ?? selected.service.group ?? "—"}
</dd>
</div>
<div>
<dt>Target</dt>
<dd>
{selected.service.url ??
`${selected.service.host ?? "—"}${selected.service.port ? `:${selected.service.port}` : ""}`}
</dd>
</div>
<div>
<dt>Probe</dt>
<dd>
{selected.service.kind.toUpperCase()} ·{" "}
{hasCurrentNetworkProbe(selected) &&
selected.latest_probe?.latency_ms != null
? `${selected.latest_probe.latency_ms} ms`
: selected.latest_probe
? "Historical probe data"
: "—"}
</dd>
</div>
<div>
<dt>{hasCurrentNetworkProbe(selected) ? "Last probe" : "Probe history"}</dt>
<dd>
{selected.latest_probe
? `${hasCurrentNetworkProbe(selected) ? "Current" : "Historical"} · ${formatDate(selected.latest_probe.checked_at)}`
: "—"}
</dd>
</div>
<div>
<dt>Next scheduled</dt>
<dd>{hasCurrentNetworkProbe(selected) ? formatDate(selected.next_probe_at) : "No active probe"}</dd>
</div>
<div>
<dt>Availability</dt>
<dd>
{hasCurrentNetworkProbe(selected) &&
hasCurrentHistory &&
availability[24]?.uptime_percent != null
? `${availability[24]?.uptime_percent}% (24h)`
: selected.latest_probe
? "Historical probe data"
: "—"}
</dd>
</div>
<div>
<dt>7d / 30d</dt>
<dd>
{hasCurrentNetworkProbe(selected) && hasCurrentHistory
? `${availability[168]?.uptime_percent ?? "—"}% / ${availability[720]?.uptime_percent ?? "—"}%`
: "Not current"}
</dd>
</div>
<div>
<dt>Failures / recovery</dt>
<dd>
{selected.service.failure_streak} /{" "}
{selected.service.recovery_streak}
</dd>
</div>
<div>
<dt>Dependencies</dt>
<dd>{selected.service.depends_on.length}</dd>
</div>
<div>
<dt>Direct dependents</dt>
<dd>{directDependents.get(selected.service.id)?.length ?? 0}</dd>
</div>
<div>
<dt>Group / env</dt>
<dd>
{selected.service.group ?? "—"} /{" "}
{selected.service.environment ?? "—"}
</dd>
</div>
<div>
<dt>Category / lifecycle</dt>
<dd>
{selected.service.category.replace("_", " ")} /{" "}
{selected.service.lifecycle}
</dd>
</div>
<div>
<dt>Discovery source</dt>
<dd>
{selected.service.source_type
? `${selected.service.source_type} · ${selected.service.source_ref ?? "—"}`
: "Manual"}
</dd>
</div>
<div>
<dt>Last seen</dt>
<dd>{formatDate(selected.service.last_seen_at)}</dd>
</div>
</dl>
{hasCurrentNetworkProbe(selected) && selected.latest_probe && <p className="probe-detail">{selected.latest_probe.detail}</p>}
<div className="evidence-list health-evidence-list">
<strong>Health evidence</strong>
<small>
{selected.health_resolution.selected_provider
? `${selected.health_resolution.selected_provider} selected${selected.health_resolution.conflict ? " · conflicting signals retained" : ""}`
: `No current component signal selected · ${selected.health_resolution.reason_code.replaceAll("_", " ")}`}
</small>
<small>{selected.health_resolution.explanation}</small>
{selected.health_evidence.slice(0, 8).map((item) => (
<small key={item.id}>
{item.scope} · {item.provider} · {statusLabel(item.status)} · {item.freshness} · {(item.confidence * 100).toFixed(0)}% · {formatDate(item.observed_at)}
</small>
))}
</div>
{!!selected.sources.length && (
<div className="evidence-list">
<strong>Sources</strong>
{selected.sources.map((source) => (
<small key={source.id}>
{source.source_type}: {source.source_name ?? source.source_id}{" "}
· {source.lifecycle}
</small>
))}
</div>
)}
{!!selected.evidence.length && (
<div className="evidence-list">
<strong>Relationship evidence</strong>
{selected.evidence
.filter((item) => item.active)
.map((item) => (
<small key={item.id}>
{item.confidence} · {item.explanation}
</small>
))}
</div>
)}
{hasCurrentHistory && !!history.length && (
<div
className="history-strip"
title="Probe history, oldest to newest"
aria-label="Probe history, oldest to newest"
>
{[...history].reverse().map((item) => (
<i
key={item.id}
className={`history-dot status-${item.status}`}
title={`${item.status} · ${formatDate(item.checked_at)}`}
aria-label={item.status}
/>
))}
</div>
)}
<div className="button-row wrap">
<button
onClick={() => void probe(selected.service.id)}
disabled={
busyId === selected.service.id ||
Boolean(demo?.active) ||
!hasCurrentNetworkProbe(selected)
}
>
{demo?.active
? "Synthetic data"
: busyId === selected.service.id
? "Probing…"
: hasCurrentNetworkProbe(selected)
? "Probe now"
: "No active probe"}
</button>
{!hasCurrentNetworkProbe(selected) && (
<button className="ghost" onClick={() => beginEdit(selected)}>
Configure endpoint
</button>
)}
<button className="ghost" onClick={() => beginEdit(selected)}>
Edit
</button>
<button className="ghost" onClick={() => void toggleMaintenance()}>
{selected.service.maintenance_enabled
? "End maintenance"
: "Maintenance"}
</button>
<button className="danger" onClick={() => void removeSelected()}>
Delete
</button>
</div>
</>
)}
</section>
);
const renderServiceForm = () => (
<details
className="panel-section form-section"
open={formOpen}
onToggle={(event) => setFormOpen((event.target as HTMLDetailsElement).open)}
>
<summary className="panel-heading">
<span className="eyebrow">
{editingId ? "Edit service" : "Add service"}
</span>
</summary>
<form className="service-form" onSubmit={(event) => void submit(event)}>
<label>
Name
<input
required
value={form.name}
onChange={(event) => setForm({ ...form, name: event.target.value })}
/>
</label>
<div className="field-row">
<label>
Probe type
<select
value={form.kind}
onChange={(event) =>
setForm({ ...form, kind: event.target.value as ServiceKind })
}
>
<option value="http">HTTP(S)</option>
<option value="tcp">TCP</option>
<option value="dns">DNS</option>
<option value="tls">TLS certificate</option>
</select>
</label>
<label>
Criticality
<select
value={form.criticality}
onChange={(event) =>
setForm({
...form,
criticality: event.target.value as Criticality,
})
}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</label>
</div>
{form.kind === "http" ? (
<label>
URL
<input
required
value={form.url}
onChange={(event) =>
setForm({ ...form, url: event.target.value })
}
placeholder="https://service.local/health"
/>
</label>
) : (
<div className="field-row">
<label>
{form.kind === "dns" ? "Hostname" : "Host"}
<input
required
value={form.host}
onChange={(event) =>
setForm({ ...form, host: event.target.value })
}
/>
</label>
{form.kind !== "dns" && (
<label>
Port
<input
required
type="number"
min="1"
max="65535"
value={form.port ?? ""}
onChange={(event) =>
setForm({ ...form, port: Number(event.target.value) })
}
/>
</label>
)}
</div>
)}
<div className="field-row">
<label>
Group
<input
value={form.group}
onChange={(event) =>
setForm({ ...form, group: event.target.value })
}
placeholder="Applications"
/>
</label>
<label>
Environment
<input
value={form.environment}
onChange={(event) =>
setForm({ ...form, environment: event.target.value })
}
/>
</label>
</div>
<fieldset className="semantic-fields">
<legend>Topology identity</legend>
<div className="field-row">
<label>
Application
<input
value={form.application ?? ""}
onChange={(event) =>
setForm({
...form,
application: event.target.value,
application_locked: Boolean(event.target.value),
})
}
placeholder="Derived when blank"
/>
</label>
<label>
Domain
<input
value={form.domain ?? ""}
onChange={(event) =>
setForm({
...form,
domain: event.target.value,
domain_locked: Boolean(event.target.value),
})
}
placeholder="Derived when blank"
/>
</label>
</div>
<div className="field-row">
<label>
Component role
<select
value={form.component_role ?? "unknown"}
onChange={(event) =>
setForm({
...form,
component_role: event.target.value as ComponentRole,
component_role_locked: event.target.value !== "unknown",
})
}
>
{(
[
"unknown",
"frontend",
"api",
"database",
"cache",
"proxy",
"worker",
"storage",
"monitoring",
"agent",
"scheduler",
"auxiliary",
] as ComponentRole[]
).map((role) => (
<option key={role} value={role}>
{role}
</option>
))}
</select>
</label>
<p className="muted compact">
Set a value to lock the operator override; leave it blank/unknown
for deterministic discovery metadata.
</p>
</div>
</fieldset>
<div className="field-row">
<label>
Category
<select
value={form.category ?? "unknown"}
onChange={(event) =>
setForm({
...form,
category: event.target.value as ServiceInput["category"],
})
}
>
{[
"application",
"database",
"cache",
"proxy",
"monitoring",
"development",
"storage",
"infrastructure",
"background_worker",
"system",
"unknown",
].map((item) => (
<option key={item} value={item}>
{item.replace("_", " ")}
</option>
))}
</select>
</label>
<label>
Visibility
<select
value={form.visibility ?? "visible"}
onChange={(event) =>
setForm({
...form,
visibility: event.target.value as ServiceInput["visibility"],
})
}
>
<option value="visible">Visible</option>
<option value="hidden">Hidden</option>
<option value="ignored">Ignored</option>
</select>
</label>
</div>
<label>
Description
<input
value={form.description}
onChange={(event) =>
setForm({ ...form, description: event.target.value })
}
/>
</label>
<label>
Tags
<input
value={form.tags}
onChange={(event) => setForm({ ...form, tags: event.target.value })}
placeholder="api, customer-facing"
/>
</label>
<div className="field-row">
<label>
Interval (s)
<input
type="number"
min="1"
value={form.interval_seconds ?? ""}
onChange={(event) =>
setForm({
...form,
interval_seconds: event.target.value
? Number(event.target.value)
: undefined,
})
}
/>
</label>
<label>
Timeout (s)
<input
type="number"
min="0.1"
step="0.1"
value={form.timeout_seconds ?? ""}
onChange={(event) =>
setForm({
...form,
timeout_seconds: event.target.value
? Number(event.target.value)
: undefined,
})
}
/>
</label>
</div>
<div className="field-row">
<label>
Fail threshold
<input
type="number"
min="1"
value={form.failure_threshold ?? 1}
onChange={(event) =>
setForm({
...form,
failure_threshold: Number(event.target.value),
})
}
/>
</label>
<label>
Recovery threshold
<input
type="number"
min="1"
value={form.recovery_threshold ?? 1}
onChange={(event) =>
setForm({
...form,
recovery_threshold: Number(event.target.value),
})
}
/>
</label>
</div>
<fieldset className="dependency-fieldset">
<legend>Dependencies</legend>
{services
.filter((item) => item.service.id !== editingId)
.map((item) => (
<label className="check-row" key={item.service.id}>
<input
type="checkbox"
checked={form.depends_on.includes(item.service.id)}
onChange={() => toggleDependency(item.service.id)}
/>
{item.service.name}
</label>
))}
{!services.length && (
<span className="muted compact">No services yet.</span>
)}
</fieldset>
<div className="button-row">
<button type="submit">
{editingId ? "Save changes" : "Add service"}
</button>
{editingId && (
<button type="button" className="ghost" onClick={resetForm}>
Cancel
</button>
)}
</div>
</form>
</details>
);
const renderOverview = () => (
<div className="overview-grid">
<section className="panel-section overview-lead">
<span className="eyebrow">Service intelligence</span>
<h2>
{demo?.active
? "Northstar incident at a glance"
: "Operational overview"}
</h2>
<p className="muted">
{demo?.active
? "Synthetic data shows how one unavailable dependency propagates through a production topology."
: "A compact view of the real infrastructure topology, health evidence and active incidents."}
</p>
<div className="summary-cards">
<button onClick={() => { setStatusFilter("all"); setPanel("services"); }}>
<strong>{services.length}</strong>
<span>visible services</span>
</button>
<button className="status-healthy" onClick={() => { setStatusFilter("healthy"); setPanel("services"); }}>
<strong>{counts.healthy ?? 0}</strong>
<span>healthy</span>
</button>
<button className="status-degraded" onClick={() => { setStatusFilter("degraded"); setPanel("services"); }}>
<strong>{counts.degraded ?? 0}</strong>
<span>degraded</span>
</button>
<button className="status-down" onClick={() => { setStatusFilter("down"); setPanel("services"); }}>
<strong>{counts.down ?? 0}</strong>
<span>down</span>
</button>
</div>
{(overview?.total_services ?? services.length) > services.length && (
<p className="muted compact">{(overview?.total_services ?? services.length) - services.length} hidden services are excluded from this operational view.</p>
)}
{!!providerStatuses.length && (
<div className="provider-statuses" aria-label="Health data providers">
{providerStatuses.map((provider) => (
<small key={provider.provider} className={`status-${provider.status}`}>
{provider.provider.replace("_", " ")}: {provider.status}
</small>
))}
</div>
)}
</section>
<section className="panel-section overview-incident">
<span className="eyebrow">Incident posture</span>
<h3>
{incident?.active
? `${incident.candidates.length} likely root cause${incident.candidates.length === 1 ? "" : "s"}`
: "No active incident"}
</h3>
{incident?.candidates.length ? (
<div className="candidate-list overview-root-list">
{incident.candidates.map((candidate) => (
<button key={candidate.service_id} className="candidate" onClick={() => {
const record = incident.incidents.find((item) =>
item.root_service_id === candidate.service_id && item.status !== "resolved");
if (record) void openIncident(record);
else { setSelectedId(candidate.service_id); setPanel("topology"); }
}}>
<strong>{candidate.service_name}</strong>
<small>Affects {candidate.affected_service_ids.length} downstream services</small>
<small>{candidate.reason}</small>
</button>
))}
</div>
) : (
<p className="muted compact">
Healthy dependency graph. Events will appear here when state
changes.
</p>
)}
</section>
<section className="panel-section coverage-overview">
<div className="panel-heading">
<div>
<span className="eyebrow">Monitoring coverage</span>
<h3>
{services.length
? `${resolvedComponentCount}/${services.length} with current health evidence`
: "No services yet"}
</h3>
</div>
<button
className="ghost"
onClick={() => {
setStatusFilter("unresolved");
setMonitoringFilter("all");
setPanel("services");
}}
disabled={unresolvedComponentCount === 0}
>
Review unresolved
</button>
</div>
<div className="coverage-breakdown">
{(
[
"active_probe",
"docker_health",
"appops_observation",
"observed",
"unmonitored",
] as MonitoringState[]
).map((state) => (
<div key={state}>
<strong>{coverageCounts[state] ?? 0}</strong>
<span>{monitoringLabel(state)}</span>
</div>
))}
</div>
<p className="muted compact">
{unresolvedComponentCount} services still have unresolved health. Runtime observation alone confirms presence, not health. Active probes require a verified endpoint.
</p>
</section>
<section className="panel-section discovery-overview">
<span className="eyebrow">Infrastructure discovery</span>
<h3>
{discoverySuggestions.length
? `${discoverySuggestions.length} open findings across runs`
: "Discovery is up to date"}
</h3>
<p className="muted compact">
{discoveryRun
? `Most recent run ${formatDate(discoveryRun.completed_at ?? discoveryRun.started_at)}: ${discoveryRun.diff.new} new, ${discoveryRun.diff.changed} changed and ${discoveryRun.diff.missing} missing in that run.`
: "Run discovery to compare Docker and AppOps observations with the canonical topology."}
</p>
<div className="button-row wrap">
<button onClick={() => setPanel("discovery")}>Open discovery</button>
{!demo?.active && (
<button
className="ghost"
onClick={() => void runProductionDiscovery()}
disabled={discoveryBusy}
>
Run now
</button>
)}
</div>
</section>
<section className="panel-section event-panel">
<div className="panel-heading">
<span className="eyebrow">Recent events</span>
<button className="text-button" onClick={() => setPanel("incidents")}>
Open journal
</button>
</div>
<div className="event-list">
{(overview?.recent_events ?? events.slice(0, 8)).map((item) => (
<div className="event-row" key={item.id}>
<span className="event-dot" />
<div>
<strong>{item.message}</strong>
<small>{formatDate(item.created_at)}</small>
</div>
</div>
))}
{!(overview?.recent_events.length || events.length) && (
<div className="empty-inline">No events yet.</div>
)}
</div>
</section>
</div>
);
const renderIncidents = () => (
<div className="incident-workspace">
<section className="panel-section incident-list-panel">
<div className="panel-heading">
<div>
<span className="eyebrow">Persistent incident journal</span>
<h3>
{incident?.active
? "Active and historical incidents"
: "Incident history"}
</h3>
</div>
<span className="health-pill">
{incident?.incidents.length ?? 0} records
</span>
</div>
<div className="review-filters">
<label>Search incidents<input value={incidentSearch} onChange={(event) => setIncidentSearch(event.target.value)} placeholder="Service or cause" /></label>
<label>Status<select value={incidentStatusFilter} onChange={(event) => setIncidentStatusFilter(event.target.value)}>
<option value="active">Active</option><option value="all">All</option><option value="open">Open</option><option value="acknowledged">Acknowledged</option><option value="resolved">Resolved</option>
</select></label>
</div>
<p className="muted compact">Showing {visibleIncidents.length} of {incident?.incidents.length ?? 0} records.</p>
<div className="candidate-list">
{visibleIncidents.map((item) => (
<button
className={`candidate incident-card ${item.status}`}
key={item.id}
onClick={() => void openIncident(item)}
>
<div className="incident-card-top">
<span
className={`health-pill status-${item.status === "resolved" ? "healthy" : item.status === "acknowledged" ? "degraded" : "down"}`}
>
{item.status}
</span>
<span>{item.severity}</span>
</div>
<strong>{item.title}</strong>
<small>Root cause · {item.root_service_name}</small>
<small>
{item.affected_service_ids.length} downstream affected · started{" "}
{formatDate(item.started_at)}
</small>
</button>
))}
{!visibleIncidents.length && (
<div className="empty-inline">No incidents match these filters.</div>
)}
</div>
</section>
<section className="panel-section incident-detail-panel">
{incidentDetail ? (
<>
<span className="eyebrow">Incident timeline</span>
<h3>{incidentDetail.incident.title}</h3>
<div className="button-row wrap">
<span
className={`health-pill status-${incidentDetail.incident.status === "resolved" ? "healthy" : incidentDetail.incident.status === "acknowledged" ? "degraded" : "down"}`}
>
{incidentDetail.incident.status}
</span>
{incidentDetail.incident.status === "open" && (
<button onClick={() => void acknowledge()}>
Acknowledge incident
</button>
)}
<button
className="ghost"
onClick={() => {
setPanel("topology");
setBlastRadius(true);
}}
>
Show impact
</button>
</div>
<p className="muted compact">
{incidentDetail.incident.root_cause_reason}
</p>
<div className="timeline">
{incidentDetail.timeline.map((item) => (
<div className="timeline-row" key={item.id}>
<span className="timeline-line" />
<div>
<strong>{item.message}</strong>
<small>{formatDate(item.created_at)}</small>
</div>
</div>
))}
</div>
</>
) : (
<p className="muted compact">
Select an incident to inspect its root cause, acknowledgement state,
timeline and blast radius.
</p>
)}
</section>
</div>
);
const renderServicesTable = () => (
<div className="services-workspace">
<section className="panel-section table-panel">
<div className="table-toolbar">
<label className="search-field">
Search
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Name, tag or description"
/>
</label>
<button
type="button"
onClick={() => {
resetForm();
setSelectedId(null);
setFormOpen(true);
}}
>
+ Add service
</button>
<label>
Status
<select
value={statusFilter}
onChange={(event) =>
setStatusFilter(event.target.value as FilterStatus)
}
>
<option value="all">All statuses</option>
<option value="unresolved">Unresolved health</option>
{(
[
"healthy",
"degraded",
"down",
"recovering",
"unknown",
"unmonitored",
"maintenance",
] as HealthStatus[]
).map((item) => (
<option key={item} value={item}>
{statusLabel(item)}
</option>
))}
</select>
</label>
<label>
Monitoring
<select
value={monitoringFilter}
onChange={(event) =>
setMonitoringFilter(event.target.value as MonitoringFilter)
}
>
<option value="all">Any source</option>
{(
[
"active_probe",
"docker_health",
"appops_observation",
"observed",
"unmonitored",
] as MonitoringState[]
).map((item) => (
<option key={item} value={item}>
{monitoringLabel(item)}
</option>
))}
</select>
</label>
<label>
Group
<select
value={groupFilter}
onChange={(event) => setGroupFilter(event.target.value)}
>
<option value="all">All groups</option>
{groups.map((item) => (
<option key={item}>{item}</option>
))}
</select>
</label>
<label>
Environment
<select
value={environmentFilter}
onChange={(event) => setEnvironmentFilter(event.target.value)}
>
<option value="all">All environments</option>
{environments.map((item) => (
<option key={item}>{item}</option>
))}
</select>
</label>
<label>
Criticality
<select
value={criticalityFilter}
onChange={(event) => setCriticalityFilter(event.target.value)}
>
<option value="all">All criticality</option>
{(["low", "medium", "high", "critical"] as Criticality[]).map(
(item) => (
<option key={item} value={item}>
{item}
</option>
),
)}
</select>
</label>
</div>
<div className="service-table">
<div className="service-table-head">
<span>Service</span>
<span>Application</span>
<span>Runtime</span>
<span>Monitoring</span>
<span>Health</span>
<span>Latency</span>
<span>24h</span>
</div>
{filteredServices.map((item) => (
<button
className="service-table-row"
key={item.service.id}
aria-pressed={selectedId === item.service.id}
onClick={() => setSelectedId(item.service.id)}
>
<span>
<strong>{item.service.name}</strong>
<small>
{item.service.group ?? "Ungrouped"} ·{" "}
{item.service.environment ?? "—"} · {item.service.criticality}
</small>
</span>
<span>
{item.service.application ?? item.service.group ?? "—"}
</span>
<span>
{item.service.runtime_state ?? "—"}
{item.service.runtime_health
? ` · ${item.service.runtime_health}`
: ""}
</span>
<span className="monitoring-cell">
{monitoringLabel(item.service.monitoring_state)}
<small>{item.service.probe_source.replace("_", " ")}</small>
</span>
<span className={`status-${effectiveStatus(item)}`}>
{statusIcon(effectiveStatus(item))}{" "}
{statusLabel(effectiveStatus(item))}
</span>
<span>
{hasCurrentNetworkProbe(item) && item.latest_probe?.latency_ms != null
? `${item.latest_probe.latency_ms} ms`
: "—"}
</span>
<span>
{hasCurrentNetworkProbe(item) && item.availability_24h?.uptime_percent != null
? `${item.availability_24h.uptime_percent.toFixed(1)}%`
: item.latest_probe
? "Historical"
: "—"}
</span>
</button>
))}
{!filteredServices.length && (
<div className="empty-inline">
No services match the current filters.
</div>
)}
</div>
</section>
{(selected || formOpen) && (
<aside className="service-drawer" aria-label="Service details">
<div className="drawer-header">
<span className="eyebrow">Service details</span>
<button
className="ghost"
onClick={() => {
setSelectedId(null);
resetForm();
}}
>
Close
</button>
</div>
{selected && renderInspector()}
{formOpen && renderServiceForm()}
</aside>
)}
</div>
);
const renderDiscovery = () => {
const diff = discoveryRun?.diff;
const currentRun = discoveryStatus?.current;
return (
<div className="discovery-workspace">
<section className="panel-section discovery-panel">
<span className="eyebrow">Production infrastructure</span>
<h2>Discover the real environment</h2>
<p className="muted compact">
Read-only discovery inspects Docker metadata and optional AppOps
enrichment. A run records observations and suggestions; it does not
change the canonical topology until you explicitly reconcile it.
</p>
<div className="field-row">
<label>
Docker host
<input
value={dockerHost}
onChange={(event) => setDockerHost(event.target.value)}
placeholder="unix:///var/run/docker.sock"
/>
</label>
<label>
AppOps URL
<input
value={appopsUrl}
onChange={(event) => setAppopsUrl(event.target.value)}
placeholder="optional read-only URL"
/>
</label>
</div>
<div className="button-row wrap">
<button
onClick={() => void runProductionDiscovery()}
disabled={discoveryBusy}
>
{discoveryBusy ? "Discovering…" : "Run discovery"}
</button>
<button
className="ghost"
onClick={() => void refreshDiscovery()}
disabled={discoveryBusy}
>
Refresh state
</button>
<button
className="ghost"
onClick={() => void discoverDocker()}
disabled={dockerBusy}
>
Preview Docker candidates
</button>
</div>
{currentRun && (
<p className="notice-inline">
Discovery is currently {currentRun.status}.
</p>
)}
{discoveryRun && (
<div className="discovery-summary">
<div>
<strong>{discoveryRun.status}</strong>
<span>last run</span>
</div>
<div>
<strong>{diff?.new ?? 0}</strong>
<span>new</span>
</div>
<div>
<strong>{diff?.changed ?? 0}</strong>
<span>changed</span>
</div>
<div>
<strong>{diff?.missing ?? 0}</strong>
<span>missing</span>
</div>
<div>
<strong>{diff?.new_suggestions ?? 0}</strong>
<span>new suggestions</span>
</div>
</div>
)}
<div className="discovery-callout">
<strong>Canonical topology is deliberate.</strong>
<span>
Importing high-confidence services and relationships is explicit.
Low-confidence network hints remain review-only and never affect
health propagation.
</span>
</div>
<div className="probe-safety-callout">
<strong>Monitoring coverage repair</strong>
<span>
Published endpoints are preflighted before activation. Container-only
Docker DNS is discovery evidence only; AppOps, Docker health and
runtime observations retain their own provenance.
</span>
<div className="button-row wrap">
<button
className="ghost"
onClick={() => void repairDiscoveredProbes(true)}
disabled={discoveryBusy || !discoveryRun}
>
Preview coverage repair
</button>
<button
onClick={() => void repairDiscoveredProbes(false)}
disabled={
discoveryBusy || !probeRepair || probeRepair.dry_run === false || probeRepair.changed === 0
}
>
Apply verified changes
</button>
</div>
{probeRepair && (
<small>
{probeRepair.dry_run ? "Preview" : "Applied"} ·{" "}
{probeRepair.changed} changed · {probeRepair.disabled_unsafe}{" "}
unsafe disabled · {probeRepair.enabled_published} published
endpoints verified · {probeRepair.docker_health_observed} Docker
health · {probeRepair.appops_observed} AppOps ·{" "}
{probeRepair.runtime_observed} runtime observed ·{" "}
{probeRepair.unmonitored} unmonitored
</small>
)}
{!!probeRepair?.changes.length && (
<div className="coverage-change-list">
{probeRepair.changes.slice(0, 12).map((change) => (
<div key={change.service_id}>
<strong>{change.service_name}</strong>
<small>
{monitoringLabel(change.next_monitoring_state)} ·{" "}
{change.next_probe_source
? change.next_probe_source.replace("_", " ")
: "no source"}
{change.next_endpoint ? ` · ${change.next_endpoint}` : ""}
{change.next_probe_confidence
? ` · ${change.next_probe_confidence}`
: ""}
</small>
<small>{change.reason}</small>
</div>
))}
</div>
)}
</div>
<div className="button-row wrap">
<button
onClick={() => setReconcileConfirm(true)}
disabled={
discoveryBusy ||
!discoveryRun ||
discoveryRun.status !== "completed"
}
>
Review import
</button>
<span className="muted compact">
{discoverySuggestions.length} open suggestions ·{" "}
{discoveryItems.length} observations
</span>
</div>
{reconcileConfirm && (
<div className="review-confirm" role="alert">
<strong>Import discovery findings?</strong>
<p>Last run: {formatDate(discoveryRun?.completed_at ?? discoveryRun?.started_at)}. {discoveryRun?.diff.new ?? 0} new and {discoveryRun?.diff.changed ?? 0} changed candidates; {discoverySuggestions.filter((item) => ["high", "confirmed"].includes(item.confidence)).length} open high-confidence suggestions. Reconciliation can also update lifecycle and retire missing services. Review the candidates before applying.</p>
<div className="button-row wrap"><button className="ghost" onClick={() => setReconcileConfirm(false)}>Cancel</button><button onClick={() => void reconcileProduction()} disabled={discoveryBusy}>Apply import</button></div>
</div>
)}
</section>
<section className="panel-section suggestion-panel">
<div className="panel-heading">
<div>
<span className="eyebrow">Evidence review</span>
<h3>Open discovery suggestions</h3>
</div>
<span className="health-pill">
{discoverySuggestions.length} open
</span>
</div>
<p className="muted compact">
Accept individual findings only after checking their evidence.
Rejected findings are retained and will not reappear on every scan.
</p>
<div className="review-filters">
<label>Search findings<input value={suggestionSearch} onChange={(event) => { setSuggestionSearch(event.target.value); setSuggestionLimit(30); }} placeholder="Service, source or evidence" /></label>
<label>Confidence<select value={suggestionConfidence} onChange={(event) => { setSuggestionConfidence(event.target.value); setSuggestionLimit(30); }}><option value="actionable">High + confirmed</option><option value="all">All</option><option value="confirmed">Confirmed</option><option value="high">High</option><option value="medium">Medium</option><option value="low">Low</option></select></label>
<label>Type<select value={suggestionType} onChange={(event) => { setSuggestionType(event.target.value); setSuggestionLimit(30); }}><option value="all">All</option><option value="service">Service</option><option value="relation">Relationship</option></select></label>
</div>
<p className="muted compact">Showing {Math.min(suggestionLimit, visibleSuggestions.length)} of {visibleSuggestions.length} matching findings ({discoverySuggestions.length} open).</p>
<div className="suggestion-list">
{visibleSuggestions.slice(0, suggestionLimit).map((suggestion) => {
const source = services.find(
(item) => item.service.id === suggestion.source_service_id,
)?.service;
const target = services.find(
(item) => item.service.id === suggestion.target_service_id,
)?.service;
return (
<article className="suggestion-card" key={suggestion.id}>
<div className="suggestion-card-heading">
<div>
<span className="eyebrow">
{suggestion.suggestion_type}
</span>
<strong>
{source?.name ??
suggestion.proposed_name ??
suggestion.source_ref ??
"Discovered item"}
{suggestion.suggestion_type === "relation" && (
<>
{" "}
<span aria-hidden="true">→</span>{" "}
{target?.name ?? suggestion.target_ref ?? "target"}
</>
)}
</strong>
</div>
<span
className={`health-pill status-${suggestion.confidence === "low" ? "unknown" : suggestion.confidence === "medium" ? "degraded" : "healthy"}`}
>
{suggestion.confidence}
</span>
</div>
{suggestion.suggestion_type === "service" && (
<small>
{suggestion.proposed_category.replace("_", " ")} ·{" "}
{suggestion.proposed_url ??
`${suggestion.proposed_host ?? "no endpoint"}${suggestion.proposed_port ? `:${suggestion.proposed_port}` : ""}`}
</small>
)}
{suggestion.suggestion_type === "relation" && (
<small>
{suggestion.relation_type ?? "relationship"} · canonical
health edge only after acceptance
</small>
)}
<div className="evidence-list">
<strong>Evidence</strong>
{suggestion.evidence.map((item) => (
<small key={item}>{item}</small>
))}
</div>
<div className="button-row wrap">
<button
onClick={() =>
void reviewSuggestion(suggestion.id, "accept")
}
disabled={discoveryBusy}
>
Accept
</button>
<button
className="ghost"
onClick={() =>
void reviewSuggestion(suggestion.id, "reject")
}
disabled={discoveryBusy}
>
Reject
</button>
{suggestion.suggestion_type === "service" && (
<button
className="ghost"
onClick={() => {
const candidate = dockerCandidates.find(
(item) =>
item.stable_source_ref === suggestion.source_ref,
);
if (candidate) prefillDockerCandidate(candidate);
}}
>
Inspect probe
</button>
)}
</div>
</article>
);
})}
{!visibleSuggestions.length && (
<div className="empty-inline">
No findings match these filters.
</div>
)}
</div>
{visibleSuggestions.length > suggestionLimit && <button className="ghost show-more" onClick={() => setSuggestionLimit((current) => current + 30)}>Show 30 more findings</button>}
</section>
<section className="panel-section discovery-observations">
<div className="panel-heading">
<div>
<span className="eyebrow">Observed infrastructure</span>
<h3>Latest source observations</h3>
</div>
<span className="health-pill">
{dockerCandidates.length} Docker candidates
</span>
</div>
<div className="observation-list">
{discoveryItems.slice(0, 24).map((item) => (
<div className="observation-row" key={item.id}>
<strong>{item.source_name}</strong>
<span>{item.change_type}</span>
<small>
{item.lifecycle} · {formatDate(item.observed_at)}
</small>
</div>
))}
{!discoveryItems.length && (
<div className="empty-inline">
No completed discovery run yet.
</div>
)}
</div>
<details className="candidate-details">
<summary>
Show candidate inventory ({dockerCandidates.length})
</summary>
<div className="docker-list compact-list">
{dockerCandidates.slice(0, 80).map((candidate) => (
<article
className="docker-card"
key={`${candidate.container_id}-${candidate.stable_source_ref}`}
>
<div>
<strong>{candidate.name}</strong>
<span>
{candidate.category.replace("_", " ")} · {candidate.state}
{candidate.health ? ` · ${candidate.health}` : ""}
</span>
</div>
<small>
{candidate.image} · {candidate.import_state}
</small>
<small>
{candidate.stable_source_ref ?? "no stable identity"}
{candidate.compose_project
? ` · compose ${candidate.compose_project}`
: ""}
</small>
{candidate.proxy_route_evidence.map((item) => (
<small key={item}>{item}</small>
))}
{candidate.probe_candidates.map((probe) => (
<small key={`${probe.source}-${probe.url ?? probe.host ?? "none"}-${probe.port ?? ""}`}>
{probe.source.replace("_", " ")} · {probe.reachability}
{probe.url ??
(probe.host
? ` · ${probe.host}${probe.port ? `:${probe.port}` : ""}`
: "")}
{probe.preflight_detail ? ` · ${probe.preflight_detail}` : ""}
</small>
))}
</article>
))}
</div>
</details>
</section>
</div>
);
};
const renderChanges = () => {
const snapshotIndex = topologySnapshots.findIndex((item) => item.id === selectedSnapshotId);
const selectedSnapshot = topologySnapshots[snapshotIndex] ?? topologySnapshots[0];
const previousSnapshot = topologySnapshots[snapshotIndex + 1];
const changes = topologyDiff
? [...topologyDiff.added, ...topologyDiff.changed, ...topologyDiff.removed]
: [];
const inspectChange = (entityId: string, after: Record<string, unknown> | null) => {
const serviceId =
services.some((item) => item.service.id === entityId)
? entityId
: typeof after?.source_service_id === "string"
? after.source_service_id
: null;
if (!serviceId) return;
setSelectedId(serviceId);
setPanel("topology");
setInspectorOpen(true);
};
return (
<section className="panel-section change-intelligence">
<span className="eyebrow">Change intelligence</span>
<h3>Topology evidence over time</h3>
<p className="muted compact">
Snapshots are compact canonical topology captures. They compare service identity, lifecycle, semantic metadata and dependencies without changing your topology.
</p>
{selectedSnapshot && (
<label className="snapshot-picker">Compare snapshot
<select value={selectedSnapshot.id} onChange={(event) => { setSelectedSnapshotId(event.target.value); setTopologyDiff(null); setChangeLimit(30); }}>
{topologySnapshots.map((item) => <option key={item.id} value={item.id}>{formatDate(item.created_at)} · {item.service_count} services</option>)}
</select>
</label>
)}
{selectedSnapshot && snapshotCheckedAt - new Date(selectedSnapshot.created_at).getTime() > 24 * 60 * 60 * 1000 && (
<p className="notice-inline">This topology snapshot is more than 24 hours old. Snapshots update after discovery reconciliation; current health evidence is shown in Overview and Services. <button className="text-button" onClick={() => setPanel("discovery")}>Open discovery</button></p>
)}
{selectedSnapshot ? (
<div className="detail-grid">
<div><dt>Selected topology snapshot</dt><dd>{formatDate(selectedSnapshot.created_at)}</dd></div>
<div><dt>Compared with</dt><dd>{formatDate(previousSnapshot?.created_at)}</dd></div>
<div><dt>Services / relationships</dt><dd>{selectedSnapshot.service_count} / {selectedSnapshot.relationship_count}</dd></div>
<div><dt>Added</dt><dd>{topologyDiff?.summary.added ?? 0}</dd></div>
<div><dt>Changed</dt><dd>{topologyDiff?.summary.changed ?? 0}</dd></div>
<div><dt>Removed</dt><dd>{topologyDiff?.summary.removed ?? 0}</dd></div>
</div>
) : <p className="muted compact">A topology snapshot will be captured after the next reconciliation.</p>}
{changes.length ? (
<>
<p className="muted compact">Showing {Math.min(changeLimit, changes.length)} of {changes.length} changes. Expand an item to inspect the changed fields.</p>
<div className="change-list">
{changes.slice(0, changeLimit).map((change) => {
const fields = changedFields(change.before, change.after);
return <details key={`${change.change_type}-${change.entity_type}-${change.entity_id}`} className="change-card">
<summary><strong>{change.name}</strong><small>{change.change_type} {change.entity_type} · {fields.length ? `${fields.length} fields changed` : "record added or removed"}</small></summary>
{fields.length > 0 && <div className="change-fields">{fields.map((item) => <div key={item.field}><strong>{item.field.replaceAll("_", " ")}</strong><small>{item.before} → {item.after}</small></div>)}</div>}
{change.after && <button className="ghost" onClick={() => inspectChange(change.entity_id, change.after)}>Inspect in topology</button>}
</details>;
})}
</div>
{changes.length > changeLimit && <button className="ghost show-more" onClick={() => setChangeLimit((current) => current + 30)}>Show 30 more changes</button>}
</>
) : (
<p className="muted compact">No topology difference is recorded between the two latest snapshots.</p>
)}
</section>
);
};
const renderConfig = () => (
<section className="panel-section config-panel">
<span className="eyebrow">Configuration v2</span>
<h3>Import / export</h3>
<p className="muted compact">
Imports validate the complete graph atomically before committing.
Existing v1 documents remain importable.
</p>
<div className="field-row">
<label>
Format
<select
value={configFormat}
onChange={(event) =>
setConfigFormat(event.target.value as "yaml" | "json")
}
>
<option value="yaml">YAML</option>
<option value="json">JSON</option>
</select>
</label>
<label>
Mode
<select
value={configMode}
onChange={(event) =>
setConfigMode(event.target.value as "merge" | "replace")
}
>
<option value="merge">Merge</option>
<option value="replace">Replace</option>
</select>
</label>
</div>
<label className="config-label">
Configuration
<textarea
value={configText}
onChange={(event) => setConfigText(event.target.value)}
spellCheck={false}
/>
</label>
<div className="button-row wrap">
<button onClick={() => void importConfig()}>Apply configuration</button>
<button className="ghost" onClick={() => void exportConfig("yaml")}>
YAML
</button>
<button className="ghost" onClick={() => void exportConfig("json")}>
JSON
</button>
</div>
</section>
);
return (
<main className={`shell ${demo?.active ? "demo-mode" : ""}`}>
<header className="topbar">
<div className="brand-lockup">
<img src="/brand/servicemap-mark.svg" alt="" />
<div>
<span className="eyebrow">
Production infrastructure intelligence
</span>
<h1>
ServiceMap <small>v1.6.2</small>
</h1>
</div>
</div>
<div className="toolbar">
<button
className="ghost"
onClick={() => void loadDemo()}
disabled={demoBusy}
>
{demo?.active ? "Reset demo" : "Explore demo"}
</button>
<button className="ghost" onClick={() => void exportConfig("yaml")}>
Export YAML
</button>
<button
onClick={() => void probeAll()}
disabled={busyAll || Boolean(demo?.active)}
>
{demo?.active
? "Synthetic probes"
: busyAll
? "Probing all…"
: "Probe all"}
</button>
</div>
</header>
<section
className="operations-strip"
aria-label="Current operational picture"
>
<div>
<span className="eyebrow">Operational picture</span>
<strong>
{incident?.active
? "Active dependency incident"
: "Topology and monitoring are live"}
</strong>
<small>
{services.length} services · {resolvedComponentCount} with current component evidence ·{" "}
{unresolvedComponentCount} unresolved
</small>
</div>
<div className="health-summary">
{(["healthy", "degraded", "down", "unknown"] as HealthStatus[]).map(
(item) => (
<div key={item} className={`status-${item}`}>
<strong>{counts[item] ?? 0}</strong>
<span>{item}</span>
</div>
),
)}
</div>
</section>
{error && (
<div className="error-banner" role="alert">
{error}
</div>
)}
{notice && (
<div className="notice-banner" role="status">
<span>{notice}</span><button className="text-button" aria-label="Dismiss notice" onClick={() => setNotice(null)}>Dismiss</button>
</div>
)}
{demo?.active && (
<section
className="demo-banner"
role="status"
aria-label="Explore demo scenario"
aria-live="polite"
aria-busy={demoBusy}
>
<div>
<span className="eyebrow">
Demo scenario · synthetic monitoring data
</span>
<strong>PostgreSQL is unavailable.</strong>
<p>
ServiceMap identified it as the likely root cause affecting{" "}
{demo.affected_count} downstream services. Select a node to
inspect health and history.
</p>
</div>
<div className="demo-actions">
<button
className="ghost"
onClick={() => {
setPanel("topology");
setTopologyMode("impact");
setBlastRadius(true);
}}
>
Trace impact
</button>
<button
className="ghost"
onClick={() => {
const dataDomain = semanticTopology?.domains.find(
(item) => item.name === "Data",
);
if (dataDomain) setTopologyLocation(domainLocation(dataDomain.id));
setPanel("topology");
}}
>
Explore data
</button>
<button className="ghost" onClick={() => setPanel("incidents")}>
View incident
</button>
<button
className="ghost"
onClick={() => void resetDemo()}
disabled={demoBusy}
>
Reset demo
</button>
</div>
<DemoWalkthrough
step={demoStep}
affectedCount={demo.affected_count}
busy={demoBusy}
onStepChange={setDemoWalkthroughStep}
onExit={() => void exitDemo()}
/>
</section>
)}
{demoConfirm && (
<section className="demo-confirm" role="alert">
<div>
<strong>
Explore demo will temporarily replace this topology.
</strong>
<p>
Your current services, probes, incidents and event history will be
snapshotted and restored when you choose Exit demo.
</p>
</div>
<div className="demo-actions">
<button className="ghost" onClick={() => setDemoConfirm(false)}>
Cancel
</button>
<button onClick={() => void startDemo()} disabled={demoBusy}>
Snapshot and explore
</button>
</div>
</section>
)}
<nav className="panel-tabs" aria-label="ServiceMap workspaces">
{(
[
"overview",
"topology",
"incidents",
"services",
"discovery",
"changes",
"config",
] as Panel[]
).map((item) => (
<button
key={item}
className={panel === item ? "active" : ""}
onClick={() => { setPanel(item); setNotice(null); setReconcileConfirm(false); }}
>
{item === "incidents"
? `Incidents${incident?.active ? " •" : ""}`
: item[0].toUpperCase() + item.slice(1)}
</button>
))}
</nav>
{panel === "overview" && renderOverview()}
{panel === "incidents" && renderIncidents()}
{panel === "services" && renderServicesTable()}
{panel === "discovery" && renderDiscovery()}
{panel === "changes" && renderChanges()}
{panel === "config" && renderConfig()}
{panel === "topology" && (
<>
<TopologyCommandBar
location={topologyLocation}
setLocation={setTopologyLocation}
selectedDomain={selectedDomain ?? null}
selectedApplication={selectedApplication ?? null}
mode={topologyMode}
setMode={setTopologyMode}
setImpact={setBlastRadius}
showImpact={Boolean(demo?.active || selectedIncident)}
search={search}
setSearch={setSearch}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
monitoringFilter={monitoringFilter}
setMonitoringFilter={setMonitoringFilter}
groupFilter={groupFilter}
environmentFilter={environmentFilter}
setEnvironmentFilter={setEnvironmentFilter}
criticalityFilter={criticalityFilter}
setCriticalityFilter={setCriticalityFilter}
domainFilter={domainFilter}
setDomainFilter={setDomainFilter}
applicationFilter={serviceApplicationFilter}
setApplicationFilter={setServiceApplicationFilter}
lifecycleFilter={lifecycleFilter}
setLifecycleFilter={setLifecycleFilter}
filtersOpen={filtersOpen}
setFiltersOpen={setFiltersOpen}
domains={domains}
applications={applications}
environments={environments}
activeFilters={activeTopologyFilters}
onFit={() => flowInstance.current?.fitView({ padding: 0.16 })}
onArrange={() => void autoArrangeGraph()}
inspectorOpen={inspectorOpen}
onToggleInspector={() => setInspectorOpen(!inspectorOpen)}
/> <section
className={`workspace topology-workspace ${inspectorOpen ? "" : "inspector-collapsed"}`}
>
<div className="canvas-card">
{loading && !services.length ? (
<div className="empty-state">
<strong>Loading topology…</strong>
<span>Reading services, incidents and live state.</span>
</div>
) : services.length ? (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
fitView
minZoom={0.2}
nodesDraggable={editMode && topologyLocation.level === "components"}
nodesConnectable={editMode && topologyLocation.level === "components"}
onInit={(instance) => {
flowInstance.current = instance;
instance.fitView({ padding: 0.16 });
}}
onNodeClick={(_, node) => {
setSelectedEdgeId(null);
const data = node.data as ServiceNodeData;
if (data.nodeKind === "domain") {
setTopologyLocation(domainLocation(node.id));
setComponentScope(null);
setTopologyMode("landscape");
} else if (
data.nodeKind === "application" ||
node.id.startsWith("app:")
) {
const members = data.memberIds ?? [];
setComponentScope({ label: data.name, memberIds: members });
setTopologyLocation(applicationLocation(data.domainId ?? null, node.id));
setApplicationFilter("all");
setTopologyMode("landscape");
const member =
members.find((id) => id === rootCauseId) ?? members[0];
if (member) setSelectedId(member);
} else setSelectedId(node.id);
setInspectorOpen(true);
setPanel("topology");
}}
onNodeMouseEnter={(_, node) => setHoveredNodeId(node.id)}
onNodeMouseLeave={() => setHoveredNodeId(null)}
onPaneClick={() => setSelectedEdgeId(null)}
onEdgeClick={(_, edge) => {
const evidence = edgeEvidence.find(
(item) =>
item.source_service_id === edge.target &&
item.target_service_id === edge.source,
);
setSelectedEdgeId(evidence?.id ?? null);
setInspectorOpen(true);
}}
onConnect={(connection) => void createManualEdge(connection)}
onNodeDragStop={(_, node) =>
void persistNodePosition(node as ServiceNode)
}
>
<Background />
<Controls />
{showMiniMap && <MiniMap pannable zoomable />}
</ReactFlow>
) : (
<div className="empty-state">
<strong>No services yet</strong>
<span>
Add an endpoint, import configuration or explore the synthetic
demo.
</span>
</div>
)}
</div>
{(inspectorOpen || formOpen) && (
<aside ref={sidePanelRef} className="side-panel topology-drawer">
{inspectorOpen && (
<>{selectedEdge ? renderEdgeInspector() : renderInspector()}</>
)}
<div className="graph-tools panel-section">
<div className="panel-heading">
<span className="eyebrow">Topology controls</span>
<button
className="ghost"
onClick={() => setInspectorOpen(!inspectorOpen)}
>
{inspectorOpen ? "Hide inspector" : "Show inspector"}
</button>
</div>
<div className="mode-tabs" aria-label="Topology mode">
{(
[
"landscape",
"dependencies",
"impact",
] as TopologyMode[]
).map((mode) => (
<button
key={mode}
className={topologyMode === mode ? "active" : "ghost"}
onClick={() => {
setTopologyMode(mode);
if (mode === "impact") setBlastRadius(true);
else {
setBlastRadius(false);
setComponentScope(null);
if (mode === "landscape") {
setSemanticScope({
domainId: null,
applicationId: null,
});
}
}
}}
>
{mode === "impact"
? "Incident impact"
: mode[0].toUpperCase() + mode.slice(1)}
</button>
))}
</div>
{semanticTopology &&
(semanticScope.domainId || semanticScope.applicationId) && (
<div
className="topology-breadcrumbs"
aria-label="Topology hierarchy"
>
<button
className="text-button"
onClick={() => {
setSemanticScope({
domainId: null,
applicationId: null,
});
setTopologyMode("landscape");
}}
>
Domains
</button>
{semanticScope.domainId && (
<>
<span>›</span>
<button
className="text-button"
onClick={() =>
setSemanticScope((scope) => ({
...scope,
applicationId: null,
}))
}
>
{semanticTopology.domains.find(
(item) => item.id === semanticScope.domainId,
)?.name ?? "Domain"}
</button>
</>
)}
{semanticScope.applicationId && (
<>
<span>›</span>
<span>
{semanticTopology.applications.find(
(item) => item.id === semanticScope.applicationId,
)?.name ?? "Application"}
</span>
</>
)}
</div>
)}
{topologyLocation.level === "components" && !semanticTopology && (
<>
<label className="scope-label">
{componentScope
? `Landscape group: ${componentScope.label}`
: "Application components"}
</label>
<select
aria-label="Application components"
value={applicationFilter}
onChange={(event) => {
setComponentScope(null);
setApplicationFilter(event.target.value);
}}
>
<option value="all">All applications</option>
{applications.map((item) => (
<option key={item}>{item}</option>
))}
</select>
</>
)}
<div className="button-row wrap">
<button
className={focusMode ? "" : "ghost"}
onClick={() => setFocusMode(!focusMode)}
disabled={
!selectedId ||
topologyLocation.level !== "components" ||
Boolean(semanticTopology)
}
>
Focus selected
</button>
<button
className={blastRadius ? "" : "ghost"}
onClick={() => {
setBlastRadius(!blastRadius);
setTopologyMode("impact");
}}
disabled={!demo?.active && !selectedIncident}
>
Show impact
</button>
<button
className="ghost"
onClick={() => void autoArrangeGraph()}
>
Auto arrange
</button>
<button
className="ghost"
onClick={() =>
flowInstance.current?.fitView({ padding: 0.16 })
}
>
Fit view
</button>
<button
className={showMiniMap ? "" : "ghost"}
onClick={() => setShowMiniMap(!showMiniMap)}
>
Mini-map
</button>
</div>
<div className="filter-row">
<input
aria-label="Search topology"
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search all services"
/>
<select
aria-label="Filter status"
value={statusFilter}
onChange={(event) =>
setStatusFilter(event.target.value as FilterStatus)
}
>
<option value="all">All status</option>
{(
[
"healthy",
"degraded",
"down",
"recovering",
"unmonitored",
"unknown",
"maintenance",
] as HealthStatus[]
).map((item) => (
<option key={item} value={item}>
{statusLabel(item)}
</option>
))}
</select>
<select
aria-label="Filter group"
value={groupFilter}
onChange={(event) => setGroupFilter(event.target.value)}
>
<option value="all">All groups</option>
{groups.map((item) => (
<option key={item}>{item}</option>
))}
</select>
</div>
<p className="muted compact">
{topologyMode === "landscape"
? semanticScope.domainId
? "Application view for the selected operational domain. Select an application to inspect its components."
: "Landscape is organized by operational domain. Select a domain to explore its applications."
: editMode
? "Drag component nodes to pin their layout. New semantic edges are confirmed as manual dependencies."
: focusMode
? "Showing two dependency levels around the selected service."
: blastRadius
? "Showing the root cause and downstream impact."
: "Arrowheads point from a dependency to the service that depends on it. Select an edge for evidence."}
</p>
<button
className={editMode ? "" : "ghost"}
onClick={() => setEditMode(!editMode)}
disabled={topologyLocation.level !== "components"}
>
{editMode ? "Done editing" : "Edit components"}
</button>
</div>
{formOpen && renderServiceForm()}
</aside>
)}
</section>
</>
)}
</main>
);
}