From 105251b9c8e99fb23d52cc793d015d23e251634f Mon Sep 17 00:00:00 2001 From: Public Source Publisher Date: Thu, 1 Oct 2026 01:34:38 +0200 Subject: [PATCH] Initial curated ServiceMap source from 7a0a61232d05ba9097432abef6f7f4102a64d0b8 --- .dockerignore | 14 + .env.example | 32 + .gitignore | 29 + Dockerfile.unraid | 30 + PUBLIC_SOURCE_EXPORT.md | 3 + PUBLIC_SOURCE_MANIFEST.json | 476 ++ README.md | 79 + backend/Dockerfile | 14 + backend/app/__init__.py | 1 + backend/app/auth.py | 178 + backend/app/config_io.py | 173 + backend/app/demo.py | 121 + backend/app/discovery.py | 996 +++++ backend/app/discovery_store.py | 979 ++++ backend/app/docker_discovery.py | 682 +++ backend/app/docker_provider.py | 334 ++ backend/app/events.py | 63 + backend/app/evidence.py | 134 + backend/app/health.py | 167 + backend/app/incidents.py | 180 + backend/app/main.py | 743 ++++ backend/app/models.py | 862 ++++ backend/app/observation.py | 206 + backend/app/probes.py | 204 + backend/app/scheduler.py | 67 + backend/app/semantic.py | 429 ++ backend/app/store.py | 2012 +++++++++ backend/pyproject.toml | 26 + backend/requirements-dev.lock | 106 + backend/requirements.lock | 92 + backend/tests/test_api.py | 182 + backend/tests/test_auth.py | 69 + backend/tests/test_config_io.py | 133 + backend/tests/test_demo.py | 186 + backend/tests/test_discovery_v120.py | 345 ++ backend/tests/test_docker_discovery.py | 135 + backend/tests/test_docker_provider.py | 244 + backend/tests/test_health.py | 99 + backend/tests/test_probes.py | 156 + backend/tests/test_scheduler.py | 69 + backend/tests/test_store.py | 107 + backend/tests/test_v110_features.py | 159 + backend/tests/test_v130_probe_reachability.py | 322 ++ backend/tests/test_v140_semantic.py | 127 + backend/tests/test_v160_evidence_changes.py | 132 + backend/tests/test_v161_health_resolution.py | 347 ++ compose.yaml | 66 + docker-compose.yml | 70 + docs/ARCHITECTURE.md | 119 + docs/screenshots/demo-health.png | Bin 0 -> 312563 bytes docs/screenshots/demo-landscape.png | Bin 0 -> 406522 bytes frontend/Dockerfile | 13 + frontend/e2e/demo.spec.ts | 170 + frontend/eslint.config.js | 38 + frontend/index.html | 19 + frontend/nginx.conf | 23 + frontend/package-lock.json | 3929 +++++++++++++++++ frontend/package.json | 34 + frontend/playwright.config.ts | 27 + frontend/public/brand/servicemap-dark.svg | 1 + frontend/public/brand/servicemap-mark.svg | 1 + frontend/public/brand/servicemap-mono.svg | 1 + frontend/public/brand/servicemap-wordmark.svg | 1 + frontend/public/icons/apple-touch-icon.png | Bin 0 -> 4051 bytes frontend/public/icons/favicon-16.png | Bin 0 -> 447 bytes frontend/public/icons/favicon-32.png | Bin 0 -> 803 bytes frontend/public/icons/icon-192.png | Bin 0 -> 4313 bytes frontend/public/icons/icon-512.png | Bin 0 -> 12350 bytes .../public/icons/servicemap-dockerman-256.png | Bin 0 -> 6066 bytes .../public/icons/servicemap-dockerman-512.png | Bin 0 -> 12350 bytes frontend/public/site.webmanifest | 1 + frontend/src/App.tsx | 3739 ++++++++++++++++ frontend/src/LivingEdge.tsx | 49 + frontend/src/api.test.ts | 132 + frontend/src/api.ts | 940 ++++ frontend/src/auditViewModel.test.ts | 37 + frontend/src/auditViewModel.ts | 31 + frontend/src/demo/DemoWalkthrough.test.ts | 15 + frontend/src/demo/DemoWalkthrough.tsx | 77 + frontend/src/main.tsx | 10 + frontend/src/realtime/useRealtimeEvents.ts | 118 + frontend/src/styles.css | 376 ++ frontend/src/topology.test.ts | 223 + frontend/src/topology/ServiceNode.tsx | 117 + frontend/src/topology/TopologyCommandBar.tsx | 191 + frontend/src/topology/legacyLayout.ts | 285 ++ frontend/src/topology/semanticGraph.ts | 256 ++ frontend/src/topologyLayout.ts | 88 + frontend/src/topologyNavigation.test.ts | 20 + frontend/src/topologyNavigation.ts | 37 + frontend/tsconfig.json | 20 + frontend/vite.config.ts | 16 + package-lock.json | 12 + package.json | 12 + requirements.txt | 2 + unraid/my-servicemap.xml | 27 + 96 files changed, 23587 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile.unraid create mode 100644 PUBLIC_SOURCE_EXPORT.md create mode 100644 PUBLIC_SOURCE_MANIFEST.json create mode 100644 README.md create mode 100644 backend/Dockerfile create mode 100644 backend/app/__init__.py create mode 100644 backend/app/auth.py create mode 100644 backend/app/config_io.py create mode 100644 backend/app/demo.py create mode 100644 backend/app/discovery.py create mode 100644 backend/app/discovery_store.py create mode 100644 backend/app/docker_discovery.py create mode 100644 backend/app/docker_provider.py create mode 100644 backend/app/events.py create mode 100644 backend/app/evidence.py create mode 100644 backend/app/health.py create mode 100644 backend/app/incidents.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models.py create mode 100644 backend/app/observation.py create mode 100644 backend/app/probes.py create mode 100644 backend/app/scheduler.py create mode 100644 backend/app/semantic.py create mode 100644 backend/app/store.py create mode 100644 backend/pyproject.toml create mode 100644 backend/requirements-dev.lock create mode 100644 backend/requirements.lock create mode 100644 backend/tests/test_api.py create mode 100644 backend/tests/test_auth.py create mode 100644 backend/tests/test_config_io.py create mode 100644 backend/tests/test_demo.py create mode 100644 backend/tests/test_discovery_v120.py create mode 100644 backend/tests/test_docker_discovery.py create mode 100644 backend/tests/test_docker_provider.py create mode 100644 backend/tests/test_health.py create mode 100644 backend/tests/test_probes.py create mode 100644 backend/tests/test_scheduler.py create mode 100644 backend/tests/test_store.py create mode 100644 backend/tests/test_v110_features.py create mode 100644 backend/tests/test_v130_probe_reachability.py create mode 100644 backend/tests/test_v140_semantic.py create mode 100644 backend/tests/test_v160_evidence_changes.py create mode 100644 backend/tests/test_v161_health_resolution.py create mode 100644 compose.yaml create mode 100644 docker-compose.yml create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/screenshots/demo-health.png create mode 100644 docs/screenshots/demo-landscape.png create mode 100644 frontend/Dockerfile create mode 100644 frontend/e2e/demo.spec.ts create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/public/brand/servicemap-dark.svg create mode 100644 frontend/public/brand/servicemap-mark.svg create mode 100644 frontend/public/brand/servicemap-mono.svg create mode 100644 frontend/public/brand/servicemap-wordmark.svg create mode 100644 frontend/public/icons/apple-touch-icon.png create mode 100644 frontend/public/icons/favicon-16.png create mode 100644 frontend/public/icons/favicon-32.png create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/icons/servicemap-dockerman-256.png create mode 100644 frontend/public/icons/servicemap-dockerman-512.png create mode 100644 frontend/public/site.webmanifest create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/LivingEdge.tsx create mode 100644 frontend/src/api.test.ts create mode 100644 frontend/src/api.ts create mode 100644 frontend/src/auditViewModel.test.ts create mode 100644 frontend/src/auditViewModel.ts create mode 100644 frontend/src/demo/DemoWalkthrough.test.ts create mode 100644 frontend/src/demo/DemoWalkthrough.tsx create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/realtime/useRealtimeEvents.ts create mode 100644 frontend/src/styles.css create mode 100644 frontend/src/topology.test.ts create mode 100644 frontend/src/topology/ServiceNode.tsx create mode 100644 frontend/src/topology/TopologyCommandBar.tsx create mode 100644 frontend/src/topology/legacyLayout.ts create mode 100644 frontend/src/topology/semanticGraph.ts create mode 100644 frontend/src/topologyLayout.ts create mode 100644 frontend/src/topologyNavigation.test.ts create mode 100644 frontend/src/topologyNavigation.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 requirements.txt create mode 100644 unraid/my-servicemap.xml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..44e660c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitea +.env +.venv +__pycache__ +.pytest_cache +node_modules +frontend/node_modules +frontend/dist +data +*.db +*.db-shm +*.db-wal +*.sqlite* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..130be59 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Unraid deployment defaults: expose the ServiceMap UI on host port 1253. +SERVICEMAP_PORT=1253 +SERVICEMAP_WEBUI_HOST=127.0.0.1 +SERVICEMAP_PROBE_INTERVAL=60 +SERVICEMAP_PROBE_TIMEOUT=5 +SERVICEMAP_PROBE_RETRIES=1 +SERVICEMAP_PROBE_CONCURRENCY=10 +SERVICEMAP_RETENTION_DAYS=30 +SERVICEMAP_EVENT_RETENTION_DAYS=90 +SERVICEMAP_TLS_WARNING_DAYS=30 +SERVICEMAP_FLAP_THRESHOLD=6 +SERVICEMAP_DISCOVERY_INTERVAL_SECONDS=0 +# Health-only Docker/AppOps observation; it never runs discovery or reconciliation. +SERVICEMAP_OBSERVATION_INTERVAL_SECONDS=60 +SERVICEMAP_DOCKER_HOST=unix:///var/run/docker.sock +# Default is the legacy, read-only socket mount. Broker mode has no automatic +# fallback and requires a dedicated registered ServiceMap token mount. +SERVICEMAP_DOCKER_PROVIDER=socket +SERVICEMAP_DOCKER_BROKER_URL= +SERVICEMAP_DOCKER_BROKER_TOKEN_FILE= +SERVICEMAP_APPOPS_URL= +# Leave empty unless this host address is reachable from the ServiceMap container. +SERVICEMAP_PROBE_HOST= +# Optional OIDC API authorization. Keep disabled until a dedicated ServiceMap +# client, issuer, audience and role groups have been provisioned. +SERVICEMAP_AUTH_MODE=disabled +SERVICEMAP_OIDC_ISSUER= +SERVICEMAP_OIDC_AUDIENCE= +SERVICEMAP_OIDC_ROLE_CLAIMS=groups,roles +SERVICEMAP_OIDC_VIEWER_GROUPS= +SERVICEMAP_OIDC_OPERATOR_GROUPS= +SERVICEMAP_OIDC_ADMIN_GROUPS= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b75c564 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +.pytest_cache/ +.ruff_cache/ + +# Node +node_modules/ +dist/ +.vite/ +frontend/test-results/ + +# Local state +*.db +*.db-shm +*.db-wal +*.sqlite +*.sqlite3 +.env +.env.* +!.env.example + +# IDE / OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/Dockerfile.unraid b/Dockerfile.unraid new file mode 100644 index 0000000..027040e --- /dev/null +++ b/Dockerfile.unraid @@ -0,0 +1,30 @@ +FROM node:22-alpine AS frontend-build + +WORKDIR /frontend +COPY frontend/package.json ./package.json +COPY frontend/package-lock.json ./package-lock.json +RUN npm ci --no-audit --no-fund +COPY frontend/ ./ +RUN npm run build + +FROM python:3.12-slim + +LABEL org.opencontainers.image.title="ServiceMap" \ + org.opencontainers.image.description="Production infrastructure intelligence" \ + org.opencontainers.image.version="1.6.2" \ + org.opencontainers.image.licenses="MIT" + +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + SERVICEMAP_DB=/data/servicemap.db \ + SERVICEMAP_STATIC_DIR=/app/static + +COPY backend/requirements.lock ./requirements.lock +COPY backend/app ./app +RUN pip install --no-cache-dir -r requirements.lock +COPY --from=frontend-build /frontend/dist ./static + +VOLUME ["/data"] +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/PUBLIC_SOURCE_EXPORT.md b/PUBLIC_SOURCE_EXPORT.md new file mode 100644 index 0000000..b7d224e --- /dev/null +++ b/PUBLIC_SOURCE_EXPORT.md @@ -0,0 +1,3 @@ +# Public source export + +This source snapshot was generated from the private canonical repository at revision `7a0a61232d05ba9097432abef6f7f4102a64d0b8`. It contains no private Git history or operational evidence. Changes are published from the canonical repository. diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json new file mode 100644 index 0000000..59cf10f --- /dev/null +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -0,0 +1,476 @@ +{ + "schemaVersion": 1, + "sourceRevision": "7a0a61232d05ba9097432abef6f7f4102a64d0b8", + "files": [ + { + "path": ".dockerignore", + "sha256": "bf97a1ebf1e9b5d01538a0e92a7cd4763139350c46ffbd344c87e13f6da0b8af", + "bytes": 150 + }, + { + "path": ".env.example", + "sha256": "b1e3e17c79d606b293f5cc027943c83a4cd4fdb4c9b518c50e84e10bb3ccb262", + "bytes": 1353 + }, + { + "path": ".gitignore", + "sha256": "238a6b3a02cf4b1de38f1c506beab1c3722d604f42100fc1ef3d92b4ed0ab62d", + "bytes": 297 + }, + { + "path": "Dockerfile.unraid", + "sha256": "644039841da59594f369f80ad0766eb1c934dcddaf4a594cf2bdad1fc685d99a", + "bytes": 934 + }, + { + "path": "README.md", + "sha256": "8887dc7df5db24b00873789961e855fe08c9a9ce992302cbf48745c50de34285", + "bytes": 4738 + }, + { + "path": "backend/Dockerfile", + "sha256": "7f8627da7d4d15a9df9a8bb650b18d74dbfa3c226d25ab25070f65709ef6870f", + "bytes": 373 + }, + { + "path": "backend/app/__init__.py", + "sha256": "a7589dccbf098c71a4bb02f3547e170d0d538db54b17dd355e62f582fe2f0e9e", + "bytes": 35 + }, + { + "path": "backend/app/auth.py", + "sha256": "4202bb02fa0133a6816e599f3e35426a0604ef407313cee7076e04e96ecc495d", + "bytes": 6878 + }, + { + "path": "backend/app/config_io.py", + "sha256": "f289ecac3ec35848e0097f2d3c81b4401536586564dc36a8e6a6301952247fe6", + "bytes": 7255 + }, + { + "path": "backend/app/demo.py", + "sha256": "0c5845029d5cfffae3e83665b7aa89bf715f8646cdfcc525f70abada860ea148", + "bytes": 6219 + }, + { + "path": "backend/app/discovery.py", + "sha256": "646db051a01658d6f5ef6b75070dceb05a59c40264d56e2761e9fc165e7527ef", + "bytes": 53184 + }, + { + "path": "backend/app/discovery_store.py", + "sha256": "f83a21b6f71e73314d6602a262d2d2eada0fa83b9db33e59b6d3070c6a2a14bd", + "bytes": 48117 + }, + { + "path": "backend/app/docker_discovery.py", + "sha256": "170b950925f26ad12e1c285edc039342d2eedbc2af04a7b0683501c65393c957", + "bytes": 32124 + }, + { + "path": "backend/app/docker_provider.py", + "sha256": "b409d13f149cb15e88f258510787b73ed3b67c92ba3bd9ab30be76473bcaf492", + "bytes": 13288 + }, + { + "path": "backend/app/events.py", + "sha256": "6f71a41d0154306f5d5f7a215b4ca69cf139448ef78d50f3d6c5aaea48bbe1b6", + "bytes": 2154 + }, + { + "path": "backend/app/evidence.py", + "sha256": "bf3545eb916326acfc5742c56c99089781babdb1221d62394710b36417bbd5ec", + "bytes": 4843 + }, + { + "path": "backend/app/health.py", + "sha256": "6e0eb886abc5e3a30f1d256bb3094cc09566cfde8510d433318f18e403ec9549", + "bytes": 6131 + }, + { + "path": "backend/app/incidents.py", + "sha256": "d87bebdfe902c2875e49067245a571bc30debe9f375d0faab1ea91bb5c2d39df", + "bytes": 7327 + }, + { + "path": "backend/app/main.py", + "sha256": "16807aa5f8e3301f76d9b5116e0ce1da824814e62bce2efa3f63829ed5d3bd5c", + "bytes": 31255 + }, + { + "path": "backend/app/models.py", + "sha256": "2e5d6faeb1606f6f66e06aebc236daed996ab1d56d72f84123fe6261ace3f065", + "bytes": 28385 + }, + { + "path": "backend/app/observation.py", + "sha256": "5714125fe38065d7d4cb9a9cdbaa95e969ae9710fccbc45838f3beaa7588f36c", + "bytes": 9078 + }, + { + "path": "backend/app/probes.py", + "sha256": "61e5a9bf6dc83973bc56e9d48fa94def7222663de13a4338f5e829ad8b41d354", + "bytes": 8436 + }, + { + "path": "backend/app/scheduler.py", + "sha256": "4ea3e15d3f107cf5d29c2ce5ec71b2c48826602543fb4c08ebf2edd5a53fd7fa", + "bytes": 2544 + }, + { + "path": "backend/app/semantic.py", + "sha256": "2c87be83a4c4bef48ab17e7140e7d989058750c5acf42a96a1380bb34272a07c", + "bytes": 20105 + }, + { + "path": "backend/app/store.py", + "sha256": "5e3713f222521f22cc53eeda4005f46a06d50b13a1f43090b58783f7f4a84ebe", + "bytes": 105338 + }, + { + "path": "backend/pyproject.toml", + "sha256": "f739af56b1a5dbe009481de79aa5c578cb7b6865cf80cc32b641d4ade5c35534", + "bytes": 524 + }, + { + "path": "backend/requirements-dev.lock", + "sha256": "466630ac7ad0bccc6ab7e0678a24cbd3132ceb6937b20c0dcf54ba7f373be814", + "bytes": 2297 + }, + { + "path": "backend/requirements.lock", + "sha256": "f21c3b78c3c852126dfe4e42cd7fca074e2ca3587f5d4cd47539727474dd78dd", + "bytes": 1967 + }, + { + "path": "backend/tests/test_api.py", + "sha256": "3b8644b53200cdac233dc629be3a1282e332e1f782e215d237f77770fcf7fd2a", + "bytes": 7330 + }, + { + "path": "backend/tests/test_auth.py", + "sha256": "3023839a1dc787b26d98f738adc8ef212187370878edb53e6a6dbc42fd8fb8ad", + "bytes": 2531 + }, + { + "path": "backend/tests/test_config_io.py", + "sha256": "382869227e1d0fe6ad10e233ce70978b1adfa42162e2439d0e0747b7ebfc8cc7", + "bytes": 3686 + }, + { + "path": "backend/tests/test_demo.py", + "sha256": "1290b10727ad668af6639cf7a65a3fbcc89e25c01d17e97ee3f0f60565a3f256", + "bytes": 7610 + }, + { + "path": "backend/tests/test_discovery_v120.py", + "sha256": "7b3c1242815f65da45ec2d3a57cc1ba89a4a599f8852be781923242104c69472", + "bytes": 16143 + }, + { + "path": "backend/tests/test_docker_discovery.py", + "sha256": "0fc5b719bf1d0556a3fd6c7befa7e1be2860c5c9e562a357c58b232848b177a6", + "bytes": 4966 + }, + { + "path": "backend/tests/test_docker_provider.py", + "sha256": "dc949328ba6e3762b40a17c9a61029f538609507d713bee839fce6c639e3873f", + "bytes": 8637 + }, + { + "path": "backend/tests/test_health.py", + "sha256": "26056d2de98b6546e00e195dfe5fa81fa3a43c697261190abdfd39e40acc490d", + "bytes": 3983 + }, + { + "path": "backend/tests/test_probes.py", + "sha256": "263f4cf12444f998dd59d72bc75e5976cd9b2b8d52b576bae79e545db3cc631d", + "bytes": 4816 + }, + { + "path": "backend/tests/test_scheduler.py", + "sha256": "ede0a460f3ed60047db0ecd5f76b09a13be1e93b97b210fc2735116cd1ddc74d", + "bytes": 1934 + }, + { + "path": "backend/tests/test_store.py", + "sha256": "b43d83b3030d80b416147903666add662ebbd4fc6f8ae3b98e9a50b432dee6ac", + "bytes": 3755 + }, + { + "path": "backend/tests/test_v110_features.py", + "sha256": "8ad4e82ad5ee65c700aeaf796df46337690c84c494463e5da57968b71819e336", + "bytes": 6876 + }, + { + "path": "backend/tests/test_v130_probe_reachability.py", + "sha256": "8e021432c4f850958e3d4443e3095502437401890ae02661c43869ca46476724", + "bytes": 11754 + }, + { + "path": "backend/tests/test_v140_semantic.py", + "sha256": "eb78d80bb4c8a92f6526aa3e4d22d5f44f6ef946c97e2dd12767c404b5531cbe", + "bytes": 5382 + }, + { + "path": "backend/tests/test_v160_evidence_changes.py", + "sha256": "4fe8d2de180d6b4d265befe6aec8dcb9e1c01be22b42539a9703ed8b9cd9a441", + "bytes": 4280 + }, + { + "path": "backend/tests/test_v161_health_resolution.py", + "sha256": "1f30ed8929188c6fb0c3fc636ee36ff6e9d016f40a01edc2887585d96353edab", + "bytes": 15856 + }, + { + "path": "compose.yaml", + "sha256": "ef5c82e6330a2ce8bea27268a0f2a19960f08a26bc6c53b7d84218b342494258", + "bytes": 2195 + }, + { + "path": "docker-compose.yml", + "sha256": "c051b66f67511a02fb11fb8da18393380662394542830c96927d32d26a7079c9", + "bytes": 3239 + }, + { + "path": "docs/ARCHITECTURE.md", + "sha256": "ac4d87a866c3d9fc5b19507abbb1f6afb2d68ce5e4729b9e332943755075d2dc", + "bytes": 14240 + }, + { + "path": "docs/screenshots/demo-health.png", + "sha256": "7814f69214c8b6b65cd735f9a75d9f7b974b88470a37641e3c76bc314e164085", + "bytes": 312563 + }, + { + "path": "docs/screenshots/demo-landscape.png", + "sha256": "185649a264c5e1fb58d83e1d25da94d5a61b60c4adddb398a55b2298d26cebf8", + "bytes": 406522 + }, + { + "path": "frontend/Dockerfile", + "sha256": "0d8bf5dd1725879b8ea2859d78a67dd0d499fb1fb87e44aa4efad8c2e89b1899", + "bytes": 358 + }, + { + "path": "frontend/e2e/demo.spec.ts", + "sha256": "5fe2de577a41f31e3574c75bb0b46a51e695acb1eb8aadbb094d7b4ad0a1e887", + "bytes": 7851 + }, + { + "path": "frontend/eslint.config.js", + "sha256": "e779a8cbd7200eaf5176c3c031467345dde6af2dac138cc4a32407f14432062f", + "bytes": 940 + }, + { + "path": "frontend/index.html", + "sha256": "21a1858bfa8b92521d179c05d16991e6c7b0fbef61fe16f7f0f12a72db3e9ab2", + "bytes": 865 + }, + { + "path": "frontend/nginx.conf", + "sha256": "c07a3ce7936beef0d3f124f18878b46c87693150e83cf571cebbf3f6f6635885", + "bytes": 520 + }, + { + "path": "frontend/package-lock.json", + "sha256": "4b35333ebae3a5ac1830820cba08561dc12cf244566bb21b5183bf513902aed7", + "bytes": 138931 + }, + { + "path": "frontend/package.json", + "sha256": "480fd200b705f407e1d1fda6d9a28b09189aaec28b3253d10e463fb0b17a7119", + "bytes": 881 + }, + { + "path": "frontend/playwright.config.ts", + "sha256": "d959bee365c474dc6380de9056bfb5d27a905b9e4e2c79ee43071e0006d659f3", + "bytes": 855 + }, + { + "path": "frontend/public/brand/servicemap-dark.svg", + "sha256": "79ed84eba76e6362d6bbb69ef79ea3d3686bb49f89e44d6ae477bd94e87de988", + "bytes": 576 + }, + { + "path": "frontend/public/brand/servicemap-mark.svg", + "sha256": "12b8f893d5a85c9dc9c96e18ea57735348ceaff420e891483c3c2ebc8e3e7523", + "bytes": 671 + }, + { + "path": "frontend/public/brand/servicemap-mono.svg", + "sha256": "79e35d1c8ac11d14ae6e83f1d49e37dcbfd79ff207e99610b1f5407b873eed5c", + "bytes": 568 + }, + { + "path": "frontend/public/brand/servicemap-wordmark.svg", + "sha256": "75f8b5b7698d4959234c96a418e2857d64806ff85d6d9693ac5d06a79bec2a42", + "bytes": 855 + }, + { + "path": "frontend/public/icons/apple-touch-icon.png", + "sha256": "00206a46b358446d25494be66bd812aff9af40fb972640d2635891e614911f3d", + "bytes": 4051 + }, + { + "path": "frontend/public/icons/favicon-16.png", + "sha256": "d9f1eaef74f8f4159118fe9a50d05409bfbf92995b6f52ce100184b172a8c205", + "bytes": 447 + }, + { + "path": "frontend/public/icons/favicon-32.png", + "sha256": "62709f3bcfee2c00be5cfb628e96197d7983e3604a8cbd6669ed59fcd61abf4d", + "bytes": 803 + }, + { + "path": "frontend/public/icons/icon-192.png", + "sha256": "73ac592d98b2f5d299872b9509bee18533ccb82704e2dbada4b39cb417aa9262", + "bytes": 4313 + }, + { + "path": "frontend/public/icons/icon-512.png", + "sha256": "26a7927f877ccb98e9e8109fd71518136766edef81dcf56799420c839cfcf826", + "bytes": 12350 + }, + { + "path": "frontend/public/icons/servicemap-dockerman-256.png", + "sha256": "8c8807d5ef5bae83b9071b2962e90a65fded243f1474e2e27d9ad7ef06441d2d", + "bytes": 6066 + }, + { + "path": "frontend/public/icons/servicemap-dockerman-512.png", + "sha256": "26a7927f877ccb98e9e8109fd71518136766edef81dcf56799420c839cfcf826", + "bytes": 12350 + }, + { + "path": "frontend/public/site.webmanifest", + "sha256": "6ccd6440da6471ad2326010f1e1ae5b47760ea058d6b5cd674f0d1567d6a6eba", + "bytes": 340 + }, + { + "path": "frontend/src/App.tsx", + "sha256": "b78e5998577869103f9260e93fda6b6ab7d81d7f2ddb6f990294b4e8929e2257", + "bytes": 141427 + }, + { + "path": "frontend/src/LivingEdge.tsx", + "sha256": "0699d5ac00159befe6f37caaceb742c34a8a909079269cfeb0177cca8fb6a954", + "bytes": 1371 + }, + { + "path": "frontend/src/api.test.ts", + "sha256": "f62fc355a399642b0d809c19c3eb1911bfb4e2eb2e1a8d0effee0cd07c35b105", + "bytes": 5837 + }, + { + "path": "frontend/src/api.ts", + "sha256": "ac71a7b3adf4188eaa26058afa8ef6c03e023cbbb67b445ca4f7f3227c74d4ee", + "bytes": 27507 + }, + { + "path": "frontend/src/auditViewModel.test.ts", + "sha256": "e9bd72409f745321af37e54565eb3a2a1777c5696cfa5235aa674c279a6537ec", + "bytes": 2039 + }, + { + "path": "frontend/src/auditViewModel.ts", + "sha256": "228a7c02b27f4c064d05abe95e544da9a2389bcae8e8b79f97a670319a9ad986", + "bytes": 1428 + }, + { + "path": "frontend/src/demo/DemoWalkthrough.test.ts", + "sha256": "2d95993bef2286d60cb8ab6cd71b1f081a3f6cabd3f4e8b32791721578933bda", + "bytes": 487 + }, + { + "path": "frontend/src/demo/DemoWalkthrough.tsx", + "sha256": "1201c5d832475dd7e09b72287343e91e4a363fc79624ec7eb055b68ad2d390db", + "bytes": 2214 + }, + { + "path": "frontend/src/main.tsx", + "sha256": "b3f023da133154adff448b24c256c554072536939e52291490a6128a9cfc82f7", + "bytes": 237 + }, + { + "path": "frontend/src/realtime/useRealtimeEvents.ts", + "sha256": "e800faa868b0d2e811848647101f4a21897e22b284b5a24572d0a687a03b47be", + "bytes": 3421 + }, + { + "path": "frontend/src/styles.css", + "sha256": "6deaffb725b10cee74496187c1364a848ccfb2843b8d278a232e99e6cea5fd84", + "bytes": 31996 + }, + { + "path": "frontend/src/topology.test.ts", + "sha256": "3a1267d0af23a45889d6a39184ff6f32d89f9ccafc9d53872cbb38931b7a3434", + "bytes": 7110 + }, + { + "path": "frontend/src/topology/ServiceNode.tsx", + "sha256": "0e3e673d1e148483bdd331b10d7b515348b308f943fd370e8bb2435ac8d4d9f9", + "bytes": 3872 + }, + { + "path": "frontend/src/topology/TopologyCommandBar.tsx", + "sha256": "3349725592ca64c9a845d1fe677ec7ed919069e0ea9af52373e955698c20e039", + "bytes": 8375 + }, + { + "path": "frontend/src/topology/legacyLayout.ts", + "sha256": "e843fbb70a14346269987e4a6846fab2b88ff955cf35cd69c691851f7f94ea09", + "bytes": 9835 + }, + { + "path": "frontend/src/topology/semanticGraph.ts", + "sha256": "53da1a124bb798674b6da14f45ddf7c35a20cacce62d38223b6fc27909854d2c", + "bytes": 9526 + }, + { + "path": "frontend/src/topologyLayout.ts", + "sha256": "d51e7f827df124f1932b544ce253c09fd3731f36ec66b051cdcc0f2933c38b39", + "bytes": 2842 + }, + { + "path": "frontend/src/topologyNavigation.test.ts", + "sha256": "7381dc580b132ba55775b50cb623db5497000c5694c7888fb740c2e3fbf87858", + "bytes": 600 + }, + { + "path": "frontend/src/topologyNavigation.ts", + "sha256": "1fd2d65317ed1445fba823b7351cdcd25b0ab87405f7b1b61505021c5ba23748", + "bytes": 976 + }, + { + "path": "frontend/tsconfig.json", + "sha256": "9616ea859510ecae0cb8464fbe3bbd1448d66bb5a4ebf40d163e8f14fa8fff2b", + "bytes": 545 + }, + { + "path": "frontend/vite.config.ts", + "sha256": "72b18ff3a6e5ef68c37ebd6de037943ecbd51cbe27b86342e6222580eecd1e16", + "bytes": 359 + }, + { + "path": "package-lock.json", + "sha256": "43f354f7539a2315a36d1fa499707e8f198e447b4ee50ec736add96c89d09eb2", + "bytes": 215 + }, + { + "path": "package.json", + "sha256": "96398a3dbf6e8545aead833277fd60c8405ced51790ad7fe20c99068c3524b6e", + "bytes": 490 + }, + { + "path": "requirements.txt", + "sha256": "b61a55489732c882dd350b471b9050e019f29ba1965f7eb4cc65f128271dbcf6", + "bytes": 53 + }, + { + "path": "unraid/my-servicemap.xml", + "sha256": "24bd225838c710187fb93950b29b79f782792c96cffa05062172020a91f69e0f", + "bytes": 3110 + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa513d7 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# ServiceMap + +**Understand what runs, what depends on it, and why a service is unhealthy.** + +ServiceMap is a self-hosted service intelligence workspace for homelabs and small environments. It combines explicit dependencies, bounded probes, reviewable discovery and durable incident evidence in a navigable topology. + +```mermaid +flowchart LR + A[Define / discover] --> B[Review topology] + B --> C[Probe and observe] + C --> D[Resolve health evidence] + D --> E[Trace impact and incidents] +``` + +## What you can do + +- Probe HTTP(S), TCP, DNS and TLS certificate expiry with configurable thresholds and retries. +- Navigate Domain → Application → Component with search, filters, focus and blast-radius views. +- Inspect provider, confidence, scope, freshness and conflicts behind each health decision. +- Review Docker discovery and optional AppOps enrichment before importing canonical topology. +- Track incident acknowledgement, resolution, availability, maintenance and retained event history. +- Compare canonical topology snapshots and retain relationship provenance. +- Use SSE updates and deterministic ELK topology layout with reduced-motion support. +- Import/export validated YAML or JSON configuration atomically. + +## Explore safely + +Choose **Explore demo** to open the fictional Northstar Platform: nine services, synthetic observations and a seeded PostgreSQL failure. The same incident and dependency logic identifies downstream impact. The demo contacts no fictional endpoints; **Exit demo** restores the saved topology, and **Reset demo** restores its synthetic starting state. + +## Demo preview + +Captured from the real application with its synthetic Northstar demo. No private infrastructure or live monitoring inventory is shown. + +![Dependency landscape with synthetic root-cause evidence](docs/screenshots/demo-landscape.png) + +![Synthetic incident and monitoring-coverage overview](docs/screenshots/demo-health.png) + +## Deploy with Docker Compose + +```bash +git clone https://gitea.itworx.tech/Jens/ServiceMap-Public.git +cd ServiceMap-Public +cp .env.example .env +docker compose up --build +``` + +Open `http://localhost:1253`. Standard Compose runs the Python API and React frontend separately and persists SQLite under `./data`. Preserve this directory during upgrades. Set `SERVICEMAP_PROBE_INTERVAL=0` to disable scheduled probes. Define targets and dependencies in the UI or import a configuration before expecting live coverage. + +The single-container Unraid build is provided by `Dockerfile.unraid` and `docker-compose.yml`. Review the socket/network mounts before enabling discovery. A read-only Docker socket mount still exposes a powerful Docker API boundary. Broker mode requires a dedicated registered identity and token file and deliberately has no socket fallback. AppOps enrichment is optional; configure your own endpoint. + +## Security and health semantics + +Protect the UI/API with network restrictions or tested OIDC configuration. Authentication is disabled by default; do not expose an unprotected deployment to the Internet. OIDC supports viewer/operator/admin roles when its issuer, audience and groups are provisioned. Discovery uses deployment-configured targets, not arbitrary browser-supplied targets. + +Health distinguishes **unmonitored, unknown, healthy, degraded, down, recovering and maintenance**. A runtime observation is not automatically a successful endpoint probe. Application-scoped AppOps evidence does not silently establish component health. Expired evidence and ambiguous relationships remain visible instead of inventing certainty. + +## Development + +```bash +python -m venv .venv +# Activate .venv for your shell. +pip install -r requirements.txt +uvicorn app.main:app --app-dir backend --reload --port 8000 +``` + +In another terminal: + +```bash +npm --prefix frontend ci +npm --prefix frontend run dev +``` + +Vite proxies the API to port 8000. Validate changes with `python -m pytest`, `npm --prefix frontend test`, `npm --prefix frontend run typecheck`, `npm --prefix frontend run lint` and `npm --prefix frontend run build`. Browser tests exercise the synthetic demo. See [architecture](docs/ARCHITECTURE.md) for the data and observation model. + +## Source distribution and licensing + +This is the curated public source edition of ServiceMap 1.6.2. Reviewed updates flow from the private canonical repository after merges to `main`; private history, production evidence and internal CI/deployment workflows are excluded. Deployment examples use generic addresses and must be configured for your environment. + +No project-wide license is included. Public source visibility does not grant reuse or redistribution rights; obtain the owner's terms and review third-party dependencies before redistribution. diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b2c9fbc --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.12-slim + +WORKDIR /app +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + SERVICEMAP_DB=/data/servicemap.db + +COPY backend/requirements.lock ./requirements.lock +COPY backend/app ./app +RUN pip install --no-cache-dir -r requirements.lock + +VOLUME ["/data"] +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..5941914 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1 @@ +"""ServiceMap backend package.""" diff --git a/backend/app/auth.py b/backend/app/auth.py new file mode 100644 index 0000000..76cbae9 --- /dev/null +++ b/backend/app/auth.py @@ -0,0 +1,178 @@ +"""Optional, fail-closed OIDC bearer authorization for ServiceMap APIs.""" + +from __future__ import annotations + +import os +from dataclasses import dataclass +from functools import lru_cache +from urllib.parse import urlparse + +import httpx +import jwt +from fastapi import HTTPException, Request, status + + +class AuthorizationError(RuntimeError): + pass + + +_ROLE_ORDER = {"viewer": 1, "operator": 2, "admin": 3} + + +def _csv(name: str) -> set[str]: + return {item.strip().casefold() for item in os.getenv(name, "").split(",") if item.strip()} + + +@dataclass(frozen=True) +class AuthConfig: + mode: str + issuer: str | None = None + audience: str | None = None + role_claims: tuple[str, ...] = ("groups", "roles") + viewer_groups: frozenset[str] = frozenset() + operator_groups: frozenset[str] = frozenset() + admin_groups: frozenset[str] = frozenset() + + @classmethod + def from_environment(cls) -> AuthConfig: + mode = os.getenv("SERVICEMAP_AUTH_MODE", "disabled").strip().casefold() or "disabled" + if mode == "disabled": + return cls(mode="disabled") + if mode != "oidc": + raise AuthorizationError("SERVICEMAP_AUTH_MODE must be 'disabled' or 'oidc'") + issuer = os.getenv("SERVICEMAP_OIDC_ISSUER", "").strip().rstrip("/") + audience = os.getenv("SERVICEMAP_OIDC_AUDIENCE", "").strip() + parsed = urlparse(issuer) + local_http = os.getenv("SERVICEMAP_AUTH_ALLOW_HTTP_FOR_LOCAL", "false").casefold() == "true" + if ( + not issuer + or not audience + or not parsed.netloc + or (parsed.scheme != "https" and not local_http) + ): + raise AuthorizationError("OIDC mode requires a valid HTTPS issuer and audience") + claims = tuple( + item.strip() + for item in os.getenv("SERVICEMAP_OIDC_ROLE_CLAIMS", "groups,roles").split(",") + if item.strip() + ) + return cls( + mode=mode, + issuer=issuer, + audience=audience, + role_claims=claims or ("groups", "roles"), + viewer_groups=frozenset(_csv("SERVICEMAP_OIDC_VIEWER_GROUPS")), + operator_groups=frozenset(_csv("SERVICEMAP_OIDC_OPERATOR_GROUPS")), + admin_groups=frozenset(_csv("SERVICEMAP_OIDC_ADMIN_GROUPS")), + ) + + +@lru_cache(maxsize=8) +def _jwks_client(issuer: str) -> jwt.PyJWKClient: + try: + document = httpx.get(f"{issuer}/.well-known/openid-configuration", timeout=5.0).json() + jwks_uri = str(document.get("jwks_uri", "")) if isinstance(document, dict) else "" + except (httpx.HTTPError, ValueError) as exc: + raise AuthorizationError("OIDC discovery is unavailable") from exc + parsed = urlparse(jwks_uri) + issuer_host = urlparse(issuer).hostname + if not parsed.netloc or parsed.scheme != "https" or parsed.hostname != issuer_host: + raise AuthorizationError("OIDC discovery returned an invalid JWKS URI") + return jwt.PyJWKClient(jwks_uri, cache_keys=True, lifespan=300) + + +def _claim_values(claims: dict[str, object], names: tuple[str, ...]) -> set[str]: + values: set[str] = set() + for name in names: + raw = claims.get(name) + if isinstance(raw, str): + values.add(raw.casefold()) + elif isinstance(raw, list): + values.update(str(item).casefold() for item in raw if isinstance(item, (str, int))) + return values + + +def _roles_for(claims: dict[str, object], config: AuthConfig) -> set[str]: + values = _claim_values(claims, config.role_claims) + roles: set[str] = set() + # Explicit role claims remain useful with a constrained issuer; group + # mapping gives operators a stable least-privilege model in Authentik. + roles.update(value for value in values if value in _ROLE_ORDER) + if values & config.viewer_groups: + roles.add("viewer") + if values & config.operator_groups: + roles.add("operator") + if values & config.admin_groups: + roles.add("admin") + if "admin" in roles: + roles.update({"operator", "viewer"}) + elif "operator" in roles: + roles.add("viewer") + return roles + + +def verify_bearer(token: str, config: AuthConfig) -> set[str]: + if not config.issuer or not config.audience: + raise AuthorizationError("OIDC configuration is incomplete") + try: + signing_key = _jwks_client(config.issuer).get_signing_key_from_jwt(token) + claims = jwt.decode( + token, + signing_key.key, + algorithms=[signing_key.algorithm_name], + audience=config.audience, + issuer=config.issuer, + options={"require": ["exp", "sub", "iss", "aud"]}, + ) + except jwt.PyJWTError as exc: + raise AuthorizationError("Bearer token verification failed") from exc + if not isinstance(claims, dict): + raise AuthorizationError("Bearer token claims are invalid") + return _roles_for(claims, config) + + +def required_role(request: Request) -> str: + if request.method in {"GET", "HEAD", "OPTIONS"}: + return "viewer" + path = request.url.path + if path.startswith( + ("/api/v1/config", "/api/v1/discovery", "/api/v1/docker", "/api/v1/maintenance") + ): + return "admin" + return "operator" + + +def authorize_request(request: Request) -> None: + """Authorize API requests; health and static content remain reachable. + + Disabled mode preserves v1.5 behavior intentionally. Enabling OIDC with + incomplete configuration fails closed on the API while `/health` remains + available for recovery and deployment diagnostics. + """ + + if request.url.path == "/health" or not request.url.path.startswith("/api/"): + return + try: + config = AuthConfig.from_environment() + except AuthorizationError as exc: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=str(exc) + ) from exc + if config.mode == "disabled": + return + header = request.headers.get("Authorization", "") + if not header.startswith("Bearer "): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Bearer authentication is required" + ) + try: + roles = verify_bearer(header.removeprefix("Bearer ").strip(), config) + except AuthorizationError as exc: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc + if ( + _ROLE_ORDER.get(max(roles, key=lambda item: _ROLE_ORDER[item]) if roles else "", 0) + < _ROLE_ORDER[required_role(request)] + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient ServiceMap role" + ) diff --git a/backend/app/config_io.py b/backend/app/config_io.py new file mode 100644 index 0000000..7ea0ab5 --- /dev/null +++ b/backend/app/config_io.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import json + +import yaml +from pydantic import ValidationError + +from .models import ConfigDocument, ConfigImportResult, ConfigService +from .store import Store + + +def parse_document(content: str, format_name: str) -> ConfigDocument: + try: + raw = json.loads(content) if format_name == "json" else yaml.safe_load(content) + except (json.JSONDecodeError, yaml.YAMLError) as exc: + raise ValueError(f"Invalid {format_name.upper()} configuration: {exc}") from exc + if raw is None: + raw = {"version": 1, "services": []} + try: + document = ConfigDocument.model_validate(raw) + except ValidationError as exc: + raise ValueError(f"Configuration schema validation failed: {exc}") from exc + if document.version == 2 and not document.services: + # Preserve the v1 contract's explicit unsupported-empty-v2 guard while + # accepting real v2 documents that carry the richer service schema. + raise ValueError("Configuration version must be less than or equal to 1 for an empty document") + + names = [service.name.casefold() for service in document.services] + if len(names) != len(set(names)): + raise ValueError("Service names must be unique in the configuration") + + for service in document.services: + dependencies = [dependency.casefold() for dependency in service.depends_on] + if len(dependencies) != len(set(dependencies)): + raise ValueError(f"Service {service.name} has duplicate dependencies") + if service.name.casefold() in dependencies: + raise ValueError(f"Service {service.name} cannot depend on itself") + + # A merge document may reference services that already exist in the store, + # so missing-reference validation belongs to import_document(). Cycles that + # are fully contained in this document can still be rejected immediately. + _validate_acyclic(document.services) + return document + + +def _validate_acyclic(services: list[ConfigService]) -> None: + graph = { + service.name.casefold(): [item.casefold() for item in service.depends_on] + for service in services + } + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + raise ValueError("Dependency graph contains a cycle") + if node in visited: + return + visiting.add(node) + for dependency in graph.get(node, []): + visit(dependency) + visiting.remove(node) + visited.add(node) + + for node in graph: + visit(node) + + +def _validate_import_graph(store: Store, document: ConfigDocument, mode: str) -> None: + if mode not in {"merge", "replace"}: + raise ValueError("Import mode must be merge or replace") + + projected: dict[str, ConfigService] = {} + if mode == "merge": + existing = store.list_services() + names_by_id = {service.id: service.name for service in existing} + for service in existing: + projected[service.name.casefold()] = ConfigService( + name=service.name, + kind=service.kind, + url=service.url, + host=service.host, + port=service.port, + depends_on=[names_by_id[item] for item in service.depends_on if item in names_by_id], + description=service.description, + tags=service.tags, + group=service.group, + environment=service.environment, + owner=service.owner, + enabled=service.enabled, + monitoring_enabled=service.monitoring_enabled, + criticality=service.criticality, + source_type=service.source_type, + source_ref=service.source_ref, + interval_seconds=service.interval_seconds, + timeout_seconds=service.timeout_seconds, + retries=service.retries, + failure_threshold=service.failure_threshold, + recovery_threshold=service.recovery_threshold, + expected_status_min=service.expected_status_min, + expected_status_max=service.expected_status_max, + availability_target=service.availability_target, + maintenance_enabled=service.maintenance_enabled, + maintenance_reason=service.maintenance_reason, + ) + + for service in document.services: + projected[service.name.casefold()] = service + + available = set(projected) + for service in projected.values(): + missing = [ + dependency + for dependency in service.depends_on + if dependency.casefold() not in available + ] + if missing: + raise ValueError( + f"Service {service.name} references unknown dependencies: {', '.join(missing)}" + ) + + _validate_acyclic(list(projected.values())) + + +def export_document(store: Store) -> ConfigDocument: + services = store.list_services() + names = {service.id: service.name for service in services} + return ConfigDocument( + version=2, + services=[ + ConfigService( + name=service.name, + kind=service.kind, + url=service.url, + host=service.host, + port=service.port, + depends_on=[names[item] for item in service.depends_on if item in names], + description=service.description, + tags=service.tags, + group=service.group, + environment=service.environment, + owner=service.owner, + enabled=service.enabled, + monitoring_enabled=service.monitoring_enabled, + criticality=service.criticality, + source_type=service.source_type, + source_ref=service.source_ref, + interval_seconds=service.interval_seconds, + timeout_seconds=service.timeout_seconds, + retries=service.retries, + failure_threshold=service.failure_threshold, + recovery_threshold=service.recovery_threshold, + expected_status_min=service.expected_status_min, + expected_status_max=service.expected_status_max, + availability_target=service.availability_target, + maintenance_enabled=service.maintenance_enabled, + maintenance_reason=service.maintenance_reason, + ) + for service in services + ], + ) + + +def serialize_document(document: ConfigDocument, format_name: str) -> str: + payload = document.model_dump(mode="json") + if format_name == "json": + return json.dumps(payload, indent=2, sort_keys=False) + return yaml.safe_dump(payload, sort_keys=False, allow_unicode=True) + + +def import_document(store: Store, document: ConfigDocument, mode: str) -> ConfigImportResult: + _validate_import_graph(store, document, mode) + return store.import_config_services(document.services, mode) diff --git a/backend/app/demo.py b/backend/app/demo.py new file mode 100644 index 0000000..efc40ee --- /dev/null +++ b/backend/app/demo.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid5 + +from .models import ( + ComponentRole, + Criticality, + DiscoveryCategory, + HealthStatus, + ProbeResult, + Service, + ServiceKind, +) + +DEMO_SCENARIO_NAME = "Northstar Platform" +DEMO_ROOT_CAUSE_NAME = "PostgreSQL" +_DEMO_NAMESPACE = UUID("4b3f31f4-8ee8-4e34-9a30-4bd4e5d4d4c1") + + +def _stable_id(label: str) -> str: + return str(uuid5(_DEMO_NAMESPACE, label)) + + +def build_demo_records(now: datetime | None = None) -> tuple[list[Service], list[ProbeResult]]: + """Build the complete deterministic Northstar topology and its synthetic history.""" + + current = now or datetime.now(UTC) + definitions = [ + ("Public Endpoint", ServiceKind.HTTP, "https://northstar.synthetic/public", None, None, ["Reverse Proxy"]), + ("Reverse Proxy", ServiceKind.HTTP, "https://northstar.synthetic/proxy", None, None, ["Web Frontend", "Auth Service"]), + ("Web Frontend", ServiceKind.HTTP, "https://northstar.synthetic/web", None, None, ["API Gateway"]), + ("Auth Service", ServiceKind.HTTP, "https://northstar.synthetic/auth", None, None, []), + ("API Gateway", ServiceKind.HTTP, "https://northstar.synthetic/api", None, None, ["PostgreSQL", "Redis"]), + ("PostgreSQL", ServiceKind.TCP, None, "postgresql.northstar.synthetic", 5432, []), + ("Redis", ServiceKind.TCP, None, "redis.northstar.synthetic", 6379, []), + ("Background Worker", ServiceKind.TCP, None, "worker.northstar.synthetic", 9000, ["PostgreSQL", "Redis", "Object Storage"]), + ("Object Storage", ServiceKind.HTTP, "https://northstar.synthetic/object-storage", None, None, []), + ] + ids_by_name = {name: _stable_id(f"service:{name}") for name, *_ in definitions} + metadata = { + "Public Endpoint": ("Edge", "Public Gateway", "Edge", ComponentRole.FRONTEND, DiscoveryCategory.PROXY, Criticality.CRITICAL, "Customer-facing availability check", 100.0), + "Reverse Proxy": ("Edge", "Public Gateway", "Edge", ComponentRole.PROXY, DiscoveryCategory.PROXY, Criticality.HIGH, "Northstar ingress gateway", 100.0), + "Web Frontend": ("Application", "Northstar App", "Application", ComponentRole.FRONTEND, DiscoveryCategory.APPLICATION, Criticality.HIGH, "Browser application", 99.0), + "Auth Service": ("Application", "Northstar App", "Application", ComponentRole.API, DiscoveryCategory.APPLICATION, Criticality.HIGH, "Identity and session service", 100.0), + "API Gateway": ("Application", "Northstar App", "Application", ComponentRole.API, DiscoveryCategory.APPLICATION, Criticality.CRITICAL, "Application API entry point", 99.0), + "PostgreSQL": ("Data", "Data Platform", "Data", ComponentRole.DATABASE, DiscoveryCategory.DATABASE, Criticality.CRITICAL, "Primary relational datastore", 99.9), + "Redis": ("Data", "Data Platform", "Data", ComponentRole.CACHE, DiscoveryCategory.CACHE, Criticality.HIGH, "Cache and queue backend", 100.0), + "Background Worker": ("Application", "Worker Platform", "Application", ComponentRole.WORKER, DiscoveryCategory.BACKGROUND_WORKER, Criticality.HIGH, "Asynchronous job processor", 99.0), + "Object Storage": ("Data", "Worker Platform", "Application", ComponentRole.STORAGE, DiscoveryCategory.STORAGE, Criticality.MEDIUM, "Durable object store", 100.0), + } + services = [ + Service( + id=ids_by_name[name], + name=name, + kind=kind, + url=url, + host=host, + port=port, + depends_on=[ids_by_name[item] for item in dependencies], + group=metadata[name][0], + environment="production", + application=metadata[name][1], + domain=metadata[name][2], + component_role=metadata[name][3], + category=metadata[name][4], + criticality=metadata[name][5], + description=metadata[name][6], + availability_target=metadata[name][7], + interval_seconds=60, + created_at=current - timedelta(days=30), + updated_at=current, + ) + for name, kind, url, host, port, dependencies in definitions + ] + + probes: list[ProbeResult] = [] + for name, *_ in definitions: + for index in range(24): + status = _history_status(name, index) + checked_at = current - timedelta(hours=23 - index) + latency = None if status == HealthStatus.DOWN else round(42 + ((index * 17 + len(name) * 3) % 120), 2) + detail = _probe_detail(name, status, index) + probes.append( + ProbeResult( + id=_stable_id(f"probe:{name}:{index}"), + service_id=ids_by_name[name], + status=status, + latency_ms=latency, + detail=detail, + checked_at=checked_at, + trigger="synthetic", + metadata={"scenario": DEMO_SCENARIO_NAME}, + ) + ) + return services, probes + + +def _history_status(name: str, index: int) -> HealthStatus: + if name == "PostgreSQL": + if index in {21, 23}: + return HealthStatus.DOWN + if index == 22: + return HealthStatus.DEGRADED + if name in {"API Gateway", "Background Worker"} and index == 20: + return HealthStatus.DOWN + if name == "Web Frontend" and index == 21: + return HealthStatus.DOWN + if name == "Reverse Proxy" and index == 22: + return HealthStatus.DEGRADED + return HealthStatus.HEALTHY + + +def _probe_detail(name: str, status: HealthStatus, index: int) -> str: + if name == DEMO_ROOT_CAUSE_NAME and status == HealthStatus.DOWN: + return "Synthetic outage: PostgreSQL connection refused" + if status == HealthStatus.DOWN: + return f"Synthetic dependency impact observed at history point {index + 1}" + if status == HealthStatus.DEGRADED: + return f"Synthetic elevated latency observed at history point {index + 1}" + return "Synthetic monitoring observation" diff --git a/backend/app/discovery.py b/backend/app/discovery.py new file mode 100644 index 0000000..8286cd8 --- /dev/null +++ b/backend/app/discovery.py @@ -0,0 +1,996 @@ +from __future__ import annotations + +import hashlib +import os +import threading +from datetime import UTC, datetime +from typing import Any +from uuid import uuid4 + +from .discovery_store import DiscoveryStore +from .docker_discovery import discover_containers +from .evidence import appops_health_status +from .health import validate_dependencies +from .models import ( + Criticality, + DiscoveryCategory, + DiscoveryConfidence, + DiscoveryDiff, + DiscoveryLifecycle, + DiscoveryReconcileRequest, + DiscoveryResult, + DiscoveryRun, + DiscoveryRunRequest, + DiscoveryRunStatus, + DockerCandidate, + EdgeMutationRequest, + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthStatus, + MonitoringState, + ProbeEndpointSource, + ProbeReachability, + ProbeRepairChange, + ProbeRepairReport, + ProviderHealth, + ProviderStatus, + ReconcileResult, + RelationEvidence, + RelationType, + Service, + ServiceCreate, + ServiceKind, + ServiceUpdate, + SuggestionStatus, + TopologySuggestion, +) +from .observation import observation_ttl +from .store import Store + + +def _now() -> datetime: + return datetime.now(UTC) + + +def _evidence_ttl(): + try: + interval = max(10, int(os.getenv("SERVICEMAP_OBSERVATION_INTERVAL_SECONDS", "60"))) + except ValueError: + interval = 60 + return observation_ttl(interval) + + +def _fingerprint(*parts: object) -> str: + value = "|".join(str(part or "") for part in parts) + return hashlib.sha256(value.encode("utf-8")).hexdigest() + + +def _confidence(value: str | DiscoveryConfidence) -> DiscoveryConfidence: + try: + return value if isinstance(value, DiscoveryConfidence) else DiscoveryConfidence(value) + except ValueError: + return DiscoveryConfidence.LOW + + +def _criticality(value: str | None) -> Criticality: + normalized = (value or "medium").casefold() + if normalized in {item.value for item in Criticality}: + return Criticality(normalized) + if normalized in {"urgent", "very_high"}: + return Criticality.CRITICAL + return Criticality.MEDIUM + + +def _application_name(candidate: DockerCandidate) -> str: + explicit = candidate.labels.get("servicemap.application", "").strip() + if explicit: + return explicit + if candidate.compose_project: + return candidate.compose_project.replace("-", " ").replace("_", " ").title() + if candidate.appops_app_id: + return f"AppOps · {candidate.appops_app_id}" + return candidate.category.value.replace("_", " ").title() + + +def _metadata(candidate: DockerCandidate) -> dict[str, object]: + return { + "candidate": candidate.model_dump(mode="json"), + "stable_source_ref": candidate.stable_source_ref, + "container_id": candidate.container_id, + "image": candidate.image, + "state": candidate.state, + "health": candidate.health, + "networks": candidate.networks, + "ports": [item.model_dump(mode="json") for item in candidate.ports], + "labels": candidate.labels, + "compose_project": candidate.compose_project, + "compose_service": candidate.compose_service, + "category": candidate.category.value, + "visibility": candidate.visibility.value, + "environment_evidence": candidate.environment_evidence, + "proxy_route_evidence": candidate.proxy_route_evidence, + "appops_app_id": candidate.appops_app_id, + } + + +def _same_observation(previous: dict[str, object], current: dict[str, object]) -> bool: + ignored = {"state", "health", "container_id"} + return {key: value for key, value in previous.items() if key not in ignored} == { + key: value for key, value in current.items() if key not in ignored + } + + +class DiscoveryManager: + def __init__(self, store: Store, discovery_store: DiscoveryStore) -> None: + self.store = store + self.discovery_store = discovery_store + self._lock = threading.Lock() + + def _docker_host(self, requested: str | None) -> str: + return requested or os.getenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock") + + def _appops_url(self, requested: str | None) -> str | None: + return requested or os.getenv("SERVICEMAP_APPOPS_URL") + + def _existing_service_for_ref(self, services: list[Service], source_ref: str | None) -> Service | None: + if not source_ref: + return None + return next((item for item in services if item.source_ref == source_ref), None) + + def _candidate_suggestion(self, candidate: DockerCandidate): + return candidate.suggestions[0] if candidate.suggestions else None + + def _relation_suggestions( + self, + run: DiscoveryRun, + candidates: list[DockerCandidate], + service_by_ref: dict[str, Service], + ) -> list[TopologySuggestion]: + result: list[TopologySuggestion] = [] + + def aliases(candidate: DockerCandidate) -> set[str]: + return {item.casefold() for item in (candidate.name, candidate.compose_service or "") if item} + + def resolve(source: DockerCandidate, reference: str, *, compose_only: bool = False) -> list[DockerCandidate]: + """Resolve only inside a defensible compose/network scope. + + A duplicate service name must remain a review suggestion. Choosing + the last name seen in a host-wide index is not topology evidence. + """ + token = reference.casefold().strip() + matches = [item for item in candidates if token in aliases(item) or token == (item.stable_source_ref or "").casefold()] + same_compose = [item for item in matches if source.compose_project and item.compose_project and item.compose_project.casefold() == source.compose_project.casefold()] + if compose_only: + return same_compose + if same_compose: + return same_compose + shared_network = [item for item in matches if set(source.networks).intersection(item.networks)] + return shared_network or matches + + def add(source: DockerCandidate, target: DockerCandidate, confidence: DiscoveryConfidence, evidence: str, relation_type: RelationType = RelationType.DEPENDS_ON) -> None: + if source.stable_source_ref is None or target.stable_source_ref is None or source is target: + return + # Evidence changes as discovery learns more. It must enrich the + # same relationship rather than create a second suggestion/edge. + fingerprint = _fingerprint("relation", source.stable_source_ref, target.stable_source_ref, relation_type.value) + now = _now() + result.append( + self.discovery_store.save_suggestion( + TopologySuggestion( + id=f"suggest-{fingerprint[:14]}", + run_id=run.id, + suggestion_type="relation", + source_ref=source.stable_source_ref, + source_service_id=service_by_ref.get(source.stable_source_ref).id if source.stable_source_ref in service_by_ref else None, + target_ref=target.stable_source_ref, + target_service_id=service_by_ref.get(target.stable_source_ref).id if target.stable_source_ref in service_by_ref else None, + proposed_name=f"{source.name} depends on {target.name}", + relation_type=relation_type, + confidence=confidence, + evidence=[evidence], + created_at=now, + updated_at=now, + ), + fingerprint, + ) + ) + + def resolve_and_add( + source: DockerCandidate, + reference: str, + confidence: DiscoveryConfidence, + evidence: str, + *, + compose_only: bool = False, + relation_type: RelationType = RelationType.DEPENDS_ON, + ) -> None: + matches = resolve(source, reference, compose_only=compose_only) + if len(matches) == 1: + add(source, matches[0], confidence, evidence, relation_type) + return + if not matches or not source.stable_source_ref: + return + candidates_label = ", ".join(sorted(item.stable_source_ref or item.name for item in matches)[:6]) + fingerprint = _fingerprint("ambiguous-relation", source.stable_source_ref, reference.casefold(), RelationType.DEPENDS_ON.value) + now = _now() + result.append( + self.discovery_store.save_suggestion( + TopologySuggestion( + id=f"suggest-{fingerprint[:14]}", + run_id=run.id, + suggestion_type="relation", + source_ref=source.stable_source_ref, + source_service_id=service_by_ref.get(source.stable_source_ref).id if source.stable_source_ref in service_by_ref else None, + proposed_name=f"{source.name} has an ambiguous reference to {reference}", + relation_type=RelationType.DEPENDS_ON, + confidence=DiscoveryConfidence.LOW, + evidence=[f"{evidence}; candidates: {candidates_label}", "Manual review required; no canonical dependency was created"], + created_at=now, + updated_at=now, + ), + fingerprint, + ) + ) + + for source in candidates: + if not source.stable_source_ref: + continue + labels = source.labels + explicit = [item.strip() for item in labels.get("servicemap.depends_on", "").split(",") if item.strip()] + compose = [item.split(":", 1)[0].strip() for item in labels.get("com.docker.compose.depends_on", "").split(",") if item.strip()] + for name in sorted(set(explicit)): + resolve_and_add(source, name, DiscoveryConfidence.CONFIRMED, "Explicit servicemap.depends_on label") + for name in sorted(set(compose)): + resolve_and_add(source, name, DiscoveryConfidence.HIGH, "Docker Compose com.docker.compose.depends_on label", compose_only=True) + for item in source.environment_evidence: + if "hostname=" not in item: + continue + hostname = item.split("hostname=", 1)[1].split(":", 1)[0].casefold() + resolve_and_add(source, hostname, DiscoveryConfidence.MEDIUM, f"Environment endpoint reference: {item}") + for item in source.proxy_route_evidence: + proxies = [proxy for proxy in candidates if proxy.category == DiscoveryCategory.PROXY and set(proxy.networks).intersection(source.networks)] + if len(proxies) == 1: + add(proxies[0], source, DiscoveryConfidence.MEDIUM, item, RelationType.ROUTES_TO) + # Shared networks are review-only hints. Cap them to avoid flooding + # a large host with O(n²) low-confidence suggestions. + for name in source.dependency_suggestions[:8]: + resolve_and_add(source, name, DiscoveryConfidence.LOW, f"Shared Docker network hint: {', '.join(source.networks)}") + return result + + def run(self, request: DiscoveryRunRequest) -> DiscoveryResult: + if not self._lock.acquire(blocking=False): + raise ValueError("A discovery run is already in progress") + run = self.discovery_store.start_run() + try: + docker_host = self._docker_host(request.docker_host) + result = discover_containers( + docker_host, + self.store.list_services(), + include_stopped=request.include_stopped, + appops_url=self._appops_url(request.appops_url), + ) + observed_at = _now() + evidence_ttl = _evidence_ttl() + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.DOCKER, + status=ProviderHealth.HEALTHY, + observed_at=observed_at, + valid_until=observed_at + evidence_ttl, + detail=f"Docker inventory returned {len(result.candidates)} candidates", + metadata={"docker_host": docker_host}, + ) + ) + if self._appops_url(request.appops_url): + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.APPOPS, + status=ProviderHealth.HEALTHY if result.appops_available else ProviderHealth.UNAVAILABLE, + observed_at=observed_at, + valid_until=observed_at + evidence_ttl, + detail=result.appops_detail, + metadata={"mappings": sum(1 for item in result.candidates if item.appops_app_id)}, + ) + ) + existing = self.store.list_services() + by_ref = {service.source_ref: service for service in existing if service.source_ref} + diff = DiscoveryDiff() + observations = [] + source_counts: dict[str, int] = {"docker": len(result.candidates), "running": 0, "stopped": 0, "appops": 0, "proxy_routes": result.proxy_routes} + for candidate in result.candidates: + if candidate.state == "running": + source_counts["running"] += 1 + else: + source_counts["stopped"] += 1 + if candidate.appops_app_id: + source_counts["appops"] += 1 + stable_ref = candidate.stable_source_ref or f"docker:name:{candidate.name.casefold()}" + previous = self.discovery_store.get_source("docker", stable_ref) + previous_meta = previous.metadata if previous else {} + missing_cycles = int(previous_meta.get("missing_cycles", 0) or 0) if previous else 0 + if candidate.lifecycle == DiscoveryLifecycle.MISSING: + missing_cycles += 1 + if missing_cycles >= 3: + candidate.lifecycle = DiscoveryLifecycle.RETIRED + elif missing_cycles >= 2: + candidate.lifecycle = DiscoveryLifecycle.STALE + else: + missing_cycles = 0 + current_meta = _metadata(candidate) + current_meta["missing_cycles"] = missing_cycles + if candidate.lifecycle == DiscoveryLifecycle.MISSING: + change = "missing" + diff.missing += 1 + elif candidate.lifecycle == DiscoveryLifecycle.STALE: + change = "stale" + diff.stale += 1 + elif candidate.lifecycle == DiscoveryLifecycle.RETIRED: + change = "retired" + diff.stale += 1 + elif previous is None: + change = "new" + diff.new += 1 + elif _same_observation(previous_meta, current_meta): + change = "unchanged" + diff.unchanged += 1 + else: + change = "changed" + diff.changed += 1 + source = self.discovery_store.save_source( + source_type="docker", + source_id=stable_ref, + source_name=candidate.name, + lifecycle=candidate.lifecycle, + metadata=current_meta, + ) + observations.append( + self.discovery_store.save_observation( + run_id=run.id, + source=source, + source_name=candidate.name, + lifecycle=candidate.lifecycle, + change_type=change, + metadata=current_meta, + ) + ) + suggestion = self._candidate_suggestion(candidate) + if suggestion and candidate.lifecycle != DiscoveryLifecycle.MISSING and candidate.visibility.value != "ignored": + fingerprint = _fingerprint("service", stable_ref) + now = _now() + saved = self.discovery_store.save_suggestion( + TopologySuggestion( + id=f"suggest-{fingerprint[:14]}", + run_id=run.id, + suggestion_type="service", + source_ref=stable_ref, + source_service_id=by_ref.get(stable_ref).id if stable_ref in by_ref else None, + proposed_name=suggestion.name, + proposed_kind=suggestion.kind, + proposed_url=suggestion.url, + proposed_host=suggestion.host, + proposed_port=suggestion.port, + proposed_category=candidate.category, + proposed_group=suggestion.group, + confidence=_confidence(suggestion.confidence), + evidence=suggestion.evidence + candidate.environment_evidence, + created_at=now, + updated_at=now, + ), + fingerprint, + ) + if saved.status == SuggestionStatus.OPEN and change in {"new", "changed"}: + diff.new_suggestions += 1 + self._relation_suggestions(run, result.candidates, by_ref) + completed = self.discovery_store.finish_run( + run.id, + status=DiscoveryRunStatus.COMPLETED, + source_counts=source_counts, + diff=diff, + ) + suggestions = self.discovery_store.list_suggestions(run_id=run.id) + return DiscoveryResult( + docker_host=docker_host, + candidates=result.candidates, + run=completed, + suggestions=suggestions, + observations=observations, + appops_available=result.appops_available, + appops_mappings=source_counts["appops"], + proxy_routes=result.proxy_routes, + ) + except Exception as exc: + self.discovery_store.finish_run( + run.id, + status=DiscoveryRunStatus.FAILED, + source_counts={}, + diff=DiscoveryDiff(), + error=str(exc)[:500], + ) + raise + finally: + self._lock.release() + + def status(self) -> dict[str, object]: + current, completed = self.discovery_store.status() + return {"current": current, "last_completed": completed, "active": current is not None} + + def _payload_for_candidate(self, candidate: DockerCandidate, name: str | None = None) -> ServiceCreate: + suggestion = self._candidate_suggestion(candidate) + primary = next((item for item in candidate.probe_candidates if item.primary and item.reachability in {ProbeReachability.VERIFIED, ProbeReachability.REACHABLE}), None) + kind = primary.kind if primary else (suggestion.kind if suggestion else ServiceKind.TCP) + # Suggestions are discovery evidence, not authorization to probe. A + # published endpoint is only promoted after a bounded preflight marks + # it as the primary candidate. + url = primary.url if primary else None + host = primary.host if primary else None + port = primary.port if primary else None + # HTTP probing requires a complete URL. A container can expose only + # an internal HTTP-like port, which is useful classification evidence + # but is not a safe externally reachable endpoint for ServiceMap. + if kind == ServiceKind.HTTP: + # Normalize stale/legacy candidate suggestions as well as fresh + # Docker observations to the canonical URL-only representation. + host = None + port = None + monitoring = bool(url) if kind == ServiceKind.HTTP else bool(url or (host and port)) + if not monitoring: + url = None + host = None + port = None + group = suggestion.group if suggestion else (f"Compose · {candidate.compose_project}" if candidate.compose_project else candidate.category.value.title()) + if monitoring: + monitoring_state = MonitoringState.ACTIVE + probe_source = primary.source if primary else ProbeEndpointSource.KNOWN_URL + probe_reachability = primary.reachability if primary else ProbeReachability.REACHABLE + probe_confidence = primary.confidence if primary else DiscoveryConfidence.HIGH + elif candidate.healthcheck_present: + monitoring_state = MonitoringState.DOCKER_HEALTH + probe_source = ProbeEndpointSource.DOCKER_HEALTHCHECK + probe_reachability = ProbeReachability.VERIFIED + probe_confidence = DiscoveryConfidence.HIGH + elif candidate.state == "running": + monitoring_state = MonitoringState.OBSERVED + probe_source = ( + ProbeEndpointSource.CONTAINER_INTERNAL + if candidate.probe_candidates + else ProbeEndpointSource.DISCOVERED_ENVIRONMENT + ) + probe_reachability = ( + ProbeReachability.INTERNAL_ONLY + if candidate.probe_candidates + else ProbeReachability.NOT_CONFIGURED + ) + probe_confidence = DiscoveryConfidence.LOW + else: + monitoring_state = MonitoringState.UNMONITORED + probe_source = ProbeEndpointSource.CONTAINER_INTERNAL if candidate.probe_candidates else ProbeEndpointSource.DISCOVERED_ENVIRONMENT + probe_reachability = ProbeReachability.INTERNAL_ONLY if candidate.probe_candidates else ProbeReachability.NOT_CONFIGURED + probe_confidence = DiscoveryConfidence.LOW + return ServiceCreate( + name=name or (suggestion.name if suggestion else candidate.name), + kind=kind, + url=url, + host=host, + port=port, + description=f"Discovered Docker service from {candidate.image}", + tags=["discovered", "docker"], + group=group, + environment="production", + criticality=_criticality(candidate.appops_criticality), + category=candidate.category, + visibility=candidate.visibility, + lifecycle=candidate.lifecycle, + source_type="docker", + source_ref=candidate.stable_source_ref, + first_seen_at=_now(), + last_seen_at=_now(), + last_discovery_status=candidate.state, + container_image=candidate.image, + compose_project=candidate.compose_project, + monitoring_enabled=monitoring, + monitoring_state=monitoring_state, + probe_source=probe_source, + probe_reachability=probe_reachability, + probe_confidence=probe_confidence, + runtime_state=candidate.state, + runtime_health=candidate.health, + application=_application_name(candidate), + enabled=True, + ) + + def _update_discovered_service(self, service: Service, candidate: DockerCandidate) -> Service: + payload = self._payload_for_candidate(candidate, service.name) + updates: dict[str, Any] = payload.model_dump() + overrides = { + "name": self.discovery_store.has_override("service", service.id, "name"), + "group": self.discovery_store.has_override("service", service.id, "group"), + "category": self.discovery_store.has_override("service", service.id, "category"), + "criticality": self.discovery_store.has_override("service", service.id, "criticality"), + "environment": self.discovery_store.has_override("service", service.id, "environment"), + "probe": self.discovery_store.has_override("service", service.id, "probe"), + "visibility": self.discovery_store.has_override("service", service.id, "visibility"), + } + current = service.model_dump() + for field, locked in overrides.items(): + if locked: + if field == "probe": + for endpoint_field in ("kind", "url", "host", "port", "monitoring_enabled", "monitoring_state", "probe_source", "probe_reachability", "probe_confidence"): + updates[endpoint_field] = current[endpoint_field] + else: + updates[field] = current[field] + updates["depends_on"] = service.depends_on + # Layout is operator-owned state; rediscovery must not snap a manually + # positioned node back to an inferred default. + for field in ("layout_x", "layout_y", "layout_pinned"): + updates[field] = current[field] + updates["first_seen_at"] = service.first_seen_at or payload.first_seen_at + updates["last_seen_at"] = payload.last_seen_at + updates["lifecycle"] = candidate.lifecycle + updates["last_discovery_status"] = candidate.state + return self.store.update_service(service.id, ServiceUpdate(**updates)) or service + + def _record_candidate_evidence(self, candidate: DockerCandidate, service: Service) -> None: + """Persist source-scoped discovery claims without flattening their scope.""" + + now = _now() + evidence_ttl = _evidence_ttl() + health = (candidate.health or "").casefold() + runtime = candidate.state.casefold() + component_status: HealthStatus | None = None + if health in {"unhealthy", "down"} or runtime in {"exited", "dead", "stopped"}: + component_status = HealthStatus.DOWN + elif health == "degraded" or runtime == "restarting": + component_status = HealthStatus.DEGRADED + elif health == "healthy": + component_status = HealthStatus.HEALTHY + if component_status is not None: + self.store.record_health_evidence( + HealthEvidence( + id=str(uuid4()), + subject_type="service", + subject_id=service.id, + service_id=service.id, + provider=EvidenceProvider.DOCKER, + provider_ref=candidate.stable_source_ref, + status=component_status, + scope=EvidenceScope.COMPONENT, + confidence=0.95 if candidate.healthcheck_present else 0.8, + observed_at=now, + valid_until=now + evidence_ttl, + received_at=now, + summary="Docker healthcheck" if candidate.healthcheck_present else "Docker runtime state", + metadata={"container_id": candidate.container_id, "state": candidate.state, "health": candidate.health}, + ) + ) + normalized_appops = appops_health_status(candidate.appops_health) + if candidate.appops_app_id and normalized_appops is not None: + self.store.record_health_evidence( + HealthEvidence( + id=str(uuid4()), + subject_type="application", + subject_id=f"appops:{candidate.appops_app_id}", + service_id=None, + provider=EvidenceProvider.APPOPS, + provider_ref=candidate.appops_app_id, + status=normalized_appops, + scope=EvidenceScope.APPLICATION, + confidence=candidate.appops_discovery_confidence if candidate.appops_discovery_confidence is not None else 0.5, + observed_at=now, + valid_until=now + evidence_ttl, + received_at=now, + summary="AppOps application health; not used as component direct health", + metadata={ + "app_id": candidate.appops_app_id, + "category": candidate.appops_category, + "policy_profile": candidate.appops_policy_profile, + "app_type": candidate.appops_app_type, + "discovery_confidence": candidate.appops_discovery_confidence, + }, + ), + service_ids=[service.id], + ) + + def _service_by_ref(self) -> dict[str, Service]: + return {service.source_ref: service for service in self.store.list_services() if service.source_ref} + + def reconcile(self, request: DiscoveryReconcileRequest) -> ReconcileResult: + run = self.discovery_store.get_run(request.run_id) if request.run_id else self.discovery_store.last_completed() + if run is None or run.status != DiscoveryRunStatus.COMPLETED: + raise ValueError("A completed discovery run is required before reconciliation") + self.discovery_store.set_run_status(run.id, DiscoveryRunStatus.RECONCILING) + candidates = [DockerCandidate.model_validate(item.get("candidate", item)) for item in self.discovery_store.candidates_for_run(run.id)] + by_ref = self._service_by_ref() + created = updated = unchanged = missing = retired = 0 + service_ids: list[str] = [] + for candidate in candidates: + if not candidate.stable_source_ref: + continue + existing = by_ref.get(candidate.stable_source_ref) + if candidate.lifecycle in {DiscoveryLifecycle.MISSING, DiscoveryLifecycle.STALE, DiscoveryLifecycle.RETIRED}: + if existing: + if candidate.lifecycle == DiscoveryLifecycle.MISSING: + missing += 1 + else: + retired += 1 + payload = ServiceUpdate.model_validate(existing.model_dump()) + payload.lifecycle = candidate.lifecycle + payload.last_discovery_status = "missing" + payload.monitoring_enabled = False + self.store.update_service(existing.id, payload) + continue + if candidate.visibility.value == "ignored": + continue + if existing is None and candidate.state == "running" and request.apply_high_confidence: + payload = self._payload_for_candidate(candidate) + name_conflict = self.store.get_service_by_name(payload.name) + if name_conflict: + payload.name = f"{payload.name} [{candidate.container_id[:6]}]" + existing = self.store.create_service(payload) + by_ref[candidate.stable_source_ref] = existing + created += 1 + self.discovery_store.record_change("discovered", f"Discovered {existing.name}", service_id=existing.id) + elif existing is not None: + current = existing.model_dump() + updated_service = self._update_discovered_service(existing, candidate) + unchanged += 1 if updated_service.model_dump() == current else 0 + updated += 0 if updated_service.model_dump() == current else 1 + existing = updated_service + by_ref[candidate.stable_source_ref] = existing + if existing: + service_ids.append(existing.id) + source = self.discovery_store.get_source("docker", candidate.stable_source_ref) + if source: + self.discovery_store.attach_source(existing.id, source.id) + self._record_candidate_evidence(candidate, existing) + + # The v1.1 generic baseline is preserved for history but no longer + # appears as the primary production topology after explicit bootstrap. + if request.apply_high_confidence and created: + discovered_ids = set(service_ids) + for service in self.store.list_services(): + if service.id in discovered_ids or service.source_type: + continue + payload = ServiceUpdate.model_validate(service.model_dump()) + payload.visibility = "hidden" + payload.lifecycle = DiscoveryLifecycle.RETIRED + payload.monitoring_enabled = False + self.store.update_service(service.id, payload) + retired += 1 + + accepted_relations = 0 + suggestions = self.discovery_store.list_suggestions(run_id=run.id) + by_ref = self._service_by_ref() + for suggestion in suggestions: + if suggestion.suggestion_type != "relation" or not suggestion.source_ref or not suggestion.target_ref: + continue + source = by_ref.get(suggestion.source_ref) + target = by_ref.get(suggestion.target_ref) + if not source or not target: + continue + relation_type = suggestion.relation_type or RelationType.DEPENDS_ON + previous_evidence = self.discovery_store.get_evidence_for_relation( + source.id, + target.id, + suggestion.target_ref, + relation_type, + ) + confidence_rank = { + DiscoveryConfidence.LOW: 1, + DiscoveryConfidence.MEDIUM: 2, + DiscoveryConfidence.HIGH: 3, + DiscoveryConfidence.CONFIRMED: 4, + } + evidence_parts = list(suggestion.evidence) + if previous_evidence: + evidence_parts = [previous_evidence.explanation, *evidence_parts] + evidence_text = "; ".join(dict.fromkeys(part for part in evidence_parts if part)) + evidence = RelationEvidence( + id=suggestion.id, + source_service_id=source.id, + target_service_id=target.id, + target_ref=suggestion.target_ref, + relation_type=relation_type, + evidence_type="discovery", + confidence=( + previous_evidence.confidence + if previous_evidence and confidence_rank[previous_evidence.confidence] >= confidence_rank[suggestion.confidence] + else suggestion.confidence + ), + explanation=evidence_text, + source_ref=suggestion.source_ref, + locked=previous_evidence.locked if previous_evidence else suggestion.status == SuggestionStatus.ACCEPTED, + active=previous_evidence.active if previous_evidence else True, + first_seen_at=previous_evidence.first_seen_at if previous_evidence else suggestion.created_at, + last_seen_at=suggestion.updated_at, + ) + self.discovery_store.save_evidence(evidence) + if previous_evidence and not previous_evidence.active: + # An operator rejected/removed this inferred edge. Keep the + # durable evidence for audit, but do not resurrect its health + # propagation on a later discovery cycle. + continue + should_accept = suggestion.status == SuggestionStatus.ACCEPTED or ( + request.apply_high_confidence and suggestion.confidence in {DiscoveryConfidence.CONFIRMED, DiscoveryConfidence.HIGH} + ) + if not should_accept or suggestion.relation_type != RelationType.DEPENDS_ON: + continue + if target.id in source.depends_on: + if suggestion.status != SuggestionStatus.ACCEPTED: + self.discovery_store.set_suggestion_status(suggestion.id, SuggestionStatus.ACCEPTED) + continue + try: + validate_dependencies([*source.depends_on, target.id], self.store, service_id=source.id) + except ValueError: + continue + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [*source.depends_on, target.id] + self.store.update_service(source.id, payload) + self.discovery_store.set_suggestion_status(suggestion.id, SuggestionStatus.ACCEPTED) + self.discovery_store.record_change("dependency_added", f"{source.name} depends on {target.name}", source_service_id=source.id, target_service_id=target.id) + accepted_relations += 1 + + final_run = self.discovery_store.finish_run( + run.id, + status=DiscoveryRunStatus.COMPLETED, + source_counts=run.source_counts, + diff=run.diff, + ) + self.discovery_store.capture_snapshot(run_id=final_run.id) + return ReconcileResult( + run=final_run, + created=created, + updated=updated, + unchanged=unchanged, + missing=missing, + retired=retired, + accepted_relations=accepted_relations, + open_suggestions=len(self.discovery_store.list_suggestions(SuggestionStatus.OPEN)), + service_ids=sorted(set(service_ids)), + ) + + @staticmethod + def _endpoint_label(service: Service | ServiceCreate) -> str | None: + if service.url: + return service.url + if service.host: + return f"{service.host}{f':{service.port}' if service.port else ''}" + return None + + def repair_probe_reachability(self, *, run_id: str | None = None, dry_run: bool = True) -> ProbeRepairReport: + """Safely retire unsafe discovery endpoints without touching probe history. + + Only Docker-managed services are considered. User probe overrides remain + authoritative and the operation intentionally never deletes a service, + relation, probe result, or discovery observation. + """ + + run = self.discovery_store.get_run(run_id) if run_id else self.discovery_store.last_completed() + if run is None or run.status != DiscoveryRunStatus.COMPLETED: + raise ValueError("A completed discovery run is required before repairing probe reachability") + candidates = [DockerCandidate.model_validate(item.get("candidate", item)) for item in self.discovery_store.candidates_for_run(run.id)] + services = self._service_by_ref() + changes: list[ProbeRepairChange] = [] + disabled_unsafe = enabled_published = docker_health_observed = 0 + appops_observed = runtime_observed = unmonitored = 0 + for candidate in candidates: + if not candidate.stable_source_ref or candidate.lifecycle != DiscoveryLifecycle.ACTIVE: + continue + current = services.get(candidate.stable_source_ref) + if current is None: + continue + desired = self._payload_for_candidate(candidate, current.name) + is_override = self.discovery_store.has_override("service", current.id, "probe") + next_endpoint = self._endpoint_label(desired) + previous_endpoint = self._endpoint_label(current) + if is_override: + next_endpoint = previous_endpoint + changed = (not is_override) and any( + getattr(current, field) != getattr(desired, field) + for field in ("kind", "url", "host", "port", "monitoring_enabled", "monitoring_state", "probe_source", "probe_reachability", "probe_confidence") + ) + if not changed: + if not dry_run: + self.store.set_runtime_observation( + current.id, + runtime_state=candidate.state, + runtime_health=candidate.health, + monitoring_state=current.monitoring_state.value, + ) + continue + if current.monitoring_enabled and not desired.monitoring_enabled: + reason = "Internal container endpoint is not reachable from ServiceMap; active probe disabled" + disabled_unsafe += 1 + elif desired.monitoring_enabled: + reason = "Host-published endpoint selected using the configured probe host" + enabled_published += 1 + elif candidate.healthcheck_present: + reason = "Docker healthcheck retained as runtime observation without a network probe" + docker_health_observed += 1 + elif desired.monitoring_state == MonitoringState.OBSERVED: + reason = "No verified reachable endpoint; Docker runtime observation is retained without a network probe" + runtime_observed += 1 + else: + reason = "No verified reachable endpoint; service remains observed as unmonitored" + unmonitored += 1 + changes.append( + ProbeRepairChange( + service_id=current.id, + service_name=current.name, + previous_monitoring_state=current.monitoring_state, + next_monitoring_state=desired.monitoring_state, + previous_endpoint=previous_endpoint, + next_endpoint=next_endpoint, + next_probe_source=desired.probe_source, + next_probe_confidence=desired.probe_confidence, + reason=reason, + ) + ) + + if not dry_run: + updated = self._update_discovered_service(current, candidate) + monitoring_state = updated.monitoring_state.value + self.store.set_runtime_observation( + updated.id, + runtime_state=candidate.state, + runtime_health=candidate.health, + monitoring_state=monitoring_state, + ) + self.discovery_store.record_change("probe_endpoint_superseded", reason, service_id=updated.id) + resolved = 0 + if not dry_run: + from .incidents import sync_incidents + + before = len([item for item in self.store.list_incidents(active_only=True)]) + sync_incidents(self.store, resolution_reason="Monitoring endpoint superseded; active probe was replaced by reachability-safe observation") + after = len([item for item in self.store.list_incidents(active_only=True)]) + resolved = max(0, before - after) + return ProbeRepairReport( + run_id=run.id, + dry_run=dry_run, + inspected=sum(1 for candidate in candidates if candidate.stable_source_ref in services), + changed=len(changes), + disabled_unsafe=disabled_unsafe, + enabled_published=enabled_published, + docker_health_observed=docker_health_observed, + appops_observed=appops_observed, + runtime_observed=runtime_observed, + unmonitored=unmonitored, + resolved_incidents=resolved, + changes=changes, + ) + + def apply_edge(self, request: EdgeMutationRequest) -> RelationEvidence: + source = self.store.get_service(request.source_service_id) + target = self.store.get_service(request.target_service_id) + if source is None or target is None: + raise ValueError("Both edge services must exist") + if request.relation_type == RelationType.DEPENDS_ON and target.id not in source.depends_on: + validate_dependencies([*source.depends_on, target.id], self.store, service_id=source.id) + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [*source.depends_on, target.id] + self.store.update_service(source.id, payload) + now = _now() + evidence = RelationEvidence( + id=f"manual-edge-{uuid4().hex[:14]}", + source_service_id=source.id, + target_service_id=target.id, + relation_type=request.relation_type, + evidence_type="manual", + confidence=DiscoveryConfidence.CONFIRMED, + explanation=request.explanation, + source_ref=f"manual:{source.id}->{target.id}", + locked=request.lock, + first_seen_at=now, + last_seen_at=now, + ) + saved = self.discovery_store.save_evidence(evidence) + self.discovery_store.save_override("relation", saved.id, "locked", request.lock, request.lock) + self.discovery_store.record_change("dependency_added", request.explanation, source_service_id=source.id, target_service_id=target.id) + return saved + + def suggestion_action(self, suggestion_id: str, action: str) -> TopologySuggestion: + suggestion = self.discovery_store.get_suggestion(suggestion_id) + if suggestion is None: + raise ValueError("Suggestion not found") + if action == "reject": + if suggestion.suggestion_type == "relation" and suggestion.relation_type == RelationType.DEPENDS_ON: + source = self._service_by_ref().get(suggestion.source_ref or "") + target = self._service_by_ref().get(suggestion.target_ref or "") + existing_evidence = self.discovery_store.get_evidence(suggestion.id) + if source and target and (existing_evidence is None or not existing_evidence.locked): + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [item for item in source.depends_on if item != target.id] + self.store.update_service(source.id, payload) + if existing_evidence: + self.discovery_store.set_evidence_state(suggestion.id, active=False) + return self.discovery_store.set_suggestion_status(suggestion_id, SuggestionStatus.REJECTED) or suggestion + if action in {"confirm", "lock"}: + if suggestion.suggestion_type == "service": + if not suggestion.source_ref: + raise ValueError("Service suggestion has no stable source reference") + existing = self._service_by_ref().get(suggestion.source_ref) + if existing is None: + candidate = next( + ( + DockerCandidate.model_validate(item.get("candidate", item)) + for item in self.discovery_store.candidates_for_run(suggestion.run_id) + if item.get("candidate", {}).get("stable_source_ref") == suggestion.source_ref + ), + None, + ) + if candidate is None: + raise ValueError("Discovery observation for service suggestion is no longer available") + existing = self.store.create_service(self._payload_for_candidate(candidate)) + source = self.discovery_store.get_source("docker", suggestion.source_ref) + if source: + self.discovery_store.attach_source(existing.id, source.id) + self.discovery_store.record_change("accepted", f"Accepted discovered service {existing.name}", service_id=existing.id) + self.discovery_store.save_override("service", existing.id, "source", suggestion.source_ref, True) + elif suggestion.suggestion_type == "relation": + source = self._service_by_ref().get(suggestion.source_ref or "") + target = self._service_by_ref().get(suggestion.target_ref or "") + if source is None or target is None: + raise ValueError("Accept the related services before accepting this relationship") + if suggestion.relation_type == RelationType.DEPENDS_ON and target.id not in source.depends_on: + validate_dependencies([*source.depends_on, target.id], self.store, service_id=source.id) + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [*source.depends_on, target.id] + self.store.update_service(source.id, payload) + now = _now() + self.discovery_store.save_evidence( + RelationEvidence( + id=suggestion.id, + source_service_id=source.id, + target_service_id=target.id, + target_ref=suggestion.target_ref, + relation_type=suggestion.relation_type or RelationType.DEPENDS_ON, + evidence_type="discovery", + confidence=suggestion.confidence, + explanation="; ".join(suggestion.evidence), + source_ref=suggestion.source_ref, + locked=action == "lock", + first_seen_at=suggestion.created_at, + last_seen_at=now, + ) + ) + self.discovery_store.record_change("dependency_added", f"Accepted relationship {source.name} → {target.name}", source_service_id=source.id, target_service_id=target.id) + return self.discovery_store.set_suggestion_status(suggestion_id, SuggestionStatus.ACCEPTED) or suggestion + raise ValueError("Unsupported suggestion action") + + def record_service_override(self, before: Service, after: Service) -> None: + for field in ("name", "group", "category", "criticality", "environment", "visibility"): + if getattr(before, field) != getattr(after, field): + value = getattr(after, field) + self.discovery_store.save_override("service", after.id, field, value.value if hasattr(value, "value") else value) + if (before.kind, before.url, before.host, before.port, before.monitoring_enabled) != ( + after.kind, + after.url, + after.host, + after.port, + after.monitoring_enabled, + ): + self.discovery_store.save_override("service", after.id, "probe", after.model_dump(include={"kind", "url", "host", "port", "monitoring_enabled"})) + + def edge_action(self, evidence_id: str, action: str) -> RelationEvidence: + evidence = self.discovery_store.get_evidence(evidence_id) + if evidence is None or evidence.target_service_id is None: + raise ValueError("Edge evidence not found") + source = self.store.get_service(evidence.source_service_id) + if source is None: + raise ValueError("Edge source service not found") + target_id = evidence.target_service_id + if action in {"remove", "reject"} and evidence.relation_type == RelationType.DEPENDS_ON: + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [item for item in source.depends_on if item != target_id] + self.store.update_service(source.id, payload) + self.discovery_store.record_change("dependency_removed", f"Removed relationship from {source.name}", source_service_id=source.id, target_service_id=target_id) + return self.discovery_store.set_evidence_state(evidence_id, active=False) or evidence + if action == "confirm" and evidence.relation_type == RelationType.DEPENDS_ON and target_id not in source.depends_on: + validate_dependencies([*source.depends_on, target_id], self.store, service_id=source.id) + payload = ServiceUpdate.model_validate(source.model_dump()) + payload.depends_on = [*source.depends_on, target_id] + self.store.update_service(source.id, payload) + if action == "lock": + self.discovery_store.save_override("relation", evidence_id, "locked", True, True) + return self.discovery_store.set_evidence_state(evidence_id, active=True, locked=action == "lock" or evidence.locked) or evidence diff --git a/backend/app/discovery_store.py b/backend/app/discovery_store.py new file mode 100644 index 0000000..11d560c --- /dev/null +++ b/backend/app/discovery_store.py @@ -0,0 +1,979 @@ +from __future__ import annotations + +import json +import sqlite3 +from datetime import UTC, datetime +from hashlib import sha256 +from typing import Any +from uuid import uuid4 + +from .models import ( + DiscoveryConfidence, + DiscoveryDiff, + DiscoveryLifecycle, + DiscoveryObservation, + DiscoveryRun, + DiscoveryRunStatus, + LogicalRelation, + RelationEvidence, + RelationType, + ServiceSource, + SuggestionStatus, + TopologyChange, + TopologyDiff, + TopologySnapshot, + TopologySuggestion, +) +from .store import Store + + +def _now() -> datetime: + return datetime.now(UTC) + + +def _iso(value: datetime | None) -> str | None: + return value.isoformat() if value else None + + +def _json(value: object) -> str: + return json.dumps(value, separators=(",", ":"), sort_keys=True) + + +def _obj(value: object) -> dict[str, object]: + if isinstance(value, dict): + return value + if isinstance(value, str): + try: + parsed = json.loads(value) + return parsed if isinstance(parsed, dict) else {} + except json.JSONDecodeError: + return {} + return {} + + +def _list(value: object) -> list[str]: + if isinstance(value, list): + return [str(item) for item in value] + if isinstance(value, str): + try: + parsed = json.loads(value) + return [str(item) for item in parsed] if isinstance(parsed, list) else [] + except json.JSONDecodeError: + return [] + return [] + + +_CONFIDENCE_RANK = { + DiscoveryConfidence.LOW: 1, + DiscoveryConfidence.MEDIUM: 2, + DiscoveryConfidence.HIGH: 3, + DiscoveryConfidence.CONFIRMED: 4, +} +_CONFIDENCE_RANK_INV = {rank: confidence.value for confidence, rank in _CONFIDENCE_RANK.items()} + + +def _higher_confidence(*values: DiscoveryConfidence) -> DiscoveryConfidence: + return max(values, key=lambda value: _CONFIDENCE_RANK[value]) + + +class DiscoveryStore: + """Durable discovery observations and review state. + + The existing Store remains the owner of canonical services and probe state. + This companion store deliberately keeps observations and suggestions separate + so a discovery run cannot mutate health topology unless reconciliation or an + explicit suggestion action asks it to do so. + """ + + def __init__(self, store: Store) -> None: + self.store = store + self._init_schema() + self.ensure_baseline_snapshot() + + def _connect(self) -> sqlite3.Connection: + return self.store._connect() + + def _init_schema(self) -> None: + with self._connect() as db: + db.executescript( + """ + CREATE TABLE IF NOT EXISTS discovery_runs ( + id TEXT PRIMARY KEY, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + completed_at TEXT, + source_counts TEXT NOT NULL DEFAULT '{}', + diff TEXT NOT NULL DEFAULT '{}', + error TEXT + ); + CREATE INDEX IF NOT EXISTS idx_discovery_runs_status + ON discovery_runs(status, started_at DESC); + + CREATE TABLE IF NOT EXISTS discovery_sources ( + id TEXT PRIMARY KEY, + source_type TEXT NOT NULL, + source_id TEXT NOT NULL, + source_name TEXT, + first_seen_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, + lifecycle TEXT NOT NULL DEFAULT 'active', + managed INTEGER NOT NULL DEFAULT 1, + metadata TEXT NOT NULL DEFAULT '{}', + UNIQUE(source_type, source_id) + ); + CREATE INDEX IF NOT EXISTS idx_discovery_sources_identity + ON discovery_sources(source_type, source_id); + CREATE INDEX IF NOT EXISTS idx_discovery_sources_last_seen + ON discovery_sources(last_seen_at DESC); + + CREATE TABLE IF NOT EXISTS service_sources ( + service_id TEXT NOT NULL, + source_id TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'observed', + PRIMARY KEY(service_id, source_id), + FOREIGN KEY(service_id) REFERENCES services(id) ON DELETE CASCADE, + FOREIGN KEY(source_id) REFERENCES discovery_sources(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_service_sources_source + ON service_sources(source_id, service_id); + + CREATE TABLE IF NOT EXISTS discovery_observations ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + source_id TEXT NOT NULL, + source_type TEXT NOT NULL, + source_name TEXT NOT NULL, + lifecycle TEXT NOT NULL, + change_type TEXT NOT NULL, + observed_at TEXT NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}', + FOREIGN KEY(run_id) REFERENCES discovery_runs(id) ON DELETE CASCADE, + FOREIGN KEY(source_id) REFERENCES discovery_sources(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_discovery_observations_run + ON discovery_observations(run_id, observed_at DESC); + CREATE INDEX IF NOT EXISTS idx_discovery_observations_source + ON discovery_observations(source_id, observed_at DESC); + + CREATE TABLE IF NOT EXISTS relation_evidence ( + id TEXT PRIMARY KEY, + source_service_id TEXT NOT NULL, + target_service_id TEXT, + target_ref TEXT, + relation_type TEXT NOT NULL DEFAULT 'depends_on', + evidence_type TEXT NOT NULL, + confidence TEXT NOT NULL, + explanation TEXT NOT NULL, + source_ref TEXT, + locked INTEGER NOT NULL DEFAULT 0, + active INTEGER NOT NULL DEFAULT 1, + first_seen_at TEXT NOT NULL, + last_seen_at TEXT NOT NULL, + UNIQUE(source_service_id, target_service_id, target_ref, relation_type, evidence_type), + FOREIGN KEY(source_service_id) REFERENCES services(id) ON DELETE CASCADE, + FOREIGN KEY(target_service_id) REFERENCES services(id) ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS idx_relation_evidence_source_target + ON relation_evidence(source_service_id, target_service_id); + CREATE INDEX IF NOT EXISTS idx_relation_evidence_confidence + ON relation_evidence(confidence, active); + + CREATE TABLE IF NOT EXISTS topology_suggestions ( + id TEXT PRIMARY KEY, + run_id TEXT NOT NULL, + suggestion_type TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'open', + fingerprint TEXT NOT NULL UNIQUE, + source_ref TEXT, + source_service_id TEXT, + target_ref TEXT, + target_service_id TEXT, + proposed_name TEXT, + proposed_kind TEXT, + proposed_url TEXT, + proposed_host TEXT, + proposed_port INTEGER, + proposed_category TEXT NOT NULL DEFAULT 'unknown', + proposed_group TEXT, + relation_type TEXT, + confidence TEXT NOT NULL, + evidence TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY(run_id) REFERENCES discovery_runs(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_topology_suggestions_status + ON topology_suggestions(status, updated_at DESC); + CREATE INDEX IF NOT EXISTS idx_topology_suggestions_source + ON topology_suggestions(source_ref, target_ref); + + CREATE TABLE IF NOT EXISTS topology_overrides ( + target_type TEXT NOT NULL, + target_id TEXT NOT NULL, + field_name TEXT NOT NULL, + value TEXT NOT NULL, + locked INTEGER NOT NULL DEFAULT 1, + updated_at TEXT NOT NULL, + PRIMARY KEY(target_type, target_id, field_name) + ); + CREATE INDEX IF NOT EXISTS idx_topology_overrides_target + ON topology_overrides(target_type, target_id); + + CREATE TABLE IF NOT EXISTS topology_change_events ( + id TEXT PRIMARY KEY, + event_type TEXT NOT NULL, + service_id TEXT, + source_service_id TEXT, + target_service_id TEXT, + detail TEXT NOT NULL, + created_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_topology_change_events_time + ON topology_change_events(created_at DESC); + + CREATE TABLE IF NOT EXISTS topology_snapshots ( + id TEXT PRIMARY KEY, + run_id TEXT, + created_at TEXT NOT NULL, + revision TEXT NOT NULL UNIQUE, + service_count INTEGER NOT NULL, + relationship_count INTEGER NOT NULL, + summary TEXT NOT NULL DEFAULT '{}', + FOREIGN KEY(run_id) REFERENCES discovery_runs(id) ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS idx_topology_snapshots_time + ON topology_snapshots(created_at DESC); + CREATE TABLE IF NOT EXISTS topology_snapshot_services ( + snapshot_id TEXT NOT NULL, + service_id TEXT NOT NULL, + name TEXT NOT NULL, + fingerprint TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY(snapshot_id, service_id), + FOREIGN KEY(snapshot_id) REFERENCES topology_snapshots(id) ON DELETE CASCADE + ); + CREATE TABLE IF NOT EXISTS topology_snapshot_relations ( + snapshot_id TEXT NOT NULL, + relation_id TEXT NOT NULL, + source_service_id TEXT NOT NULL, + target_service_id TEXT, + relation_type TEXT NOT NULL, + fingerprint TEXT NOT NULL, + payload TEXT NOT NULL, + PRIMARY KEY(snapshot_id, relation_id), + FOREIGN KEY(snapshot_id) REFERENCES topology_snapshots(id) ON DELETE CASCADE + ); + """ + ) + + @staticmethod + def _diff(value: object) -> DiscoveryDiff: + data = _obj(value) + return DiscoveryDiff(**{key: int(data.get(key, 0) or 0) for key in DiscoveryDiff.model_fields}) + + @classmethod + def _run(cls, row: sqlite3.Row | None) -> DiscoveryRun | None: + if row is None: + return None + return DiscoveryRun( + id=row["id"], + status=DiscoveryRunStatus(row["status"]), + started_at=datetime.fromisoformat(row["started_at"]), + completed_at=datetime.fromisoformat(row["completed_at"]) if row["completed_at"] else None, + source_counts={str(k): int(v) for k, v in _obj(row["source_counts"]).items()}, + diff=cls._diff(row["diff"]), + error=row["error"], + ) + + @classmethod + def _source(cls, row: sqlite3.Row) -> ServiceSource: + return ServiceSource( + id=row["id"], + source_type=row["source_type"], + source_id=row["source_id"], + source_name=row["source_name"], + first_seen_at=datetime.fromisoformat(row["first_seen_at"]), + last_seen_at=datetime.fromisoformat(row["last_seen_at"]), + lifecycle=DiscoveryLifecycle(row["lifecycle"]), + managed=bool(row["managed"]), + metadata=_obj(row["metadata"]), + ) + + @classmethod + def _suggestion(cls, row: sqlite3.Row) -> TopologySuggestion: + return TopologySuggestion( + id=row["id"], + run_id=row["run_id"], + suggestion_type=row["suggestion_type"], + status=SuggestionStatus(row["status"]), + source_ref=row["source_ref"], + source_service_id=row["source_service_id"], + target_ref=row["target_ref"], + target_service_id=row["target_service_id"], + proposed_name=row["proposed_name"], + proposed_kind=row["proposed_kind"], + proposed_url=row["proposed_url"], + proposed_host=row["proposed_host"], + proposed_port=row["proposed_port"], + proposed_category=row["proposed_category"], + proposed_group=row["proposed_group"], + relation_type=row["relation_type"], + confidence=DiscoveryConfidence(row["confidence"]), + evidence=_list(row["evidence"]), + created_at=datetime.fromisoformat(row["created_at"]), + updated_at=datetime.fromisoformat(row["updated_at"]), + ) + + @classmethod + def _evidence(cls, row: sqlite3.Row) -> RelationEvidence: + return RelationEvidence( + id=row["id"], + source_service_id=row["source_service_id"], + target_service_id=row["target_service_id"], + target_ref=row["target_ref"], + relation_type=RelationType(row["relation_type"]), + evidence_type=row["evidence_type"], + confidence=DiscoveryConfidence(row["confidence"]), + explanation=row["explanation"], + source_ref=row["source_ref"], + locked=bool(row["locked"]), + active=bool(row["active"]), + first_seen_at=datetime.fromisoformat(row["first_seen_at"]), + last_seen_at=datetime.fromisoformat(row["last_seen_at"]), + ) + + def start_run(self) -> DiscoveryRun: + run = DiscoveryRun(id=f"disc-{uuid4().hex[:12]}", status=DiscoveryRunStatus.DISCOVERING, started_at=_now()) + with self._connect() as db: + # A process restart or an unexpected reconcile exception can leave + # a durable run in an active state. Close that stale marker before + # creating the next run so status never reports a phantom active + # discovery forever. + db.execute( + "UPDATE discovery_runs SET status=?, completed_at=?, error=COALESCE(error, ?) WHERE status IN (?, ?)", + ( + DiscoveryRunStatus.FAILED.value, + run.started_at.isoformat(), + "Superseded by a newer discovery run", + DiscoveryRunStatus.DISCOVERING.value, + DiscoveryRunStatus.RECONCILING.value, + ), + ) + db.execute( + "INSERT INTO discovery_runs (id, status, started_at) VALUES (?, ?, ?)", + (run.id, run.status.value, run.started_at.isoformat()), + ) + return run + + def finish_run( + self, + run_id: str, + *, + status: DiscoveryRunStatus, + source_counts: dict[str, int], + diff: DiscoveryDiff, + error: str | None = None, + ) -> DiscoveryRun: + completed = _now() + with self._connect() as db: + db.execute( + "UPDATE discovery_runs SET status=?, completed_at=?, source_counts=?, diff=?, error=? WHERE id=?", + (status.value, completed.isoformat(), _json(source_counts), _json(diff.model_dump()), error, run_id), + ) + return self.get_run(run_id) or DiscoveryRun( + id=run_id, + status=status, + started_at=completed, + completed_at=completed, + source_counts=source_counts, + diff=diff, + error=error, + ) + + def get_run(self, run_id: str) -> DiscoveryRun | None: + with self._connect() as db: + row = db.execute("SELECT * FROM discovery_runs WHERE id = ?", (run_id,)).fetchone() + return self._run(row) + + def last_completed(self) -> DiscoveryRun | None: + with self._connect() as db: + row = db.execute( + "SELECT * FROM discovery_runs WHERE status = 'completed' ORDER BY completed_at DESC LIMIT 1" + ).fetchone() + return self._run(row) + + def status(self) -> tuple[DiscoveryRun | None, DiscoveryRun | None]: + with self._connect() as db: + current_row = db.execute( + "SELECT * FROM discovery_runs WHERE status IN ('discovering', 'reconciling') ORDER BY started_at DESC LIMIT 1" + ).fetchone() + return self._run(current_row), self.last_completed() + + @staticmethod + def _snapshot_row(row: sqlite3.Row | None) -> TopologySnapshot | None: + if row is None: + return None + return TopologySnapshot( + id=row["id"], + run_id=row["run_id"], + created_at=datetime.fromisoformat(row["created_at"]), + revision=row["revision"], + service_count=int(row["service_count"]), + relationship_count=int(row["relationship_count"]), + summary={str(key): int(value) for key, value in _obj(row["summary"]).items()}, + ) + + def ensure_baseline_snapshot(self) -> TopologySnapshot: + """Create one migration baseline without manufacturing repeated no-op snapshots.""" + + with self._connect() as db: + existing = db.execute("SELECT * FROM topology_snapshots ORDER BY created_at DESC LIMIT 1").fetchone() + return self._snapshot_row(existing) if existing else self.capture_snapshot(run_id=None) + + def capture_snapshot(self, *, run_id: str | None) -> TopologySnapshot: + """Persist a compact canonical topology capture when its revision changes.""" + + services = sorted(self.store.list_services(), key=lambda item: item.id) + service_rows: list[tuple[str, str, str, dict[str, object]]] = [] + relation_rows: list[tuple[str, str, str | None, str, str, dict[str, object]]] = [] + for service in services: + payload: dict[str, object] = { + "id": service.id, + "name": service.name, + "kind": service.kind.value, + "source_ref": service.source_ref, + "application": service.application, + "domain": service.domain, + "component_role": service.component_role.value, + "category": service.category.value, + "criticality": service.criticality.value, + "visibility": service.visibility.value, + "lifecycle": service.lifecycle.value, + "monitoring_state": service.monitoring_state.value, + "depends_on": sorted(service.depends_on), + } + service_rows.append((service.id, service.name, sha256(_json(payload).encode()).hexdigest(), payload)) + for target_id in sorted(service.depends_on): + relation_payload: dict[str, object] = { + "source_service_id": service.id, + "target_service_id": target_id, + "relation_type": RelationType.DEPENDS_ON.value, + } + relation_id = sha256(_json(relation_payload).encode()).hexdigest()[:24] + relation_rows.append( + ( + relation_id, + service.id, + target_id, + RelationType.DEPENDS_ON.value, + sha256(_json(relation_payload).encode()).hexdigest(), + relation_payload, + ) + ) + revision_payload = {"services": [(item[0], item[2]) for item in service_rows], "relations": [(item[0], item[4]) for item in relation_rows]} + revision = sha256(_json(revision_payload).encode()).hexdigest() + with self._connect() as db: + existing = db.execute("SELECT * FROM topology_snapshots WHERE revision=?", (revision,)).fetchone() + if existing: + return self._snapshot_row(existing) # type: ignore[return-value] + created_at = _now() + summary = { + "visible_services": sum(1 for item in services if item.visibility.value == "visible"), + "active_services": sum(1 for item in services if item.lifecycle.value == "active"), + } + snapshot = TopologySnapshot( + id=f"topo-{uuid4().hex[:14]}", + run_id=run_id, + created_at=created_at, + revision=revision, + service_count=len(service_rows), + relationship_count=len(relation_rows), + summary=summary, + ) + db.execute( + "INSERT INTO topology_snapshots VALUES (?, ?, ?, ?, ?, ?, ?)", + (snapshot.id, snapshot.run_id, snapshot.created_at.isoformat(), snapshot.revision, snapshot.service_count, snapshot.relationship_count, _json(snapshot.summary)), + ) + db.executemany( + "INSERT INTO topology_snapshot_services VALUES (?, ?, ?, ?, ?)", + [(snapshot.id, service_id, name, fingerprint, _json(payload)) for service_id, name, fingerprint, payload in service_rows], + ) + db.executemany( + "INSERT INTO topology_snapshot_relations VALUES (?, ?, ?, ?, ?, ?, ?)", + [(snapshot.id, relation_id, source_id, target_id, relation_type, fingerprint, _json(payload)) for relation_id, source_id, target_id, relation_type, fingerprint, payload in relation_rows], + ) + self.record_change("topology_snapshot", f"Topology snapshot captured ({snapshot.service_count} services)") + return snapshot + + def list_snapshots(self, *, limit: int = 50) -> list[TopologySnapshot]: + with self._connect() as db: + rows = db.execute("SELECT * FROM topology_snapshots ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall() + return [self._snapshot_row(row) for row in rows if self._snapshot_row(row) is not None] # type: ignore[list-item] + + def topology_diff(self, *, after_snapshot_id: str, before_snapshot_id: str | None = None) -> TopologyDiff: + with self._connect() as db: + after = db.execute("SELECT * FROM topology_snapshots WHERE id=?", (after_snapshot_id,)).fetchone() + if after is None: + raise ValueError("Topology snapshot not found") + before = ( + db.execute("SELECT * FROM topology_snapshots WHERE id=?", (before_snapshot_id,)).fetchone() + if before_snapshot_id + else db.execute("SELECT * FROM topology_snapshots WHERE created_at < ? ORDER BY created_at DESC LIMIT 1", (after["created_at"],)).fetchone() + ) + after_services = { + row["service_id"]: row + for row in db.execute("SELECT * FROM topology_snapshot_services WHERE snapshot_id=?", (after_snapshot_id,)).fetchall() + } + before_services = ( + {row["service_id"]: row for row in db.execute("SELECT * FROM topology_snapshot_services WHERE snapshot_id=?", (before["id"],)).fetchall()} + if before else {} + ) + after_relations = { + row["relation_id"]: row + for row in db.execute("SELECT * FROM topology_snapshot_relations WHERE snapshot_id=?", (after_snapshot_id,)).fetchall() + } + before_relations = ( + {row["relation_id"]: row for row in db.execute("SELECT * FROM topology_snapshot_relations WHERE snapshot_id=?", (before["id"],)).fetchall()} + if before else {} + ) + + added: list[TopologyChange] = [] + removed: list[TopologyChange] = [] + changed: list[TopologyChange] = [] + for item_id, row in after_services.items(): + payload = _obj(row["payload"]) + if item_id not in before_services: + added.append(TopologyChange(change_type="added", entity_type="service", entity_id=item_id, name=row["name"], after=payload)) + elif row["fingerprint"] != before_services[item_id]["fingerprint"]: + changed.append(TopologyChange(change_type="changed", entity_type="service", entity_id=item_id, name=row["name"], before=_obj(before_services[item_id]["payload"]), after=payload)) + for item_id, row in before_services.items(): + if item_id not in after_services: + removed.append(TopologyChange(change_type="removed", entity_type="service", entity_id=item_id, name=row["name"], before=_obj(row["payload"]))) + for item_id, row in after_relations.items(): + name = f"{row['source_service_id']} → {row['target_service_id'] or 'unknown'}" + if item_id not in before_relations: + added.append(TopologyChange(change_type="added", entity_type="relationship", entity_id=item_id, name=name, after=_obj(row["payload"]))) + elif row["fingerprint"] != before_relations[item_id]["fingerprint"]: + changed.append(TopologyChange(change_type="changed", entity_type="relationship", entity_id=item_id, name=name, before=_obj(before_relations[item_id]["payload"]), after=_obj(row["payload"]))) + for item_id, row in before_relations.items(): + if item_id not in after_relations: + removed.append(TopologyChange(change_type="removed", entity_type="relationship", entity_id=item_id, name=f"{row['source_service_id']} → {row['target_service_id'] or 'unknown'}", before=_obj(row["payload"]))) + return TopologyDiff( + before_snapshot_id=before["id"] if before else None, + after_snapshot_id=after_snapshot_id, + added=added, + removed=removed, + changed=changed, + summary={"added": len(added), "removed": len(removed), "changed": len(changed)}, + ) + + def save_source( + self, + *, + source_type: str, + source_id: str, + source_name: str | None, + lifecycle: DiscoveryLifecycle, + metadata: dict[str, object], + managed: bool = True, + observed_at: datetime | None = None, + ) -> ServiceSource: + at = observed_at or _now() + with self._connect() as db: + existing = db.execute( + "SELECT * FROM discovery_sources WHERE source_type=? AND source_id=?", + (source_type, source_id), + ).fetchone() + source_pk = existing["id"] if existing else f"src-{uuid4().hex[:14]}" + first_seen = existing["first_seen_at"] if existing else at.isoformat() + db.execute( + """ + INSERT INTO discovery_sources + (id, source_type, source_id, source_name, first_seen_at, last_seen_at, lifecycle, managed, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(source_type, source_id) DO UPDATE SET + source_name=excluded.source_name, last_seen_at=excluded.last_seen_at, + lifecycle=excluded.lifecycle, managed=excluded.managed, metadata=excluded.metadata + """, + (source_pk, source_type, source_id, source_name, first_seen, at.isoformat(), lifecycle.value, int(managed), _json(metadata)), + ) + row = db.execute("SELECT * FROM discovery_sources WHERE id=?", (source_pk,)).fetchone() + return self._source(row) + + def attach_source(self, service_id: str, source_id: str, role: str = "observed") -> None: + with self._connect() as db: + db.execute( + "INSERT OR IGNORE INTO service_sources (service_id, source_id, role) VALUES (?, ?, ?)", + (service_id, source_id, role), + ) + + def get_source(self, source_type: str, source_id: str) -> ServiceSource | None: + with self._connect() as db: + row = db.execute( + "SELECT * FROM discovery_sources WHERE source_type=? AND source_id=?", + (source_type, source_id), + ).fetchone() + return self._source(row) if row else None + + def save_observation( + self, + *, + run_id: str, + source: ServiceSource, + source_name: str, + lifecycle: DiscoveryLifecycle, + change_type: str, + metadata: dict[str, object], + observed_at: datetime | None = None, + ) -> DiscoveryObservation: + at = observed_at or _now() + observation = DiscoveryObservation( + id=f"obs-{uuid4().hex[:14]}", + run_id=run_id, + source_id=source.id, + source_type=source.source_type, + source_name=source_name, + lifecycle=lifecycle, + change_type=change_type, + observed_at=at, + metadata=metadata, + ) + with self._connect() as db: + db.execute( + "INSERT INTO discovery_observations VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + (observation.id, run_id, source.id, source.source_type, source_name, lifecycle.value, change_type, at.isoformat(), _json(metadata)), + ) + return observation + + def save_suggestion(self, suggestion: TopologySuggestion, fingerprint: str) -> TopologySuggestion: + with self._connect() as db: + existing = db.execute( + "SELECT evidence, confidence FROM topology_suggestions WHERE fingerprint=?", (fingerprint,) + ).fetchone() + merged_evidence = list(dict.fromkeys([ + *(_list(existing["evidence"]) if existing else []), + *suggestion.evidence, + ])) + confidence = _higher_confidence( + suggestion.confidence, + DiscoveryConfidence(existing["confidence"]) if existing else suggestion.confidence, + ) + db.execute( + """ + INSERT INTO topology_suggestions + (id, run_id, suggestion_type, status, fingerprint, source_ref, source_service_id, target_ref, + target_service_id, proposed_name, proposed_kind, proposed_url, proposed_host, proposed_port, + proposed_category, proposed_group, relation_type, confidence, evidence, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(fingerprint) DO UPDATE SET + run_id=excluded.run_id, + source_ref=excluded.source_ref, + source_service_id=excluded.source_service_id, + target_ref=excluded.target_ref, + target_service_id=excluded.target_service_id, + proposed_name=excluded.proposed_name, + proposed_kind=excluded.proposed_kind, + proposed_url=excluded.proposed_url, + proposed_host=excluded.proposed_host, + proposed_port=excluded.proposed_port, + proposed_category=excluded.proposed_category, + proposed_group=excluded.proposed_group, + relation_type=excluded.relation_type, + confidence=excluded.confidence, + evidence=excluded.evidence, + updated_at=excluded.updated_at + """, + ( + suggestion.id, + suggestion.run_id, + suggestion.suggestion_type, + suggestion.status.value, + fingerprint, + suggestion.source_ref, + suggestion.source_service_id, + suggestion.target_ref, + suggestion.target_service_id, + suggestion.proposed_name, + suggestion.proposed_kind.value if suggestion.proposed_kind else None, + suggestion.proposed_url, + suggestion.proposed_host, + suggestion.proposed_port, + suggestion.proposed_category.value, + suggestion.proposed_group, + suggestion.relation_type.value if suggestion.relation_type else None, + confidence.value, + _json(merged_evidence), + suggestion.created_at.isoformat(), + suggestion.updated_at.isoformat(), + ), + ) + row = db.execute("SELECT * FROM topology_suggestions WHERE fingerprint=?", (fingerprint,)).fetchone() + return self._suggestion(row) + + def list_suggestions( + self, + status: SuggestionStatus | None = None, + limit: int = 500, + run_id: str | None = None, + ) -> list[TopologySuggestion]: + query = "SELECT * FROM topology_suggestions" + params: list[Any] = [] + filters: list[str] = [] + if status: + filters.append("status = ?") + params.append(status.value) + if run_id: + filters.append("run_id = ?") + params.append(run_id) + if filters: + query += " WHERE " + " AND ".join(filters) + query += " ORDER BY updated_at DESC LIMIT ?" + params.append(limit) + with self._connect() as db: + rows = db.execute(query, params).fetchall() + return [self._suggestion(row) for row in rows] + + def get_suggestion(self, suggestion_id: str) -> TopologySuggestion | None: + with self._connect() as db: + row = db.execute("SELECT * FROM topology_suggestions WHERE id=?", (suggestion_id,)).fetchone() + return self._suggestion(row) if row else None + + def set_suggestion_status(self, suggestion_id: str, status: SuggestionStatus) -> TopologySuggestion | None: + now = _now() + with self._connect() as db: + db.execute("UPDATE topology_suggestions SET status=?, updated_at=? WHERE id=?", (status.value, now.isoformat(), suggestion_id)) + row = db.execute("SELECT * FROM topology_suggestions WHERE id=?", (suggestion_id,)).fetchone() + return self._suggestion(row) if row else None + + def save_evidence(self, evidence: RelationEvidence) -> RelationEvidence: + with self._connect() as db: + existing = db.execute( + """ + SELECT * FROM relation_evidence + WHERE source_service_id=? AND target_service_id IS ? AND target_ref IS ? + AND relation_type=? AND evidence_type=? + """, + (evidence.source_service_id, evidence.target_service_id, evidence.target_ref, evidence.relation_type.value, evidence.evidence_type), + ).fetchone() + if existing: + previous = self._evidence(existing) + evidence = evidence.model_copy(update={ + "confidence": _higher_confidence(previous.confidence, evidence.confidence), + "explanation": "; ".join(dict.fromkeys(part for part in (previous.explanation, evidence.explanation) if part)), + "source_ref": evidence.source_ref or previous.source_ref, + "locked": previous.locked or evidence.locked, + "active": evidence.active, + "first_seen_at": min(previous.first_seen_at, evidence.first_seen_at), + }) + db.execute( + """ + INSERT INTO relation_evidence + (id, source_service_id, target_service_id, target_ref, relation_type, evidence_type, + confidence, explanation, source_ref, locked, active, first_seen_at, last_seen_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(source_service_id, target_service_id, target_ref, relation_type, evidence_type) DO UPDATE SET + target_service_id=excluded.target_service_id, confidence=excluded.confidence, + explanation=excluded.explanation, source_ref=excluded.source_ref, + locked=MAX(relation_evidence.locked, excluded.locked), active=excluded.active, + last_seen_at=excluded.last_seen_at + """, + (evidence.id, evidence.source_service_id, evidence.target_service_id, evidence.target_ref, evidence.relation_type.value, evidence.evidence_type, evidence.confidence.value, evidence.explanation, evidence.source_ref, int(evidence.locked), int(evidence.active), evidence.first_seen_at.isoformat(), evidence.last_seen_at.isoformat()), + ) + row = db.execute( + "SELECT * FROM relation_evidence WHERE id=?", + (evidence.id,), + ).fetchone() + if row is None: + row = db.execute( + "SELECT * FROM relation_evidence WHERE source_service_id=? AND target_service_id IS ? AND target_ref IS ? AND relation_type=? AND evidence_type=?", + (evidence.source_service_id, evidence.target_service_id, evidence.target_ref, evidence.relation_type.value, evidence.evidence_type), + ).fetchone() + return self._evidence(row) + + def list_logical_relations(self, service_id: str | None = None, active_only: bool = True) -> list[LogicalRelation]: + """Return one canonical graph edge while retaining every evidence row. + + `relation_evidence` is an append/merge audit record. Rendering every + row as an edge made a relation appear duplicated whenever independent + discovery sources agreed. The aggregation is deliberately query-time + so existing v1.3 evidence is preserved without a destructive rebuild. + """ + + grouped: dict[tuple[str, str | None, str | None, RelationType], list[RelationEvidence]] = {} + for evidence in self.list_evidence(service_id=service_id, active_only=active_only): + key = (evidence.source_service_id, evidence.target_service_id, evidence.target_ref, evidence.relation_type) + grouped.setdefault(key, []).append(evidence) + result: list[LogicalRelation] = [] + for (source_id, target_id, target_ref, relation_type), evidence_rows in grouped.items(): + confidence = _higher_confidence(*(item.confidence for item in evidence_rows)) + independent_sources = {f"{item.evidence_type}:{item.source_ref or item.explanation}" for item in evidence_rows} + if len(independent_sources) > 1 and confidence != DiscoveryConfidence.CONFIRMED: + confidence = DiscoveryConfidence(_CONFIDENCE_RANK_INV[_CONFIDENCE_RANK[confidence] + 1]) + first_seen = min(item.first_seen_at for item in evidence_rows) + last_seen = max(item.last_seen_at for item in evidence_rows) + digest = sha256("|".join((source_id, target_id or "", target_ref or "", relation_type.value)).encode("utf-8")).hexdigest()[:16] + result.append(LogicalRelation( + id=f"relation-{digest}", + source_service_id=source_id, + target_service_id=target_id, + target_ref=target_ref, + relation_type=relation_type, + confidence=confidence, + active=any(item.active for item in evidence_rows), + locked=any(item.locked for item in evidence_rows), + first_seen_at=first_seen, + last_seen_at=last_seen, + evidence=sorted(evidence_rows, key=lambda item: item.last_seen_at, reverse=True), + )) + return sorted(result, key=lambda item: item.last_seen_at, reverse=True) + + def list_evidence(self, service_id: str | None = None, active_only: bool = True) -> list[RelationEvidence]: + query = "SELECT * FROM relation_evidence WHERE 1=1" + params: list[Any] = [] + if service_id: + query += " AND (source_service_id=? OR target_service_id=?)" + params.extend([service_id, service_id]) + if active_only: + query += " AND active=1" + query += " ORDER BY last_seen_at DESC" + with self._connect() as db: + rows = db.execute(query, params).fetchall() + return [self._evidence(row) for row in rows] + + def get_evidence(self, evidence_id: str) -> RelationEvidence | None: + with self._connect() as db: + row = db.execute("SELECT * FROM relation_evidence WHERE id=?", (evidence_id,)).fetchone() + return self._evidence(row) if row else None + + def get_evidence_for_relation( + self, + source_service_id: str, + target_service_id: str | None, + target_ref: str | None, + relation_type: RelationType, + evidence_type: str = "discovery", + ) -> RelationEvidence | None: + with self._connect() as db: + row = db.execute( + """ + SELECT * FROM relation_evidence + WHERE source_service_id=? AND target_service_id IS ? AND target_ref IS ? + AND relation_type=? AND evidence_type=? + """, + (source_service_id, target_service_id, target_ref, relation_type.value, evidence_type), + ).fetchone() + return self._evidence(row) if row else None + + def set_evidence_state( + self, + evidence_id: str, + *, + active: bool | None = None, + locked: bool | None = None, + ) -> RelationEvidence | None: + current = self.get_evidence(evidence_id) + if current is None: + return None + with self._connect() as db: + db.execute( + "UPDATE relation_evidence SET active=?, locked=?, last_seen_at=? WHERE id=?", + (int(current.active if active is None else active), int(current.locked if locked is None else locked), _now().isoformat(), evidence_id), + ) + return self.get_evidence(evidence_id) + + def evidence_for_service(self, service_id: str) -> list[RelationEvidence]: + return self.list_evidence(service_id, active_only=True) + + def sources_for_service(self, service_id: str) -> list[ServiceSource]: + with self._connect() as db: + rows = db.execute( + "SELECT s.* FROM discovery_sources s JOIN service_sources ss ON ss.source_id=s.id WHERE ss.service_id=? ORDER BY s.source_type, s.source_name", + (service_id,), + ).fetchall() + return [self._source(row) for row in rows] + + def observations_for_run(self, run_id: str, limit: int = 1000) -> list[DiscoveryObservation]: + with self._connect() as db: + rows = db.execute( + "SELECT * FROM discovery_observations WHERE run_id=? ORDER BY source_name COLLATE NOCASE LIMIT ?", + (run_id, limit), + ).fetchall() + return [ + DiscoveryObservation( + id=row["id"], + run_id=row["run_id"], + source_id=row["source_id"], + source_type=row["source_type"], + source_name=row["source_name"], + lifecycle=DiscoveryLifecycle(row["lifecycle"]), + change_type=row["change_type"], + observed_at=datetime.fromisoformat(row["observed_at"]), + metadata=_obj(row["metadata"]), + ) + for row in rows + ] + + def candidates_for_run(self, run_id: str, limit: int = 1000) -> list[dict[str, object]]: + candidates: list[dict[str, object]] = [] + for observation in self.observations_for_run(run_id, limit): + candidate = observation.metadata.get("candidate") + if isinstance(candidate, dict): + candidates.append(candidate) + return candidates + + def set_run_status(self, run_id: str, status: DiscoveryRunStatus) -> None: + with self._connect() as db: + db.execute("UPDATE discovery_runs SET status=? WHERE id=?", (status.value, run_id)) + + def record_change( + self, + event_type: str, + detail: str, + *, + service_id: str | None = None, + source_service_id: str | None = None, + target_service_id: str | None = None, + ) -> None: + with self._connect() as db: + db.execute( + "INSERT INTO topology_change_events (id, event_type, service_id, source_service_id, target_service_id, detail, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + (f"change-{uuid4().hex[:14]}", event_type, service_id, source_service_id, target_service_id, detail, _now().isoformat()), + ) + + def save_override(self, target_type: str, target_id: str, field_name: str, value: object, locked: bool = True) -> None: + with self._connect() as db: + db.execute( + "INSERT INTO topology_overrides (target_type, target_id, field_name, value, locked, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(target_type, target_id, field_name) DO UPDATE SET value=excluded.value, locked=excluded.locked, updated_at=excluded.updated_at", + (target_type, target_id, field_name, _json(value), int(locked), _now().isoformat()), + ) + + def has_override(self, target_type: str, target_id: str, field_name: str) -> bool: + with self._connect() as db: + row = db.execute( + "SELECT 1 FROM topology_overrides WHERE target_type=? AND target_id=? AND field_name=? AND locked=1", + (target_type, target_id, field_name), + ).fetchone() + return row is not None + + def list_overrides(self, target_type: str | None = None, target_id: str | None = None) -> list[dict[str, object]]: + query = "SELECT target_type, target_id, field_name, value, locked, updated_at FROM topology_overrides WHERE 1=1" + params: list[Any] = [] + if target_type: + query += " AND target_type=?" + params.append(target_type) + if target_id: + query += " AND target_id=?" + params.append(target_id) + with self._connect() as db: + rows = db.execute(query, params).fetchall() + return [ + { + "target_type": row["target_type"], + "target_id": row["target_id"], + "field": row["field_name"], + "value": _obj(row["value"]) if row["value"].startswith("{") else row["value"], + "locked": bool(row["locked"]), + "updated_at": row["updated_at"], + } + for row in rows + ] diff --git a/backend/app/docker_discovery.py b/backend/app/docker_discovery.py new file mode 100644 index 0000000..6b7f9ec --- /dev/null +++ b/backend/app/docker_discovery.py @@ -0,0 +1,682 @@ +from __future__ import annotations + +import os +import re +import socket +import time +from dataclasses import dataclass +from urllib.parse import urlparse + +import docker +import httpx +from docker.errors import DockerException + +from .docker_provider import DockerProviderError, docker_client_for_inventory, docker_provider_mode +from .models import ( + DiscoveryCategory, + DiscoveryConfidence, + DiscoveryLifecycle, + DiscoveryVisibility, + DockerCandidate, + DockerDiscoveryResult, + DockerPort, + DockerSuggestion, + EvidenceScope, + ProbeEndpointCandidate, + ProbeEndpointSource, + ProbeReachability, + Service, + ServiceKind, +) + +ALLOWED_SCHEMES = {"unix", "tcp", "http", "https"} +_RELATION_ENV = re.compile(r"(URL|HOST|HOSTNAME|ADDRESS|ENDPOINT|DSN|DATABASE|REDIS|POSTGRES|MYSQL|MONGO|BROKER|UPSTREAM|S3|STORAGE)", re.IGNORECASE) +_SECRET_ENV = re.compile(r"(PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE[_-]?KEY|CREDENTIAL|AUTHORIZATION|BEARER)", re.IGNORECASE) +_SECRET_LABEL = re.compile( + r"(PASSWORD|PASSWD|SECRET|TOKEN|(?:API|ACCESS|PRIVATE|SIGNING|ENCRYPTION)[_-]?KEY|" + r"CREDENTIAL|AUTH(?:ORIZATION)?|BEARER|COOKIE)", + re.IGNORECASE, +) +_HTTP_CONTAINER_PORTS = { + 80, 443, 3000, 3001, 5000, 6060, 8000, 8080, 8081, 8181, 8188, + 8265, 8989, 9000, 9443, 9494, 11434, +} + + +@dataclass(frozen=True) +class AppOpsInfo: + app_id: str + category: str | None + criticality: str | None + health: str | None + display_name: str | None + discovery_confidence: float | None = None + policy_profile: str | None = None + app_type: str | None = None + reasons: tuple[str, ...] = () + + +@dataclass(frozen=True) +class AppOpsInventory: + by_container: dict[str, AppOpsInfo] + available: bool + detail: str = "" + + +@dataclass(frozen=True) +class DockerHealthObservation: + """The small, health-only subset needed by the recurring observer.""" + + container_id: str + name: str + stable_source_ref: str + state: str + health: str | None + healthcheck_present: bool + + +@dataclass(frozen=True) +class ProxyRoute: + domains: tuple[str, ...] + forward_host: str + forward_port: int + forward_scheme: str + + +def _normal_name(value: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-") + + +def stable_source_ref(name: str, labels: dict[str, str]) -> str: + project = labels.get("com.docker.compose.project", "").strip() + service = labels.get("com.docker.compose.service", "").strip() + if project and service: + return f"docker:compose:{_normal_name(project)}:{_normal_name(service)}" + return f"docker:name:{_normal_name(name)}" + + +def _legacy_source_ref(container_id: str, image_name: str) -> str: + return f"docker:{container_id[:12]}@{image_name}" + + +def _safe_labels(labels: dict[str, str]) -> dict[str, str]: + prefixes = ("servicemap.", "com.docker.compose.", "net.unraid.docker.", "org.opencontainers.image.") + return { + key: value + for key, value in labels.items() + if key.startswith(prefixes) and not _SECRET_LABEL.search(key) + } + + +def _classify(name: str, image: str, labels: dict[str, str], appops: AppOpsInfo | None = None) -> DiscoveryCategory: + explicit = labels.get("servicemap.category", "").strip().casefold() + if explicit in {item.value for item in DiscoveryCategory}: + return DiscoveryCategory(explicit) + # AppOps category is application metadata. It is helpful only when its + # discovery confidence is high enough; a low-confidence registry row must + # not override deterministic Docker or operator classification. + if appops and appops.category and (appops.discovery_confidence or 0) >= 0.75: + mapped = { + "database": DiscoveryCategory.DATABASE, + "database_admin": DiscoveryCategory.DATABASE, + "cache": DiscoveryCategory.CACHE, + "network": DiscoveryCategory.INFRASTRUCTURE, + "monitoring": DiscoveryCategory.MONITORING, + "media": DiscoveryCategory.APPLICATION, + "development": DiscoveryCategory.DEVELOPMENT, + "storage": DiscoveryCategory.STORAGE, + "infrastructure": DiscoveryCategory.INFRASTRUCTURE, + }.get(appops.category.casefold()) + if mapped: + return mapped + value = f"{name} {image}".casefold() + if any(token in value for token in ("postgres", "mariadb", "mysql", "mongo", "qdrant")): + return DiscoveryCategory.DATABASE + if "redis" in value: + return DiscoveryCategory.CACHE + if any(token in value for token in ("nginx", "traefik", "caddy", "proxy")): + return DiscoveryCategory.PROXY + if any(token in value for token in ("prometheus", "grafana", "zabbix", "netdata", "cadvisor", "loki", "alloy")): + return DiscoveryCategory.MONITORING + if any(token in value for token in ("worker", "agent", "runner", "backup", "scheduler", "node")): + return DiscoveryCategory.BACKGROUND_WORKER + if any(token in value for token in ("portainer", "dockdeck", "unraid", "watchtower", "appops", "gitea")): + return DiscoveryCategory.INFRASTRUCTURE + if any(token in value for token in ("dev", "forge", "lab", "builder", "runner")): + return DiscoveryCategory.DEVELOPMENT + return DiscoveryCategory.APPLICATION + + +def _visibility(name: str, state: str, category: DiscoveryCategory, labels: dict[str, str]) -> DiscoveryVisibility: + if labels.get("servicemap.ignore", "false").casefold() == "true": + return DiscoveryVisibility.IGNORED + if state != "running": + return DiscoveryVisibility.HIDDEN + if category == DiscoveryCategory.SYSTEM: + return DiscoveryVisibility.IGNORED + if labels.get("servicemap.visible", "true").casefold() == "false": + return DiscoveryVisibility.HIDDEN + return DiscoveryVisibility.VISIBLE + + +def _kind_and_endpoint( + name: str, + ports: list[DockerPort], + labels: dict[str, str], + probe_host: str | None = None, +) -> tuple[ServiceKind, str | None, str | None, int | None, list[str]]: + """Select only an endpoint ServiceMap can reach from its own network. + + Docker's embedded DNS is scoped to a container network. A name such as + ``postgres`` is useful discovery evidence but cannot become an active + probe merely because it exists in another project's network. + """ + probe_host = (probe_host or os.getenv("SERVICEMAP_PROBE_HOST") or "").strip() + explicit_kind = labels.get("servicemap.probe", "").casefold() + explicit_port = labels.get("servicemap.port", "") + tcp_ports = [item for item in ports if item.container_port.endswith("/tcp")] + + def container_port(item: DockerPort) -> int | None: + raw = item.container_port.split("/", 1)[0] + return int(raw) if raw.isdigit() else None + + explicit_value = int(explicit_port) if explicit_port.isdigit() else None + selected = next((item for item in tcp_ports if container_port(item) == explicit_value), None) + if selected is None: + published = [item for item in tcp_ports if item.host_port is not None] + # The mapped host port is arbitrary. Select using the container's + # listener port so 8080 -> 8070 remains an HTTP candidate. + selected = next((item for item in published if container_port(item) in _HTTP_CONTAINER_PORTS), None) + selected = selected or (published[0] if published else (tcp_ports[0] if tcp_ports else None)) + container_listener_port = container_port(selected) if selected else None + host_port = selected.host_port if selected else None + port = explicit_value or container_listener_port + evidence = ["container metadata"] + if explicit_kind in {"http", "tcp", "dns", "tls"}: + kind = ServiceKind(explicit_kind) + evidence.append("servicemap.probe label") + elif port in _HTTP_CONTAINER_PORTS: + kind = ServiceKind.HTTP + evidence.append("HTTP-like container port") + elif port: + kind = ServiceKind.TCP + evidence.append("published TCP port") + else: + return ServiceKind.TCP, None, name, None, evidence + if kind == ServiceKind.HTTP: + protocol = "https" if port in {443, 8443, 9443} else "http" + if host_port is not None and probe_host: + # HTTP services are represented by a URL in the canonical model; + # carrying the published port alongside it creates an invalid + # mixed endpoint payload during reconciliation. + return kind, f"{protocol}://{probe_host}:{host_port}", None, None, evidence + ["host-published endpoint"] + return kind, None, name, port, evidence + ["internal container endpoint candidate"] + if kind == ServiceKind.DNS: + return kind, None, name, None, evidence + if host_port is not None and probe_host: + return kind, None, probe_host, host_port, evidence + ["host-published endpoint"] + return kind, None, name, port, evidence + ["internal container endpoint candidate"] + + +def _preflight_candidate(candidate: ProbeEndpointCandidate) -> ProbeEndpointCandidate: + """Verify only host-published endpoints before allowing active probes. + + Discovery never probes Docker-internal DNS names and never scans paths: the + endpoint must already come from a Docker port mapping or explicit label. + """ + if candidate.source not in { + ProbeEndpointSource.PUBLISHED_PORT, + ProbeEndpointSource.EXPLICIT_LABEL, + }: + return candidate + parsed = urlparse(candidate.url) if candidate.url else None + hostname = parsed.hostname if parsed else candidate.host + port = parsed.port if parsed else candidate.port + if not hostname or not port: + return candidate.model_copy(update={"preflight_detail": "No host-published endpoint to preflight"}) + started = time.perf_counter() + try: + with socket.create_connection((hostname, port), timeout=0.75): + pass + elapsed = round((time.perf_counter() - started) * 1000, 1) + if candidate.kind != ServiceKind.HTTP or not candidate.url: + return candidate.model_copy(update={ + "reachability": ProbeReachability.VERIFIED, + "primary": True, + "preflight_detail": "TCP connect succeeded", + "preflight_latency_ms": elapsed, + }) + with httpx.Client(timeout=1.25, follow_redirects=False) as client: + response = client.request("HEAD", candidate.url) + if response.status_code == 405: + response = client.request("GET", candidate.url) + return candidate.model_copy(update={ + "reachability": ProbeReachability.VERIFIED, + "primary": True, + "preflight_detail": f"HTTP {response.status_code} responded", + "preflight_latency_ms": elapsed, + }) + except (OSError, httpx.HTTPError) as exc: + return candidate.model_copy(update={ + "reachability": ProbeReachability.UNVERIFIED, + "primary": False, + "preflight_detail": f"Preflight failed: {type(exc).__name__}", + }) + + +def _probe_candidates( + suggestion: DockerSuggestion | None, + *, + healthcheck_present: bool, +) -> list[ProbeEndpointCandidate]: + candidates: list[ProbeEndpointCandidate] = [] + if suggestion: + source = ProbeEndpointSource.EXPLICIT_LABEL if "servicemap.probe label" in suggestion.evidence else ProbeEndpointSource.PUBLISHED_PORT + reachability = ProbeReachability.UNVERIFIED if "host-published endpoint" in suggestion.evidence else ProbeReachability.INTERNAL_ONLY + candidates.append( + ProbeEndpointCandidate( + source=source if reachability == ProbeReachability.UNVERIFIED else ProbeEndpointSource.CONTAINER_INTERNAL, + kind=suggestion.kind, + url=suggestion.url, + host=suggestion.host, + port=suggestion.port, + reachability=reachability, + confidence=DiscoveryConfidence.MEDIUM if reachability == ProbeReachability.UNVERIFIED else DiscoveryConfidence.LOW, + evidence="; ".join(suggestion.evidence), + ) + ) + if healthcheck_present: + candidates.append( + ProbeEndpointCandidate( + source=ProbeEndpointSource.DOCKER_HEALTHCHECK, + kind=suggestion.kind if suggestion else ServiceKind.TCP, + reachability=ProbeReachability.VERIFIED, + confidence=DiscoveryConfidence.HIGH, + evidence="Docker healthcheck present (observed without a network probe)", + ) + ) + return candidates + + +def safe_environment_evidence(env: list[str]) -> list[str]: + evidence: list[str] = [] + for entry in env: + name, separator, value = entry.partition("=") + if not separator or not value or _SECRET_ENV.search(name) or not _RELATION_ENV.search(name): + continue + try: + parsed = urlparse(value if "://" in value else f"//{value}") + hostname = parsed.hostname + except ValueError: + # Docker environments frequently contain comma-separated host + # lists or malformed IPv6-like values. Discovery evidence is + # best-effort; one invalid value must not abort the complete scan. + continue + if not hostname or hostname.casefold() in {"localhost", "127.0.0.1", "0.0.0.0"}: + continue + try: + port = parsed.port + except ValueError: + # A parseable hostname with a non-numeric suffix is still useful + # relation evidence; omit only the unsafe port component. + port = None + suffix = f":{port}" if port else "" + evidence.append(f"{name} hostname={hostname}{suffix}") + return sorted(set(evidence)) + + +def _appops_inventory(appops_url: str | None) -> AppOpsInventory: + if not appops_url: + return AppOpsInventory({}, False, "AppOps URL is not configured") + base = appops_url.rstrip("/") + try: + mappings_response = httpx.get(f"{base}/api/inventory/mappings", timeout=5) + apps_response = httpx.get(f"{base}/api/inventory/apps", timeout=5) + mappings_response.raise_for_status() + apps_response.raise_for_status() + mappings = mappings_response.json() + apps = apps_response.json() + except (httpx.HTTPError, ValueError) as exc: + return AppOpsInventory({}, False, f"AppOps inventory unavailable: {type(exc).__name__}") + app_by_id = {str(item.get("id")): item for item in apps if isinstance(item, dict)} + result: dict[str, AppOpsInfo] = {} + for mapping in mappings if isinstance(mappings, list) else []: + if not isinstance(mapping, dict): + continue + container = str(mapping.get("containerName") or "") + app_id = str(mapping.get("appId") or "") + app = app_by_id.get(app_id, {}) + try: + confidence = float(app.get("discovery_confidence")) if app.get("discovery_confidence") is not None else None + except (TypeError, ValueError): + confidence = None + if container: + result[container.casefold()] = AppOpsInfo( + app_id=app_id, + category=str(app.get("category")) if app.get("category") else None, + criticality=str(app.get("criticality")) if app.get("criticality") else None, + health=str(app.get("health_status")) if app.get("health_status") else None, + display_name=str(app.get("display_name")) if app.get("display_name") else None, + discovery_confidence=max(0.0, min(confidence, 1.0)) if confidence is not None else None, + policy_profile=str(app.get("policy_profile")) if app.get("policy_profile") else None, + app_type=str(app.get("app_type")) if app.get("app_type") else None, + reasons=tuple(str(item) for item in (app.get("discovery_reasons") or [])[:3]), + ) + return AppOpsInventory(result, True, f"{len(result)} container mappings loaded") + + +def appops_inventory(appops_url: str | None) -> AppOpsInventory: + """Read AppOps health inventory without triggering Docker discovery.""" + + return _appops_inventory(appops_url) + + +def observe_container_health(docker_host: str) -> list[DockerHealthObservation]: + """Read bounded Docker runtime/health facts without discovery side effects. + + This deliberately avoids endpoint preflight, relationship inference, + suggestion persistence, reconciliation and snapshot capture. It is safe + to run on the observation cadence because it cannot rewrite topology or + operator-owned configuration. + """ + + parsed = urlparse(docker_host) + if parsed.scheme not in ALLOWED_SCHEMES: + raise ValueError("docker_host must use unix://, tcp://, http:// or https://") + client = None + try: + client = ( + docker.DockerClient(base_url=docker_host, timeout=5) + if docker_provider_mode() == "socket" + else docker_client_for_inventory(docker_host) + ) + client.ping() + observations: list[DockerHealthObservation] = [] + for container in client.containers.list(all=True): + attrs = container.attrs or {} + state = attrs.get("State", {}) or {} + config = attrs.get("Config", {}) or {} + raw_labels = {str(key): str(value) for key, value in (config.get("Labels", {}) or {}).items()} + name = str(getattr(container, "name", "unknown")).lstrip("/") + observations.append( + DockerHealthObservation( + container_id=container.id[:12], + name=name, + stable_source_ref=stable_source_ref(name, raw_labels), + state=str(state.get("Status", getattr(container, "status", "unknown"))), + health=( + str(state.get("Health", {}).get("Status")) + if isinstance(state.get("Health"), dict) and state.get("Health", {}).get("Status") is not None + else None + ), + healthcheck_present=bool(config.get("Healthcheck")), + ) + ) + return observations + except (DockerException, DockerProviderError) as exc: + raise RuntimeError(f"Docker observation unavailable: {type(exc).__name__}") from exc + finally: + close = getattr(client, "close", None) + if callable(close): + close() + + +def _appops_proxy_routes(appops_url: str | None) -> list[ProxyRoute]: + if not appops_url: + return [] + try: + payload = httpx.get(f"{appops_url.rstrip('/')}/api/apps/npm/proxy-hosts", timeout=5).json() + except (httpx.HTTPError, ValueError): + return [] + items = payload.get("items", []) if isinstance(payload, dict) else payload + routes: list[ProxyRoute] = [] + for item in items if isinstance(items, list) else []: + if not isinstance(item, dict) or item.get("enabled") is False: + continue + domains = tuple(sorted({str(domain).strip() for domain in item.get("domainNames", []) if str(domain).strip()})) + host = str(item.get("forwardHost") or "").strip() + try: + port = int(item.get("forwardPort")) + except (TypeError, ValueError): + continue + scheme = str(item.get("forwardScheme") or "http").casefold() + if not domains or not host or not 1 <= port <= 65535 or scheme not in {"http", "https"}: + continue + routes.append(ProxyRoute(domains=domains, forward_host=host, forward_port=port, forward_scheme=scheme)) + return routes + + +def _suggestions( + name: str, + networks: list[str], + ports: list[DockerPort], + labels: dict[str, str], + *, + image: str, + state: str, + appops: AppOpsInfo | None = None, + stable_ref: str, + probe_host: str | None = None, +) -> list[DockerSuggestion]: + if labels.get("servicemap.enabled", "true").casefold() == "false": + return [] + kind, url, host, port, evidence = _kind_and_endpoint(name, ports, labels, probe_host) + depends = [item.strip() for item in labels.get("servicemap.depends_on", "").split(",") if item.strip()] + compose_depends = labels.get("com.docker.compose.depends_on", "") + if compose_depends: + depends.extend(item.split(":", 1)[0].strip() for item in compose_depends.split(",") if item.strip()) + evidence.append("Compose depends_on metadata") + if depends: + evidence.append("explicit dependency metadata") + if appops: + confidence = f" ({appops.discovery_confidence:.2f})" if appops.discovery_confidence is not None else "" + evidence.append(f"AppOps application mapping{confidence}; application scope only") + evidence.extend(appops.reasons) + category = _classify(name, image, labels, appops) + return [ + DockerSuggestion( + name=labels.get("servicemap.name", appops.display_name if appops and appops.display_name else name), + kind=kind, + host=host, + port=port, + url=url, + group=labels.get("servicemap.group") or (f"Compose · {labels.get('com.docker.compose.project')}" if labels.get("com.docker.compose.project") else category.value.replace("_", " ").title()), + evidence=sorted(set(evidence)), + confidence="high" if labels.get("servicemap.probe") or labels.get("servicemap.name") or compose_depends or (appops and (appops.discovery_confidence or 0) >= 0.75) else "medium", + depends_on_names=sorted(set(depends)), + stable_source_ref=stable_ref, + category=category, + visibility=_visibility(name, state, category, labels), + ) + ] + + +def discover_containers( + docker_host: str, + existing_services: list[Service] | None = None, + *, + include_stopped: bool = True, + appops_url: str | None = None, + probe_host: str | None = None, +) -> DockerDiscoveryResult: + parsed = urlparse(docker_host) + if parsed.scheme not in ALLOWED_SCHEMES: + raise ValueError("docker_host must use unix://, tcp://, http:// or https://") + + # Keep the direct client import at this boundary so existing deployments + # and deterministic SDK-contract tests retain the socket behaviour. Broker + # mode is explicit and never falls back here. + client = docker.DockerClient(base_url=docker_host, timeout=5) if docker_provider_mode() == "socket" else docker_client_for_inventory(docker_host) + try: + client.ping() + containers = client.containers.list(all=include_stopped) + existing_services = existing_services or [] + appops_inventory = _appops_inventory(appops_url) + appops = appops_inventory.by_container + configured_probe_host = (probe_host or os.getenv("SERVICEMAP_PROBE_HOST") or "").strip() or None + proxy_routes = _appops_proxy_routes(appops_url) + candidates: list[DockerCandidate] = [] + discovered_refs: set[str] = set() + for container in containers: + attrs = container.attrs or {} + state = attrs.get("State", {}) or {} + config = attrs.get("Config", {}) or {} + network_settings = attrs.get("NetworkSettings", {}) or {} + raw_ports = network_settings.get("Ports", {}) or {} + ports: list[DockerPort] = [] + for container_port, bindings in raw_ports.items(): + if not bindings: + ports.append(DockerPort(container_port=container_port)) + continue + for binding in bindings: + raw_host_port = binding.get("HostPort") + host_port = str(raw_host_port) if raw_host_port is not None else "" + ports.append( + DockerPort( + container_port=container_port, + host_ip=binding.get("HostIp") or None, + host_port=int(host_port) if host_port.isdigit() else None, + ) + ) + + image_tags = getattr(container.image, "tags", []) or [] + image_name = image_tags[0] if image_tags else str(config.get("Image", "unknown")) + name = str(getattr(container, "name", "unknown")).lstrip("/") + health = state.get("Health", {}).get("Status") if isinstance(state.get("Health"), dict) else None + state_name = str(state.get("Status", getattr(container, "status", "unknown"))) + networks = sorted((network_settings.get("Networks", {}) or {}).keys()) + raw_labels = {str(key): str(value) for key, value in (config.get("Labels", {}) or {}).items()} + labels = _safe_labels(raw_labels) + stable_ref = stable_source_ref(name, raw_labels) + legacy_ref = _legacy_source_ref(container.id, image_name) + discovered_refs.add(stable_ref) + matching_source = next( + ( + service + for service in existing_services + if service.source_ref in {stable_ref, legacy_ref} + or (service.source_ref and service.source_ref.removeprefix("docker:").startswith(container.id[:12])) + ), + None, + ) + matching_name = next((service for service in existing_services if service.name.casefold() == name.casefold()), None) + appops_info = appops.get(name.casefold()) + category = _classify(name, image_name, raw_labels, appops_info) + visibility = _visibility(name, state_name, category, raw_labels) + if matching_source is not None: + import_state = "already imported" if matching_source.source_ref in {stable_ref, legacy_ref} else "changed" + elif matching_name is not None: + import_state = "already imported" + else: + import_state = "new" + suggestions = _suggestions( + name, + networks, + ports, + raw_labels, + image=image_name, + state=state_name, + appops=appops_info, + stable_ref=stable_ref, + probe_host=configured_probe_host, + ) + for suggestion in suggestions: + suggestion.source_type = "docker" + suggestion.source_ref = legacy_ref + env_evidence = safe_environment_evidence([str(item) for item in (config.get("Env") or [])]) + probe_candidates = _probe_candidates( + suggestions[0] if suggestions else None, + healthcheck_present=bool(config.get("Healthcheck")), + ) + if state_name == "running": + probe_candidates = [_preflight_candidate(item) for item in probe_candidates] + candidates.append( + DockerCandidate( + container_id=container.id[:12], + name=name, + image=image_name, + state=state_name, + health=health, + networks=networks, + ports=ports, + labels=labels, + import_state=import_state, + suggestions=suggestions, + stable_source_ref=stable_ref, + compose_project=raw_labels.get("com.docker.compose.project"), + compose_service=raw_labels.get("com.docker.compose.service"), + category=category, + visibility=visibility, + lifecycle=DiscoveryLifecycle.ACTIVE if state_name == "running" else DiscoveryLifecycle.STALE, + healthcheck_present=bool(config.get("Healthcheck")), + environment_evidence=env_evidence, + proxy_route_evidence=[], + appops_app_id=appops_info.app_id if appops_info else None, + appops_category=appops_info.category if appops_info else None, + appops_criticality=appops_info.criticality if appops_info else None, + appops_health=appops_info.health if appops_info else None, + appops_scope=EvidenceScope.APPLICATION if appops_info else None, + appops_discovery_confidence=appops_info.discovery_confidence if appops_info else None, + appops_policy_profile=appops_info.policy_profile if appops_info else None, + appops_app_type=appops_info.app_type if appops_info else None, + probe_candidates=probe_candidates, + ) + ) + + for candidate in candidates: + candidate.dependency_suggestions = sorted( + other.name + for other in candidates + if other.name != candidate.name and set(candidate.networks).intersection(other.networks) + ) + + # Reverse-proxy metadata is useful evidence, but it is never turned + # into a health dependency automatically. Persist only domains and the + # sanitized upstream tuple; AppOps metadata such as certificate or + # Let's Encrypt fields is intentionally discarded. + by_name = {candidate.name.casefold(): candidate for candidate in candidates} + by_published_port = { + port.host_port: candidate + for candidate in candidates + for port in candidate.ports + if port.host_port is not None + } + for route in proxy_routes: + target = by_name.get(route.forward_host.casefold()) or by_published_port.get(route.forward_port) + if target: + target.proxy_route_evidence.append( + f"Reverse proxy route: {', '.join(route.domains)} -> {route.forward_scheme}://{route.forward_host}:{route.forward_port}" + ) + + for service in existing_services: + if service.source_type != "docker" or not service.source_ref: + continue + if service.source_ref not in discovered_refs and service.lifecycle not in {DiscoveryLifecycle.RETIRED, DiscoveryLifecycle.IGNORED}: + candidates.append( + DockerCandidate( + container_id=service.source_ref.removeprefix("docker:")[:12], + name=service.name, + image=service.container_image or "not found", + state="missing", + health=None, + networks=[], + ports=[], + import_state="missing", + stable_source_ref=service.source_ref, + category=service.category, + visibility=service.visibility, + lifecycle=DiscoveryLifecycle.MISSING, + ) + ) + candidates.sort(key=lambda item: item.name.casefold()) + return DockerDiscoveryResult( + docker_host=docker_host, + candidates=candidates, + proxy_routes=len(proxy_routes), + appops_available=appops_inventory.available, + appops_detail=appops_inventory.detail, + ) + except (DockerException, DockerProviderError) as exc: + raise ValueError(f"Docker discovery failed: {exc}") from exc + finally: + client.close() diff --git a/backend/app/docker_provider.py b/backend/app/docker_provider.py new file mode 100644 index 0000000..0087250 --- /dev/null +++ b/backend/app/docker_provider.py @@ -0,0 +1,334 @@ +"""Docker inventory providers with an explicit security boundary. + +The legacy Unix-socket provider is retained for compatibility. The broker +provider is opt-in and deliberately never falls back to the socket: a failed +broker configuration must be visible to the operator rather than silently +re-expanding ServiceMap's Docker privilege boundary. +""" + +from __future__ import annotations + +import json +import os +import re +from dataclasses import dataclass +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit, urlunsplit + +import docker +import httpx + + +class DockerProviderError(ValueError): + pass + + +_BROKER_CONNECT_TIMEOUT_SECONDS = 2.0 +_BROKER_OPERATION_TIMEOUT_SECONDS = 5.0 +_BROKER_MAX_RESPONSE_BYTES = 8 * 1024 * 1024 +_BROKER_MAX_CONTAINER_ITEMS = 10_000 +_BROKER_PATH = "/v1/containers" +_BROKER_BEARER_TOKEN = re.compile(r"[^\x21-\x7e]") + + +def _invalid_inventory() -> DockerProviderError: + return DockerProviderError("Docker broker returned an invalid inventory response") + + +def _contains_ascii_control(value: str) -> bool: + return any(ord(char) < 32 or ord(char) == 127 for char in value) + + +@dataclass +class BrokerImage: + tags: list[str] + + +@dataclass +class BrokerContainer: + id: str + name: str + status: str + attrs: dict[str, Any] + image: BrokerImage + + +class BrokerContainers: + def __init__(self, provider: BrokerDockerClient) -> None: # type: ignore[name-defined] + self.provider = provider + + def list(self, *, all: bool = True) -> list[BrokerContainer]: + del all # Broker registration scope is deliberate; it returns all allowed targets. + payload = self.provider._get(_BROKER_PATH) + items = payload.get("items") + if not isinstance(items, list) or len(items) > _BROKER_MAX_CONTAINER_ITEMS: + raise _invalid_inventory() + if any(not isinstance(item, dict) for item in items): + raise _invalid_inventory() + return [self.provider._container(item) for item in items] + + +class BrokerDockerClient: + """Minimal Docker SDK-shaped client over the registered read broker.""" + + def __init__(self, base_url: str, token: str) -> None: + self.base_url = self._validated_base_url(base_url) + if ( + not isinstance(token, str) + or not token + or len(token) > 8192 + or _BROKER_BEARER_TOKEN.search(token) is not None + ): + raise DockerProviderError("Docker broker token is invalid") + # Keep the credential out of client-wide headers: health checks are + # public and only the one fixed inventory request needs authorization. + self._authorization = f"Bearer {token}" + self.http = httpx.Client( + timeout=httpx.Timeout( + _BROKER_OPERATION_TIMEOUT_SECONDS, + connect=_BROKER_CONNECT_TIMEOUT_SECONDS, + pool=_BROKER_CONNECT_TIMEOUT_SECONDS, + ), + follow_redirects=False, + trust_env=False, + ) + self.containers = BrokerContainers(self) + + @staticmethod + def _validated_base_url(base_url: str) -> str: + if ( + not isinstance(base_url, str) + or not base_url + or any(ord(char) < 33 for char in base_url) + ): + raise DockerProviderError("Docker broker URL is invalid") + try: + parsed = urlsplit(base_url) + # Accessing .port also rejects malformed or out-of-range ports. + _ = parsed.port + except ValueError: + raise DockerProviderError("Docker broker URL is invalid") from None + if ( + parsed.scheme.casefold() not in {"http", "https"} + or not parsed.hostname + or parsed.username is not None + or parsed.password is not None + or parsed.query + or parsed.fragment + or "\\" in parsed.path + or any(part == ".." for part in parsed.path.split("/")) + ): + raise DockerProviderError("Docker broker URL is invalid") + return urlunsplit((parsed.scheme, parsed.netloc, parsed.path.rstrip("/"), "", "")) + + @classmethod + def from_environment(cls) -> BrokerDockerClient: + # Never accept a request-provided Docker host as a broker destination: + # that could otherwise send the dedicated bearer token to an attacker- + # controlled endpoint. Broker identity is deployment configuration only. + url = (os.getenv("SERVICEMAP_DOCKER_BROKER_URL") or "").strip() + token_file = (os.getenv("SERVICEMAP_DOCKER_BROKER_TOKEN_FILE") or "").strip() + if not url or not token_file: + raise DockerProviderError( + "Broker mode requires SERVICEMAP_DOCKER_BROKER_URL and SERVICEMAP_DOCKER_BROKER_TOKEN_FILE" + ) + path = Path(token_file) + if not path.is_file(): + raise DockerProviderError("Docker broker token file is unavailable") + try: + token = path.read_text(encoding="utf-8").strip() + except OSError: + raise DockerProviderError("Docker broker token file cannot be read") from None + if not token or len(token) > 8192 or _BROKER_BEARER_TOKEN.search(token) is not None: + raise DockerProviderError("Docker broker token file is invalid") + return cls(url, token) + + def _get(self, path: str) -> dict[str, Any]: + if path != _BROKER_PATH: + raise DockerProviderError("Docker broker operation is unsupported") + try: + with self.http.stream( + "GET", + f"{self.base_url}{path}", + headers={"Authorization": self._authorization}, + ) as response: + response.raise_for_status() + if ( + response.headers.get("content-type", "").split(";", 1)[0].strip().casefold() + != "application/json" + ): + raise _invalid_inventory() + length = response.headers.get("content-length") + if length is not None: + try: + if int(length) < 0 or int(length) > _BROKER_MAX_RESPONSE_BYTES: + raise _invalid_inventory() + except ValueError: + raise _invalid_inventory() from None + raw = bytearray() + for chunk in response.iter_bytes(): + if len(raw) + len(chunk) > _BROKER_MAX_RESPONSE_BYTES: + raise _invalid_inventory() + raw.extend(chunk) + except httpx.HTTPError: + raise DockerProviderError("Docker broker request failed") from None + try: + payload = json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + raise DockerProviderError("Docker broker returned invalid JSON") from None + if not isinstance(payload, dict): + raise _invalid_inventory() + return payload + + def ping(self) -> bool: + # /healthz is intentionally unprivileged; an authenticated inventory + # request immediately follows and remains the actual authorization test. + try: + with self.http.stream("GET", f"{self.base_url}/healthz") as response: + response.raise_for_status() + except httpx.HTTPError: + raise DockerProviderError("Docker broker health check failed") from None + return True + + @staticmethod + def _container(item: dict[str, Any]) -> BrokerContainer: + def text_field(field: str, *, optional_default: str | None = None) -> str: + if field not in item: + if optional_default is not None: + return optional_default + raise _invalid_inventory() + value = item[field] + if optional_default is not None and value is None: + return optional_default + if ( + not isinstance(value, str) + or not value + or len(value) > 4096 + or _contains_ascii_control(value) + ): + raise _invalid_inventory() + return value + + ref = text_field("containerRef") + name = text_field("name").lstrip("/") + if not name: + raise _invalid_inventory() + state = text_field("state") + health = text_field("health", optional_default="none") + image = text_field("image") + + labels_value = item.get("labels") + if not isinstance(labels_value, dict) or any( + not isinstance(key, str) + or not isinstance(value, str) + or not key + or len(key) > 4096 + or len(value) > 4096 + or _contains_ascii_control(key + value) + for key, value in labels_value.items() + ): + raise _invalid_inventory() + networks_value = item.get("networks") + if not isinstance(networks_value, list) or any( + not isinstance(name, str) + or not name + or len(name) > 4096 + or _contains_ascii_control(name) + for name in networks_value + ): + raise _invalid_inventory() + # The deployed broker reports Docker health but does not yet emit an + # explicit healthcheckPresent flag. A concrete health status itself + # proves a healthcheck exists; preserve that discovery signal. + healthcheck_present = item.get("healthcheckPresent", health.casefold() not in {"none", "unknown"}) + if not isinstance(healthcheck_present, bool): + raise _invalid_inventory() + + ports: dict[str, list[dict[str, str]]] = {} + raw_ports = item.get("ports") + if not isinstance(raw_ports, dict): + raise _invalid_inventory() + for key, bindings in raw_ports.items(): + if ( + not isinstance(key, str) + or not key + or len(key) > 128 + or _contains_ascii_control(key) + ): + raise _invalid_inventory() + if bindings is None: + ports[key] = [] + continue + if not isinstance(bindings, list): + raise _invalid_inventory() + ports[key] = [] + for binding in bindings: + if not isinstance(binding, dict): + raise _invalid_inventory() + host_ip = binding.get("hostIp", "") + host_port = binding.get("hostPort", "") + if host_ip is None: + host_ip = "" + if host_port is None: + host_port = "" + if ( + not isinstance(host_ip, str) + or not isinstance(host_port, (str, int)) + or isinstance(host_port, bool) + ): + raise _invalid_inventory() + host_port = str(host_port) + if ( + len(host_ip) > 128 + or _contains_ascii_control(host_ip + host_port) + or ( + host_port + and ( + len(host_port) > 5 + or not all("0" <= char <= "9" for char in host_port) + or not 1 <= int(host_port) <= 65535 + ) + ) + ): + raise _invalid_inventory() + ports[key].append({"HostIp": host_ip, "HostPort": host_port}) + + return BrokerContainer( + id=ref, + name=name, + status=state, + attrs={ + "State": { + "Status": state, + "Health": {"Status": health} if health != "none" else {}, + }, + "Config": { + "Image": image, + "Labels": labels_value, + "Env": [], + **({"Healthcheck": {"BrokerReported": True}} if healthcheck_present else {}), + }, + "NetworkSettings": { + "Networks": {name: {} for name in networks_value}, + "Ports": ports, + }, + }, + image=BrokerImage(tags=[image]), + ) + + def close(self) -> None: + self.http.close() + + +def docker_client_for_inventory(docker_host: str): + provider = os.getenv("SERVICEMAP_DOCKER_PROVIDER", "socket").strip().casefold() + if provider == "socket": + return docker.DockerClient(base_url=docker_host, timeout=5) + if provider == "broker": + return BrokerDockerClient.from_environment() + raise DockerProviderError("SERVICEMAP_DOCKER_PROVIDER must be 'socket' or 'broker'") + + +def docker_provider_mode() -> str: + return os.getenv("SERVICEMAP_DOCKER_PROVIDER", "socket").strip().casefold() or "socket" diff --git a/backend/app/events.py b/backend/app/events.py new file mode 100644 index 0000000..b3f2010 --- /dev/null +++ b/backend/app/events.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import asyncio +import json +from queue import Empty, Full, Queue +from threading import Lock +from typing import Any + + +class EventBus: + """Small bounded fan-out bus used by the SSE endpoint. + + Persistence is handled by SQLite; this bus only wakes currently connected + clients. A queue per client keeps one slow browser from blocking probes or + other subscribers. + """ + + def __init__(self, max_queue_size: int = 100) -> None: + self.max_queue_size = max_queue_size + self._subscribers: set[Queue[dict[str, Any]]] = set() + self._lock = Lock() + + def subscribe(self) -> Queue[dict[str, Any]]: + queue: Queue[dict[str, Any]] = Queue(maxsize=self.max_queue_size) + with self._lock: + self._subscribers.add(queue) + return queue + + def unsubscribe(self, queue: Queue[dict[str, Any]]) -> None: + with self._lock: + self._subscribers.discard(queue) + + def publish(self, payload: dict[str, Any]) -> None: + with self._lock: + subscribers = tuple(self._subscribers) + for queue in subscribers: + try: + queue.put_nowait(payload) + except Full: + # Drop the oldest event for a lagging client, then retain the + # newest state notification. The durable event journal remains + # the source of truth if a client reconnects. + try: + queue.get_nowait() + except Empty: + pass + try: + queue.put_nowait(payload) + except Full: + pass +event_bus = EventBus() + + +def sse_payload(payload: dict[str, Any]) -> str: + event_type = str(payload.get("event_type", "update")) + return f"event: {event_type}\ndata: {json.dumps(payload, default=str)}\n\n" + + +async def next_event(queue: Queue[dict[str, Any]], timeout: float = 15.0) -> dict[str, Any] | None: + try: + return await asyncio.to_thread(queue.get, True, timeout) + except Empty: + return None diff --git a/backend/app/evidence.py b/backend/app/evidence.py new file mode 100644 index 0000000..9ee3e0d --- /dev/null +++ b/backend/app/evidence.py @@ -0,0 +1,134 @@ +"""Health-evidence selection policy. + +The policy is deliberately small and pure. Storage retains every provider +claim; this module decides which *current component-scoped* claim can answer +the direct-health question. It keeps aggregate AppOps health useful in the +inspector without ever treating it as a component probe. +""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +from .models import ( + EvidenceFreshness, + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthResolution, + HealthResolutionReason, + HealthStatus, +) + +_PROVIDER_PRIORITY = { + EvidenceProvider.MANUAL: 100, + EvidenceProvider.PROBE: 90, + EvidenceProvider.DOCKER: 80, + EvidenceProvider.APPOPS: 40, + EvidenceProvider.RUNTIME: 20, +} + + +def appops_health_status(value: str | None) -> HealthStatus | None: + """Keep AppOps aggregate health scoped to the application it describes.""" + + return { + "unhealthy": HealthStatus.DOWN, + "down": HealthStatus.DOWN, + "warning": HealthStatus.DEGRADED, + "degraded": HealthStatus.DEGRADED, + "healthy": HealthStatus.HEALTHY, + }.get((value or "").casefold()) + + +def utc_now() -> datetime: + return datetime.now(UTC) + + +def freshness_for(evidence: HealthEvidence, *, now: datetime | None = None) -> EvidenceFreshness: + """Return freshness without mutating the persisted observation. + + The bounded stale window is solely explanatory: stale and expired claims + are both excluded from health selection. Showing the distinction helps an + operator understand whether a provider is merely late or genuinely old. + """ + + now = now or utc_now() + if evidence.valid_until >= now: + return EvidenceFreshness.FRESH + ttl = max(evidence.valid_until - evidence.observed_at, timedelta(seconds=1)) + return ( + EvidenceFreshness.STALE if evidence.valid_until + ttl >= now else EvidenceFreshness.EXPIRED + ) + + +def with_freshness(evidence: HealthEvidence, *, now: datetime | None = None) -> HealthEvidence: + return evidence.model_copy(update={"freshness": freshness_for(evidence, now=now)}) + + +def resolve_health( + evidence: list[HealthEvidence], *, now: datetime | None = None +) -> HealthResolution: + """Resolve current direct health from independently persisted evidence.""" + + now = now or utc_now() + annotated = [with_freshness(item, now=now) for item in evidence] + eligible = [ + item + for item in annotated + if item.active + and item.scope == EvidenceScope.COMPONENT + and item.freshness == EvidenceFreshness.FRESH + ] + if not eligible: + component_claims = [ + item for item in annotated if item.scope == EvidenceScope.COMPONENT and item.active + ] + app_claims = [ + item for item in annotated if item.scope == EvidenceScope.APPLICATION and item.active + ] + if component_claims: + reason_code = ( + HealthResolutionReason.STALE_EVIDENCE + if any(item.freshness == EvidenceFreshness.STALE for item in component_claims) + else HealthResolutionReason.EXPIRED_EVIDENCE + ) + elif app_claims: + reason_code = HealthResolutionReason.APPLICATION_SCOPED_ONLY + else: + reason_code = HealthResolutionReason.NO_CURRENT_COMPONENT_EVIDENCE + explanation = ( + "Only application-scoped evidence is current; it is shown for provenance and is not " + "used as component health." + if app_claims + else "No current component-scoped health evidence is available." + ) + return HealthResolution(reason_code=reason_code, explanation=explanation, evidence=annotated) + + ordered = sorted( + eligible, + key=lambda item: ( + _PROVIDER_PRIORITY[item.provider], + item.confidence, + item.observed_at, + item.received_at, + item.id, + ), + reverse=True, + ) + selected = ordered[0] + conflict = len({item.status for item in eligible}) > 1 + scope_note = " Conflicting current component signals are retained below." if conflict else "" + return HealthResolution( + status=selected.status, + selected_evidence_id=selected.id, + selected_provider=selected.provider, + confidence=selected.confidence, + reason_code=HealthResolutionReason.SELECTED_COMPONENT_EVIDENCE, + explanation=( + f"{selected.provider.value} selected from current component-scoped evidence " + f"(confidence {selected.confidence:.2f}).{scope_note}" + ), + conflict=conflict, + evidence=annotated, + ) diff --git a/backend/app/health.py b/backend/app/health.py new file mode 100644 index 0000000..2cc3090 --- /dev/null +++ b/backend/app/health.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +from .models import HealthStatus, Service +from .store import Store + +SEVERITY = { + HealthStatus.MAINTENANCE: 0, + HealthStatus.UNMONITORED: 0, + HealthStatus.UNKNOWN: 0, + HealthStatus.HEALTHY: 1, + HealthStatus.RECOVERING: 2, + HealthStatus.DEGRADED: 3, + HealthStatus.DOWN: 4, +} + + +def _direct_status(service: Service, store: Store) -> HealthStatus: + return store.service_health(service.id) + + +def _dependency_status(service: Service, store: Store, visiting: set[str]) -> HealthStatus: + if not service.depends_on: + return HealthStatus.HEALTHY + + states: list[HealthStatus] = [] + for dependency_id in service.depends_on: + dependency = store.get_service(dependency_id) + if dependency is None: + states.append(HealthStatus.UNKNOWN) + continue + states.append(_effective_status(dependency, store, visiting)) + return max(states, key=lambda status: SEVERITY[status]) + + +def _effective_status(service: Service, store: Store, visiting: set[str]) -> HealthStatus: + if service.id in visiting: + return HealthStatus.UNKNOWN + next_visiting = set(visiting) + next_visiting.add(service.id) + own = _direct_status(service, store) + if own == HealthStatus.MAINTENANCE: + return own + dependency = _dependency_status(service, store, next_visiting) + if own == HealthStatus.DOWN: + return own + if dependency == HealthStatus.DOWN: + return HealthStatus.DEGRADED + if dependency in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} and own in { + HealthStatus.HEALTHY, + HealthStatus.RECOVERING, + }: + return HealthStatus.DEGRADED + return own + + +def dependency_status(service: Service, store: Store) -> HealthStatus: + return _dependency_status(service, store, {service.id}) + + +def effective_status(service: Service, store: Store) -> HealthStatus: + return _effective_status(service, store, set()) + + +def status_maps( + services: list[Service], + *, + direct_statuses: dict[str, HealthStatus] | None = None, +) -> tuple[dict[str, HealthStatus], dict[str, HealthStatus]]: + """Calculate the complete topology in memory for one bounded API read.""" + + by_id = {service.id: service for service in services} + def legacy_direct_state(service: Service) -> HealthStatus: + runtime_state = (service.runtime_state or "").casefold() + runtime_health = (service.runtime_health or "").casefold() + if not service.enabled: + return HealthStatus.UNKNOWN + if service.maintenance_enabled: + return HealthStatus.MAINTENANCE + if runtime_health == "unhealthy" or runtime_state in {"exited", "dead", "stopped"}: + return HealthStatus.DOWN + if runtime_state == "restarting": + return HealthStatus.DEGRADED + if not service.monitoring_enabled: + if runtime_health == "healthy": + return HealthStatus.HEALTHY + return HealthStatus.UNMONITORED if service.monitoring_state.value in {"unmonitored", "observed"} else HealthStatus.UNKNOWN + return service.state + + direct = { + service.id: (direct_statuses.get(service.id, HealthStatus.UNKNOWN) if direct_statuses else legacy_direct_state(service)) + for service in services + } + effective: dict[str, HealthStatus] = {} + dependency: dict[str, HealthStatus] = {} + + def visit(service_id: str, visiting: set[str]) -> HealthStatus: + if service_id in effective: + return effective[service_id] + if service_id in visiting: + return HealthStatus.UNKNOWN + service = by_id.get(service_id) + if service is None: + return HealthStatus.UNKNOWN + next_visiting = {*visiting, service_id} + own = direct[service_id] + if not service.depends_on: + dependency[service_id] = HealthStatus.HEALTHY + effective[service_id] = own + return own + dependencies = [visit(item, next_visiting) for item in service.depends_on] + dependency_state = max(dependencies, key=lambda status: SEVERITY[status]) + dependency[service_id] = dependency_state + if own == HealthStatus.MAINTENANCE or own == HealthStatus.DOWN: + result = own + elif dependency_state == HealthStatus.DOWN or ( + dependency_state in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} + and own in {HealthStatus.HEALTHY, HealthStatus.RECOVERING} + ): + result = HealthStatus.DEGRADED + else: + result = own + effective[service_id] = result + return result + + for service in services: + visit(service.id, set()) + return effective, dependency + + +def validate_dependencies( + depends_on: list[str], + store: Store, + *, + service_id: str | None = None, +) -> None: + if len(depends_on) != len(set(depends_on)): + raise ValueError("Dependencies must be unique") + if service_id and service_id in depends_on: + raise ValueError("A service cannot depend on itself") + + services = store.list_services() + known_ids = {service.id for service in services} + missing = [dependency_id for dependency_id in depends_on if dependency_id not in known_ids] + if missing: + raise ValueError(f"Unknown dependency ids: {', '.join(missing)}") + + if service_id is None: + return + + graph = {service.id: list(service.depends_on) for service in services} + graph[service_id] = list(depends_on) + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + raise ValueError("Dependency graph contains a cycle") + if node in visited: + return + visiting.add(node) + for dependency_id in graph.get(node, []): + visit(dependency_id) + visiting.remove(node) + visited.add(node) + + for node in graph: + visit(node) diff --git a/backend/app/incidents.py b/backend/app/incidents.py new file mode 100644 index 0000000..107d2bc --- /dev/null +++ b/backend/app/incidents.py @@ -0,0 +1,180 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from uuid import uuid4 + +from .health import dependency_status, effective_status +from .models import ( + Criticality, + HealthStatus, + IncidentCandidate, + IncidentOverview, + IncidentRecord, + IncidentStatus, + Service, +) +from .store import Store + + +def _reverse_dependencies(services: list[Service]) -> dict[str, list[str]]: + reverse: dict[str, list[str]] = {service.id: [] for service in services} + for service in services: + for dependency_id in service.depends_on: + reverse.setdefault(dependency_id, []).append(service.id) + return reverse + + +def affected_service_ids(root_id: str, services: list[Service]) -> list[str]: + reverse = _reverse_dependencies(services) + seen: set[str] = set() + queue = list(reverse.get(root_id, [])) + while queue: + current = queue.pop(0) + if current in seen or current == root_id: + continue + seen.add(current) + queue.extend(reverse.get(current, [])) + return sorted(seen) + + +def incident_overview(store: Store) -> IncidentOverview: + services = store.list_services() + by_id = {service.id: service for service in services} + effective = {service.id: effective_status(service, store) for service in services} + dependency = {service.id: dependency_status(service, store) for service in services} + + down_ids = sorted(service_id for service_id, state in effective.items() if state == HealthStatus.DOWN) + degraded_ids = sorted( + service_id for service_id, state in effective.items() if state in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} + ) + + candidates: list[IncidentCandidate] = [] + for service in services: + state = effective[service.id] + if state != HealthStatus.DOWN or service.maintenance_enabled: + continue + down_dependencies = [ + dependency_id + for dependency_id in service.depends_on + if effective.get(dependency_id) == HealthStatus.DOWN + ] + if down_dependencies: + continue + affected_ids = affected_service_ids(service.id, services) + affected_names = [by_id[item].name for item in affected_ids if item in by_id] + reason = "Service is down and no configured dependency is also down" + if dependency[service.id] == HealthStatus.UNKNOWN: + reason += "; at least one dependency has unknown health" + candidates.append( + IncidentCandidate( + service_id=service.id, + service_name=service.name, + status=state, + reason=reason, + affected_service_ids=affected_ids, + affected_service_names=affected_names, + ) + ) + + candidates.sort(key=lambda item: (-len(item.affected_service_ids), item.service_name.lower())) + return IncidentOverview( + generated_at=datetime.now(UTC), + active=bool(down_ids or degraded_ids), + candidates=candidates, + degraded_service_ids=degraded_ids, + down_service_ids=down_ids, + incidents=[], + ) + + +def _severity(service: Service) -> Criticality: + return service.criticality + + +def sync_incidents(store: Store, *, resolution_reason: str = "Root cause recovered; incident resolved") -> list[IncidentRecord]: + """Reconcile the current graph with the durable incident journal. + + A root service has at most one active incident. Repeated probe cycles only + update its affected set; a later independent outage gets a new id. + """ + + overview = incident_overview(store) + services = {service.id: service for service in store.list_services()} + now = datetime.now(UTC) + active: dict[str, IncidentRecord] = {} + for incident in sorted( + store.list_incidents(active_only=True), + key=lambda item: (item.detected_at, item.id), + ): + if incident.root_service_id in active: + store.upsert_incident(incident.model_copy(update={ + "status": IncidentStatus.RESOLVED, + "resolved_at": now, + "latest_update_at": now, + })) + store.append_incident_event( + incident.id, + "incident.duplicate_resolved", + "Duplicate active incident closed; the earliest incident remains active", + incident.root_service_id, + ) + continue + active[incident.root_service_id] = incident + for candidate in overview.candidates: + service = services.get(candidate.service_id) + if service is None: + continue + incident = active.get(candidate.service_id) + if incident is None: + incident = IncidentRecord( + id=str(uuid4()), + status=IncidentStatus.OPEN, + root_service_id=candidate.service_id, + root_service_name=candidate.service_name, + started_at=now, + detected_at=now, + title=f"{candidate.service_name} is down", + root_cause_reason=candidate.reason, + affected_service_ids=candidate.affected_service_ids, + affected_service_names=candidate.affected_service_names, + severity=_severity(service), + latest_update_at=now, + ) + store.upsert_incident(incident) + store.append_incident_event( + incident.id, + "incident.opened", + f"Incident opened: {candidate.service_name} is the likely root cause", + candidate.service_id, + ) + active[candidate.service_id] = incident + elif incident.affected_service_ids != candidate.affected_service_ids: + updated = incident.model_copy( + update={ + "affected_service_ids": candidate.affected_service_ids, + "affected_service_names": candidate.affected_service_names, + "latest_update_at": now, + "root_cause_reason": candidate.reason, + } + ) + store.upsert_incident(updated) + store.append_incident_event(incident.id, "incident.updated", "Downstream impact recalculated", candidate.service_id) + + for root_id, incident in list(active.items()): + if any(candidate.service_id == root_id for candidate in overview.candidates): + continue + root = services.get(root_id) + if ( + (root is None or store.service_health(root_id) not in { + HealthStatus.DOWN, + HealthStatus.DEGRADED, + HealthStatus.RECOVERING, + }) + and incident.status != IncidentStatus.RESOLVED + ): + resolved = incident.model_copy( + update={"status": IncidentStatus.RESOLVED, "resolved_at": now, "latest_update_at": now} + ) + store.upsert_incident(resolved) + store.append_incident_event(incident.id, "incident.resolved", resolution_reason, root_id) + return store.list_incidents(active_only=False) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..da2808d --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,743 @@ +from __future__ import annotations + +import asyncio +import os +import sqlite3 +from contextlib import asynccontextmanager, suppress +from datetime import timedelta +from pathlib import Path + +from fastapi import FastAPI, HTTPException, Query, Response, status +from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles + +from .auth import AuthConfig, AuthorizationError, authorize_request +from .config_io import export_document, import_document, parse_document, serialize_document +from .demo import DEMO_ROOT_CAUSE_NAME, DEMO_SCENARIO_NAME, build_demo_records +from .discovery import DiscoveryManager +from .discovery_store import DiscoveryStore +from .docker_discovery import discover_containers +from .docker_provider import docker_provider_mode +from .events import event_bus, next_event, sse_payload +from .health import dependency_status, effective_status, status_maps, validate_dependencies +from .incidents import incident_overview, sync_incidents +from .models import ( + AvailabilitySummary, + BulkProbeResult, + ConfigImportRequest, + ConfigImportResult, + DemoMutationResult, + DemoState, + DiscoveryObservation, + DiscoveryReconcileRequest, + DiscoveryResult, + DiscoveryRunRequest, + DiscoveryStatus, + DockerDiscoveryRequest, + DockerDiscoveryResult, + EdgeActionRequest, + EdgeMutationRequest, + EventRecord, + HealthResolution, + IncidentOverview, + LogicalRelation, + MaintenanceRequest, + OverviewSummary, + ProbeRepairReport, + ProbeResult, + ProviderStatus, + ReconcileResult, + RelationEvidence, + SemanticTopology, + Service, + ServiceCreate, + ServiceSnapshot, + ServiceUpdate, + SuggestionStatus, + TopologyDiff, + TopologySnapshot, + TopologySuggestion, +) +from .observation import ObservationManager +from .probes import probe_many, probe_service +from .scheduler import scheduled_probe_loop +from .semantic import build_semantic_topology +from .store import Store + +store = Store() +discovery_store = DiscoveryStore(store) +discovery_manager = DiscoveryManager(store, discovery_store) + + +def _env_int(name: str, default: int, minimum: int = 0) -> int: + try: + return max(minimum, int(os.getenv(name, str(default)))) + except ValueError: + return default + + +def _env_float(name: str, default: float, minimum: float = 0.1) -> float: + try: + return max(minimum, float(os.getenv(name, str(default)))) + except ValueError: + return default + + +PROBE_INTERVAL = _env_int("SERVICEMAP_PROBE_INTERVAL", 0) +PROBE_TIMEOUT = _env_float("SERVICEMAP_PROBE_TIMEOUT", 5.0) +PROBE_RETRIES = _env_int("SERVICEMAP_PROBE_RETRIES", 1) +PROBE_CONCURRENCY = _env_int("SERVICEMAP_PROBE_CONCURRENCY", 10, minimum=1) +RETENTION_DAYS = _env_int("SERVICEMAP_RETENTION_DAYS", 30, minimum=1) +EVENT_RETENTION_DAYS = _env_int("SERVICEMAP_EVENT_RETENTION_DAYS", 90, minimum=1) +DISCOVERY_INTERVAL = _env_int("SERVICEMAP_DISCOVERY_INTERVAL_SECONDS", 0, minimum=0) +OBSERVATION_INTERVAL = _env_int("SERVICEMAP_OBSERVATION_INTERVAL_SECONDS", 60, minimum=10) +observation_manager = ObservationManager(store, interval_seconds=OBSERVATION_INTERVAL) + + +def _require_configured_discovery_targets( + docker_host: str | None = None, appops_url: str | None = None +) -> None: + """Request bodies cannot redirect an unauthenticated LAN deployment's inventory reads.""" + + configured_docker = os.getenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock") + configured_appops = os.getenv("SERVICEMAP_APPOPS_URL", "") + if docker_host and docker_host != configured_docker: + raise HTTPException(status_code=403, detail="Docker discovery target is not configured") + if appops_url and appops_url.rstrip("/") != configured_appops.rstrip("/"): + raise HTTPException(status_code=403, detail="AppOps discovery target is not configured") + + +@asynccontextmanager +async def lifespan(_: FastAPI): + task: asyncio.Task[None] | None = None + discovery_task: asyncio.Task[None] | None = None + observation_task: asyncio.Task[None] | None = None + if PROBE_INTERVAL > 0: + task = asyncio.create_task( + scheduled_probe_loop( + store, + interval_seconds=PROBE_INTERVAL, + timeout_seconds=PROBE_TIMEOUT, + retries=PROBE_RETRIES, + concurrency=PROBE_CONCURRENCY, + retention_days=RETENTION_DAYS, + ) + ) + if DISCOVERY_INTERVAL > 0: + async def discovery_loop() -> None: + while True: + await asyncio.sleep(DISCOVERY_INTERVAL) + if store.demo_active(): + continue + try: + request = DiscoveryRunRequest() + result = await asyncio.to_thread(discovery_manager.run, request) + await asyncio.to_thread( + discovery_manager.reconcile, + DiscoveryReconcileRequest(run_id=result.run.id, apply_high_confidence=False), + ) + except (OSError, RuntimeError, ValueError): + # Discovery is review-first; a failed background run is + # persisted and must not take down the monitoring loop. + continue + discovery_task = asyncio.create_task(discovery_loop()) + async def observation_loop() -> None: + while True: + if not store.demo_active(): + try: + await asyncio.to_thread(observation_manager.refresh) + except (OSError, RuntimeError, ValueError): + # Provider availability is persisted by the observer; an + # unavailable provider must not stop scheduled probing. + pass + await asyncio.sleep(OBSERVATION_INTERVAL) + observation_task = asyncio.create_task(observation_loop()) + try: + yield + finally: + if task: + task.cancel() + with suppress(asyncio.CancelledError): + await task + if discovery_task: + discovery_task.cancel() + with suppress(asyncio.CancelledError): + await discovery_task + if observation_task: + observation_task.cancel() + with suppress(asyncio.CancelledError): + await observation_task + + +app = FastAPI(title="ServiceMap API", version="1.6.2", lifespan=lifespan) + + +@app.middleware("http") +async def authorization_boundary(request, call_next): + try: + authorize_request(request) + except HTTPException as exc: + return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail}) + return await call_next(request) + + +def _snapshot(service: Service) -> ServiceSnapshot: + services = store.list_services() + dependents = [item.name for item in services if service.id in item.depends_on] + return ServiceSnapshot( + service=service, + latest_probe=store.latest_probe(service.id), + availability_24h=store.availability_summary(service.id, 24), + dependency_status=dependency_status(service, store), + effective_status=effective_status(service, store), + direct_status=store.service_health(service.id), + flapping=service.flapping, + flap_count=service.flap_count, + next_probe_at=store.next_probe_at(service, PROBE_INTERVAL), + dependents=dependents, + sources=discovery_store.sources_for_service(service.id), + evidence=discovery_store.evidence_for_service(service.id), + health_evidence=store.health_evidence_for_service(service.id), + health_resolution=store.health_resolution(service.id), + ) + + +def _name_conflict(name: str, *, exclude_id: str | None = None) -> bool: + existing = store.get_service_by_name(name) + return existing is not None and existing.id != exclude_id + + +@app.get("/health") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@app.get("/api/v1/meta") +def meta() -> dict[str, str | int | float | bool]: + try: + auth_mode = AuthConfig.from_environment().mode + except AuthorizationError: + auth_mode = "misconfigured" + return { + "name": "ServiceMap", + "version": "1.6.2", + "stage": "v1", + "scheduled_probing": PROBE_INTERVAL > 0, + "probe_interval_seconds": PROBE_INTERVAL, + "probe_timeout_seconds": PROBE_TIMEOUT, + "probe_retries": PROBE_RETRIES, + "probe_concurrency": PROBE_CONCURRENCY, + "retention_days": RETENTION_DAYS, + "event_retention_days": EVENT_RETENTION_DAYS, + "sse": True, + "discovery": True, + "discovery_interval_seconds": DISCOVERY_INTERVAL, + "observation_interval_seconds": OBSERVATION_INTERVAL, + "docker_configured": bool(os.getenv("SERVICEMAP_DOCKER_HOST")), + "docker_provider": docker_provider_mode(), + "appops_configured": bool(os.getenv("SERVICEMAP_APPOPS_URL")), + "auth_mode": auth_mode, + } + + +def _service_snapshots( + *, include_hidden: bool = False, include_context: bool = True +) -> list[ServiceSnapshot]: + all_services = store.list_services() + services = ( + all_services + if include_hidden + else [ + service + for service in all_services + if service.visibility.value == "visible" and service.lifecycle.value not in {"retired", "ignored"} + ] + ) + latest = store.latest_probes([service.id for service in services]) + availability = store.availability_summaries([service.id for service in services], 24) + evidence_by_service = store.health_evidence_for_services([service.id for service in all_services]) + # Calculate propagation over the complete canonical graph first. Hidden + # infrastructure nodes may still sit between a visible service and its + # dependency; filtering before this calculation would turn that impact + # into a misleading unknown state. + direct_all, resolutions = store.service_healths( + [service.id for service in all_services], evidence_by_service=evidence_by_service + ) + effective_all, dependency_all = status_maps(all_services, direct_statuses=direct_all) + dependent_names = { + service.id: [item.name for item in services if service.id in item.depends_on] + for service in services + } + return [ + ServiceSnapshot( + service=service, + latest_probe=latest.get(service.id), + availability_24h=availability.get(service.id), + dependency_status=dependency_all.get(service.id, service.state), + effective_status=effective_all.get(service.id, service.state), + direct_status=direct_all.get(service.id, service.state), + flapping=service.flapping, + flap_count=service.flap_count, + next_probe_at=( + latest[service.id].checked_at + timedelta(seconds=service.interval_seconds or PROBE_INTERVAL) + if service.id in latest and (service.interval_seconds or PROBE_INTERVAL) > 0 + else None + ), + dependents=dependent_names.get(service.id, []), + sources=discovery_store.sources_for_service(service.id) if include_context else [], + evidence=discovery_store.evidence_for_service(service.id) if include_context else [], + health_evidence=evidence_by_service.get(service.id, []), + health_resolution=resolutions.get(service.id, HealthResolution()), + ) + for service in services + ] + + +@app.get("/api/v1/services", response_model=list[ServiceSnapshot]) +def list_services(include_hidden: bool = Query(default=False)) -> list[ServiceSnapshot]: + return _service_snapshots(include_hidden=include_hidden) + + +@app.post("/api/v1/services", response_model=Service, status_code=status.HTTP_201_CREATED) +def create_service(payload: ServiceCreate) -> Service: + if _name_conflict(payload.name): + raise HTTPException(status_code=409, detail="Service name already exists") + try: + validate_dependencies(payload.depends_on, store) + return store.create_service(payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except sqlite3.IntegrityError as exc: + raise HTTPException(status_code=409, detail="Service name already exists") from exc + + +@app.get("/api/v1/services/{service_id}", response_model=ServiceSnapshot) +def get_service(service_id: str) -> ServiceSnapshot: + service = store.get_service(service_id) + if service is None: + raise HTTPException(status_code=404, detail="Service not found") + return _snapshot(service) + + +@app.put("/api/v1/services/{service_id}", response_model=Service) +def update_service(service_id: str, payload: ServiceUpdate) -> Service: + before = store.get_service(service_id) + if before is None: + raise HTTPException(status_code=404, detail="Service not found") + if _name_conflict(payload.name, exclude_id=service_id): + raise HTTPException(status_code=409, detail="Service name already exists") + try: + validate_dependencies(payload.depends_on, store, service_id=service_id) + updated = store.update_service(service_id, payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + except sqlite3.IntegrityError as exc: + raise HTTPException(status_code=409, detail="Service name already exists") from exc + if updated is None: + raise HTTPException(status_code=404, detail="Service not found") + if before.source_type: + discovery_manager.record_service_override(before, updated) + return updated + + +@app.delete("/api/v1/services/{service_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_service(service_id: str) -> None: + if not store.delete_service(service_id): + raise HTTPException(status_code=404, detail="Service not found") + + +@app.post("/api/v1/services/{service_id}/probe", response_model=ProbeResult) +async def run_probe( + service_id: str, + timeout_seconds: float | None = Query(default=None, ge=0.1, le=30), + retries: int | None = Query(default=None, ge=0, le=5), +) -> ProbeResult: + if store.demo_active(): + raise HTTPException(status_code=409, detail="Explore demo uses synthetic probe data; live probing is paused") + service = store.get_service(service_id) + if service is None: + raise HTTPException(status_code=404, detail="Service not found") + result = await probe_service( + service, + timeout_seconds=timeout_seconds, + retries=retries, + trigger="manual", + default_timeout_seconds=PROBE_TIMEOUT, + default_retries=PROBE_RETRIES, + ) + return store.save_probe(result) + + +@app.post("/api/v1/probe-all", response_model=BulkProbeResult) +async def run_all_probes( + timeout_seconds: float | None = Query(default=None, ge=0.1, le=30), + retries: int | None = Query(default=None, ge=0, le=5), + concurrency: int = Query(default=PROBE_CONCURRENCY, ge=1, le=50), +) -> BulkProbeResult: + if store.demo_active(): + return BulkProbeResult(requested=0, completed=0, results=[]) + services = [service for service in store.list_services() if service.enabled and service.monitoring_enabled] + results = await probe_many( + services, + timeout_seconds=timeout_seconds, + retries=retries, + concurrency=concurrency, + trigger="manual", + default_timeout_seconds=PROBE_TIMEOUT, + default_retries=PROBE_RETRIES, + ) + for result in results: + store.save_probe(result) + return BulkProbeResult(requested=len(services), completed=len(results), results=results) + + +@app.get("/api/v1/services/{service_id}/history", response_model=list[ProbeResult]) +def probe_history(service_id: str, limit: int = Query(default=50, ge=1, le=500)) -> list[ProbeResult]: + if store.get_service(service_id) is None: + raise HTTPException(status_code=404, detail="Service not found") + return store.probe_history(service_id, limit) + + +@app.get("/api/v1/services/{service_id}/availability", response_model=AvailabilitySummary) +def availability(service_id: str, hours: int = Query(default=24, ge=1, le=24 * 90)) -> AvailabilitySummary: + if store.get_service(service_id) is None: + raise HTTPException(status_code=404, detail="Service not found") + return store.availability_summary(service_id, hours) + + +@app.get("/api/v1/services/{service_id}/evidence", response_model=HealthResolution) +def service_evidence(service_id: str) -> HealthResolution: + if store.get_service(service_id) is None: + raise HTTPException(status_code=404, detail="Service not found") + return store.health_resolution(service_id) + + +@app.get("/api/v1/providers", response_model=list[ProviderStatus]) +def providers() -> list[ProviderStatus]: + return store.provider_statuses() + + +@app.post("/api/v1/services/{service_id}/maintenance", response_model=Service) +def maintenance(service_id: str, payload: MaintenanceRequest) -> Service: + updated = store.set_maintenance(service_id, payload.enabled, payload.reason) + if updated is None: + raise HTTPException(status_code=404, detail="Service not found") + return updated + + +@app.get("/api/v1/incidents", response_model=IncidentOverview) +def incidents() -> IncidentOverview: + sync_incidents(store) + overview = incident_overview(store) + overview.incidents = store.list_incidents(active_only=False) + return overview + + +@app.get("/api/v1/incidents/{incident_id}") +def get_incident(incident_id: str) -> dict[str, object]: + incident = store.get_incident(incident_id) + if incident is None: + raise HTTPException(status_code=404, detail="Incident not found") + return {"incident": incident.model_dump(mode="json"), "timeline": [item.model_dump(mode="json") for item in store.incident_timeline(incident_id)]} + + +@app.post("/api/v1/incidents/{incident_id}/acknowledge") +def acknowledge_incident(incident_id: str) -> dict[str, object]: + try: + incident = store.acknowledge_incident(incident_id) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + if incident is None: + raise HTTPException(status_code=404, detail="Incident not found") + return {"incident": incident.model_dump(mode="json"), "timeline": [item.model_dump(mode="json") for item in store.incident_timeline(incident_id)]} + + +@app.get("/api/v1/events", response_model=list[EventRecord]) +def events(limit: int = Query(default=50, ge=1, le=500), service_id: str | None = None) -> list[EventRecord]: + return store.list_events(limit=limit, service_id=service_id) + + +@app.get("/api/v1/events/stream") +async def event_stream() -> StreamingResponse: + queue = event_bus.subscribe() + + async def stream(): + try: + yield ": connected\n\n" + while True: + payload = await next_event(queue) + if payload is None: + yield ": keepalive\n\n" + else: + yield sse_payload(payload) + finally: + event_bus.unsubscribe(queue) + + return StreamingResponse( + stream(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + +@app.get("/api/v1/overview", response_model=OverviewSummary) +def overview() -> OverviewSummary: + sync_incidents(store) + snapshots = _service_snapshots(include_hidden=False) + overview_state = incident_overview(store) + counts: dict[str, int] = {} + coverage: dict[str, int] = {} + for snapshot in snapshots: + counts[snapshot.effective_status.value] = counts.get(snapshot.effective_status.value, 0) + 1 + state = snapshot.service.monitoring_state.value + coverage[state] = coverage.get(state, 0) + 1 + return OverviewSummary( + generated_at=overview_state.generated_at, + total_services=len(snapshots), + health_counts=counts, + active_incidents=len(store.list_incidents(active_only=True)), + likely_root_causes=[candidate.service_name for candidate in overview_state.candidates], + flapping_services=[snapshot.service.name for snapshot in snapshots if snapshot.flapping], + recent_events=store.list_events(limit=10), + monitoring_coverage=coverage, + ) + + +@app.get("/api/v1/config") +def export_config(format: str = Query(default="yaml", pattern="^(yaml|json)$")) -> Response: + document = export_document(store) + content = serialize_document(document, format) + media_type = "application/json" if format == "json" else "application/yaml" + extension = "json" if format == "json" else "yaml" + return Response(content=content, media_type=media_type, headers={"Content-Disposition": f'attachment; filename="servicemap.{extension}"'}) + + +@app.post("/api/v1/config/import", response_model=ConfigImportResult) +def import_config(payload: ConfigImportRequest) -> ConfigImportResult: + try: + document = parse_document(payload.content, payload.format) + return import_document(store, document, payload.mode) + except (ValueError, sqlite3.IntegrityError) as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +def _demo_state() -> DemoState: + if not store.demo_active(): + return DemoState(active=False) + sync_incidents(store) + overview_state = incident_overview(store) + candidate = overview_state.candidates[0] if overview_state.candidates else None + return DemoState( + active=True, + synthetic=True, + scenario_name=DEMO_SCENARIO_NAME, + service_count=len(store.list_services()), + root_cause_service_id=candidate.service_id if candidate else None, + root_cause_service_name=candidate.service_name if candidate else DEMO_ROOT_CAUSE_NAME, + affected_service_ids=candidate.affected_service_ids if candidate else [], + affected_service_names=candidate.affected_service_names if candidate else [], + affected_count=len(candidate.affected_service_ids) if candidate else 0, + ) + + +def _demo_result(result: ConfigImportResult | None = None, *, restored: bool = False) -> DemoMutationResult: + state = _demo_state() + return DemoMutationResult( + **state.model_dump(), + created=result.created if result else 0, + updated=result.updated if result else 0, + total=result.total if result else state.service_count, + restored=restored, + ) + + +@app.get("/api/v1/demo", response_model=DemoState) +def demo_state() -> DemoState: + return _demo_state() + + +@app.post("/api/v1/demo", response_model=DemoMutationResult) +def load_demo() -> DemoMutationResult: + services, probes = build_demo_records() + result = store.activate_demo(services, probes) + sync_incidents(store) + return _demo_result(result) + + +@app.post("/api/v1/demo/reset", response_model=DemoMutationResult) +def reset_demo() -> DemoMutationResult: + services, probes = build_demo_records() + result = store.activate_demo(services, probes) + sync_incidents(store) + return _demo_result(result) + + +@app.post("/api/v1/demo/exit", response_model=DemoMutationResult) +async def exit_demo() -> DemoMutationResult: + restored = store.exit_demo() + # Refresh live provider evidence immediately after restoring the saved + # production topology. Test stores intentionally do not inherit the + # process-global observer, but production always does. + if restored and observation_manager.store is store: + await asyncio.to_thread(observation_manager.refresh) + return _demo_result(restored=restored) + + +@app.post("/api/v1/discovery/run", response_model=DiscoveryResult) +def run_discovery(payload: DiscoveryRunRequest | None = None) -> DiscoveryResult: + if store.demo_active(): + raise HTTPException(status_code=409, detail="Exit Explore demo before discovering production infrastructure") + request = payload or DiscoveryRunRequest() + _require_configured_discovery_targets(request.docker_host, request.appops_url) + try: + return discovery_manager.run(request) + except ValueError as exc: + raise HTTPException(status_code=409, detail=str(exc)) from exc + + +@app.get("/api/v1/discovery/status", response_model=DiscoveryStatus) +def discovery_status() -> DiscoveryStatus: + return DiscoveryStatus(**discovery_manager.status()) + + +@app.get("/api/v1/discovery/items", response_model=list[DiscoveryObservation]) +def discovery_items(run_id: str | None = None) -> list[DiscoveryObservation]: + selected = discovery_store.get_run(run_id) if run_id else discovery_store.last_completed() + return discovery_store.observations_for_run(selected.id) if selected else [] + + +@app.get("/api/v1/discovery/suggestions", response_model=list[TopologySuggestion]) +def discovery_suggestions( + status_filter: str | None = Query(default=None, alias="status"), +) -> list[TopologySuggestion]: + try: + suggestion_status = SuggestionStatus(status_filter) if status_filter else None + except ValueError as exc: + raise HTTPException(status_code=422, detail="Unknown suggestion status") from exc + return discovery_store.list_suggestions(status=suggestion_status) + + +@app.post("/api/v1/discovery/suggestions/{suggestion_id}/accept", response_model=TopologySuggestion) +def accept_discovery_suggestion(suggestion_id: str) -> TopologySuggestion: + try: + return discovery_manager.suggestion_action(suggestion_id, "confirm") + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.post("/api/v1/discovery/suggestions/{suggestion_id}/reject", response_model=TopologySuggestion) +def reject_discovery_suggestion(suggestion_id: str) -> TopologySuggestion: + try: + return discovery_manager.suggestion_action(suggestion_id, "reject") + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.post("/api/v1/discovery/reconcile", response_model=ReconcileResult) +def reconcile_discovery(payload: DiscoveryReconcileRequest | None = None) -> ReconcileResult: + if store.demo_active(): + raise HTTPException(status_code=409, detail="Exit Explore demo before reconciling production infrastructure") + try: + return discovery_manager.reconcile(payload or DiscoveryReconcileRequest()) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@app.post("/api/v1/discovery/probes/repair", response_model=ProbeRepairReport) +def repair_discovered_probes( + dry_run: bool = Query(default=True), + run_id: str | None = Query(default=None), +) -> ProbeRepairReport: + if store.demo_active(): + raise HTTPException(status_code=409, detail="Exit Explore demo before repairing production probe endpoints") + try: + return discovery_manager.repair_probe_reachability(run_id=run_id, dry_run=dry_run) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@app.get("/api/v1/topology/edges", response_model=list[RelationEvidence]) +def topology_edges() -> list[RelationEvidence]: + return discovery_store.list_evidence() + + +@app.get("/api/v1/topology/relations", response_model=list[LogicalRelation]) +def topology_relations() -> list[LogicalRelation]: + """Canonical relationships: one graph edge, all supporting evidence retained.""" + return discovery_store.list_logical_relations() + + +@app.get("/api/v1/topology/snapshots", response_model=list[TopologySnapshot]) +def topology_snapshots(limit: int = Query(default=30, ge=1, le=200)) -> list[TopologySnapshot]: + return discovery_store.list_snapshots(limit=limit) + + +@app.get("/api/v1/topology/diff", response_model=TopologyDiff) +def topology_diff( + after_snapshot_id: str = Query(...), + before_snapshot_id: str | None = Query(default=None), +) -> TopologyDiff: + try: + return discovery_store.topology_diff( + after_snapshot_id=after_snapshot_id, + before_snapshot_id=before_snapshot_id, + ) + except ValueError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + + +@app.get("/api/v1/topology/semantic", response_model=SemanticTopology) +def semantic_topology(include_hidden: bool = Query(default=False)) -> SemanticTopology: + snapshots = _service_snapshots(include_hidden=include_hidden, include_context=False) + incident_state = incident_overview(store) + root_ids = {candidate.service_id for candidate in incident_state.candidates} + affected_ids = { + service_id + for candidate in incident_state.candidates + for service_id in candidate.affected_service_ids + } + return build_semantic_topology( + snapshots, + discovery_store.list_logical_relations(), + root_service_ids=root_ids, + affected_service_ids=affected_ids, + ) + + +@app.post("/api/v1/topology/edges", response_model=RelationEvidence, status_code=status.HTTP_201_CREATED) +def create_topology_edge(payload: EdgeMutationRequest) -> RelationEvidence: + try: + return discovery_manager.apply_edge(payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@app.post("/api/v1/topology/edges/{evidence_id}/action", response_model=RelationEvidence) +def topology_edge_action(evidence_id: str, payload: EdgeActionRequest) -> RelationEvidence: + try: + return discovery_manager.edge_action(evidence_id, payload.action) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@app.post("/api/v1/docker/discover", response_model=DockerDiscoveryResult) +def docker_discover(payload: DockerDiscoveryRequest) -> DockerDiscoveryResult: + _require_configured_discovery_targets(payload.docker_host) + try: + return discover_containers( + payload.docker_host, + store.list_services(), + appops_url=os.getenv("SERVICEMAP_APPOPS_URL"), + ) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + + +@app.post("/api/v1/maintenance/prune") +def prune_history(retention_days: int = Query(default=RETENTION_DAYS, ge=1, le=3650)) -> dict[str, int]: + return {"deleted_probe_results": store.prune_probes(retention_days, EVENT_RETENTION_DAYS)} + + +static_dir = Path(os.getenv("SERVICEMAP_STATIC_DIR", "/app/static")) +if static_dir.is_dir(): + app.mount("/", StaticFiles(directory=static_dir, html=True), name="frontend") diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..452375b --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,862 @@ +from __future__ import annotations + +from datetime import datetime +from enum import StrEnum +from urllib.parse import urlparse + +from pydantic import AliasChoices, BaseModel, Field, model_validator + + +class ServiceKind(StrEnum): + HTTP = "http" + TCP = "tcp" + DNS = "dns" + TLS = "tls" + + +class HealthStatus(StrEnum): + UNKNOWN = "unknown" + UNMONITORED = "unmonitored" + HEALTHY = "healthy" + DEGRADED = "degraded" + DOWN = "down" + RECOVERING = "recovering" + MAINTENANCE = "maintenance" + + +class EvidenceProvider(StrEnum): + """Systems that can make a health claim about an infrastructure subject.""" + + PROBE = "active_probe" + DOCKER = "docker" + APPOPS = "appops" + RUNTIME = "runtime" + MANUAL = "manual" + + +class EvidenceScope(StrEnum): + """Whether a claim describes one component or an aggregate application.""" + + COMPONENT = "component" + APPLICATION = "application" + + +class EvidenceFreshness(StrEnum): + FRESH = "fresh" + STALE = "stale" + EXPIRED = "expired" + + +class HealthResolutionReason(StrEnum): + """Machine-readable explanation for a health answer or lack of one.""" + + SELECTED_COMPONENT_EVIDENCE = "selected_component_evidence" + APPLICATION_SCOPED_ONLY = "application_scoped_only" + NO_CURRENT_COMPONENT_EVIDENCE = "no_current_component_evidence" + STALE_EVIDENCE = "stale_evidence" + EXPIRED_EVIDENCE = "expired_evidence" + SELECTED_APPLICATION_EVIDENCE = "selected_application_evidence" + COMPONENT_FAILURE = "component_failure" + COMPONENT_IMPACT = "component_dependency_impact" + COMPONENT_COVERAGE = "component_coverage" + NO_APPLICATION_EVIDENCE = "no_application_evidence" + + +class ProviderHealth(StrEnum): + HEALTHY = "healthy" + DEGRADED = "degraded" + UNAVAILABLE = "unavailable" + UNKNOWN = "unknown" + + +class MonitoringState(StrEnum): + """How a service is currently observed, independently of its health.""" + + ACTIVE = "active_probe" + DOCKER_HEALTH = "docker_health" + APPOPS = "appops_observation" + OBSERVED = "observed" + UNMONITORED = "unmonitored" + DISABLED = "disabled" + + +class ProbeEndpointSource(StrEnum): + MANUAL = "manual" + EXPLICIT_LABEL = "explicit_label" + KNOWN_URL = "known_url" + APPOPS = "appops" + REVERSE_PROXY = "reverse_proxy" + PUBLISHED_PORT = "published_port" + DOCKER_HEALTHCHECK = "docker_healthcheck" + CONTAINER_INTERNAL = "container_internal" + DISCOVERED_ENVIRONMENT = "discovered_environment" + + +class ProbeReachability(StrEnum): + VERIFIED = "verified" + REACHABLE = "reachable" + UNVERIFIED = "unverified" + INTERNAL_ONLY = "internal_only" + NOT_CONFIGURED = "not_configured" + + +class Criticality(StrEnum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + CRITICAL = "critical" + + +class IncidentStatus(StrEnum): + OPEN = "open" + ACKNOWLEDGED = "acknowledged" + RESOLVED = "resolved" + + +class DiscoveryCategory(StrEnum): + APPLICATION = "application" + DATABASE = "database" + CACHE = "cache" + PROXY = "proxy" + MONITORING = "monitoring" + DEVELOPMENT = "development" + STORAGE = "storage" + INFRASTRUCTURE = "infrastructure" + BACKGROUND_WORKER = "background_worker" + SYSTEM = "system" + UNKNOWN = "unknown" + + +class ComponentRole(StrEnum): + """The operational role a component plays inside an application.""" + + FRONTEND = "frontend" + API = "api" + DATABASE = "database" + CACHE = "cache" + PROXY = "proxy" + WORKER = "worker" + STORAGE = "storage" + MONITORING = "monitoring" + AGENT = "agent" + SCHEDULER = "scheduler" + AUXILIARY = "auxiliary" + UNKNOWN = "unknown" + + +class DiscoveryVisibility(StrEnum): + VISIBLE = "visible" + HIDDEN = "hidden" + IGNORED = "ignored" + + +class DiscoveryLifecycle(StrEnum): + ACTIVE = "active" + MISSING = "missing" + STALE = "stale" + IGNORED = "ignored" + RETIRED = "retired" + + +class DiscoveryConfidence(StrEnum): + CONFIRMED = "confirmed" + HIGH = "high" + MEDIUM = "medium" + LOW = "low" + + +class DiscoveryRunStatus(StrEnum): + IDLE = "idle" + DISCOVERING = "discovering" + RECONCILING = "reconciling" + COMPLETED = "completed" + FAILED = "failed" + + +class SuggestionStatus(StrEnum): + OPEN = "open" + ACCEPTED = "accepted" + REJECTED = "rejected" + SUPERSEDED = "superseded" + + +class RelationType(StrEnum): + DEPENDS_ON = "depends_on" + ROUTES_TO = "routes_to" + USES_STORAGE = "uses_storage" + COMMUNICATES_WITH = "communicates_with" + + +class ServiceCreate(BaseModel): + name: str = Field(min_length=1, max_length=120) + kind: ServiceKind + url: str | None = None + host: str | None = Field(default=None, validation_alias=AliasChoices("host", "hostname")) + port: int | None = Field(default=None, ge=1, le=65535) + depends_on: list[str] = Field(default_factory=list) + description: str | None = Field(default=None, max_length=500) + tags: list[str] = Field(default_factory=list, max_length=20) + group: str | None = Field(default=None, max_length=80) + environment: str | None = Field(default=None, max_length=80) + owner: str | None = Field(default=None, max_length=120) + enabled: bool = True + monitoring_enabled: bool = True + monitoring_state: MonitoringState = MonitoringState.ACTIVE + probe_source: ProbeEndpointSource = ProbeEndpointSource.MANUAL + probe_reachability: ProbeReachability = ProbeReachability.VERIFIED + probe_confidence: DiscoveryConfidence = DiscoveryConfidence.CONFIRMED + runtime_state: str | None = Field(default=None, max_length=40) + runtime_health: str | None = Field(default=None, max_length=40) + application: str | None = Field(default=None, max_length=120) + domain: str | None = Field(default=None, max_length=80) + component_role: ComponentRole = ComponentRole.UNKNOWN + application_locked: bool = False + domain_locked: bool = False + component_role_locked: bool = False + criticality: Criticality = Criticality.MEDIUM + category: DiscoveryCategory = DiscoveryCategory.UNKNOWN + visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE + lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE + source_type: str | None = Field(default=None, max_length=40) + source_ref: str | None = Field(default=None, max_length=240) + first_seen_at: datetime | None = None + last_seen_at: datetime | None = None + last_discovery_status: str | None = Field(default=None, max_length=40) + container_image: str | None = Field(default=None, max_length=240) + compose_project: str | None = Field(default=None, max_length=120) + layout_x: float | None = None + layout_y: float | None = None + layout_pinned: bool = False + interval_seconds: int | None = Field(default=None, ge=1, le=86400) + timeout_seconds: float | None = Field(default=None, ge=0.1, le=60) + retries: int | None = Field(default=None, ge=0, le=5) + failure_threshold: int = Field(default=1, ge=1, le=20) + recovery_threshold: int = Field(default=1, ge=1, le=20) + expected_status_min: int | None = Field(default=None, ge=100, le=599) + expected_status_max: int | None = Field(default=None, ge=100, le=599) + availability_target: float | None = Field(default=None, ge=0, le=100) + maintenance_enabled: bool = False + maintenance_reason: str | None = Field(default=None, max_length=240) + + @model_validator(mode="after") + def validate_endpoint(self) -> ServiceCreate: + if not self.monitoring_enabled and not self.url and not self.host and self.port is None: + return self + if self.kind == ServiceKind.HTTP and not self.url: + raise ValueError("HTTP services require url") + if self.kind == ServiceKind.HTTP: + parsed = urlparse(self.url or "") + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise ValueError("HTTP services require an absolute http(s) url") + if self.host or self.port: + raise ValueError("HTTP services must use url instead of host/port") + if self.kind in {ServiceKind.TCP, ServiceKind.TLS} and (not self.host or self.port is None): + raise ValueError(f"{self.kind.value.upper()} services require host and port") + if self.kind == ServiceKind.DNS and not self.host: + raise ValueError("DNS services require hostname in host") + if self.kind in {ServiceKind.TCP, ServiceKind.TLS, ServiceKind.DNS} and self.url: + raise ValueError(f"{self.kind.value.upper()} services must use host/port instead of url") + if self.kind == ServiceKind.DNS and self.port is not None: + raise ValueError("DNS services use a hostname without a port") + if ( + self.expected_status_min is not None + and self.expected_status_max is not None + and self.expected_status_min > self.expected_status_max + ): + raise ValueError("expected_status_min cannot exceed expected_status_max") + return self + + +class ServiceUpdate(ServiceCreate): + pass + + +class Service(ServiceCreate): + id: str + created_at: datetime + updated_at: datetime + state: HealthStatus = HealthStatus.UNKNOWN + failure_streak: int = 0 + recovery_streak: int = 0 + flapping: bool = False + flap_count: int = 0 + last_state_change_at: datetime | None = None + + +class ProbeResult(BaseModel): + id: str + service_id: str + status: HealthStatus + latency_ms: float | None + detail: str + checked_at: datetime + trigger: str = "manual" + metadata: dict[str, object] = Field(default_factory=dict) + + +class HealthEvidence(BaseModel): + """An immutable-ish, provider-scoped claim used to resolve direct health. + + Application-scoped evidence is intentionally retained for provenance, but + cannot be promoted to a component's direct health without an explicit + component-level observation from that provider. + """ + + id: str + subject_type: str = Field(default="service", max_length=40) + subject_id: str = Field(max_length=240) + service_id: str | None = None + provider: EvidenceProvider + provider_ref: str | None = Field(default=None, max_length=240) + status: HealthStatus + scope: EvidenceScope = EvidenceScope.COMPONENT + confidence: float = Field(default=1.0, ge=0, le=1) + observed_at: datetime + valid_until: datetime + received_at: datetime + summary: str = Field(default="", max_length=500) + metadata: dict[str, object] = Field(default_factory=dict) + active: bool = True + superseded_by: str | None = None + freshness: EvidenceFreshness = EvidenceFreshness.FRESH + + +class HealthResolution(BaseModel): + status: HealthStatus = HealthStatus.UNKNOWN + selected_evidence_id: str | None = None + selected_provider: EvidenceProvider | None = None + confidence: float | None = Field(default=None, ge=0, le=1) + reason_code: HealthResolutionReason = HealthResolutionReason.NO_CURRENT_COMPONENT_EVIDENCE + explanation: str = "No current component-scoped health evidence is available." + conflict: bool = False + evidence: list[HealthEvidence] = Field(default_factory=list) + + +class ApplicationHealthResolution(BaseModel): + """Aggregate health answer that keeps application and component scopes separate.""" + + status: HealthStatus = HealthStatus.UNKNOWN + confidence: float | None = Field(default=None, ge=0, le=1) + explanation: str = "No current application or component health evidence is available." + reason_code: HealthResolutionReason = HealthResolutionReason.NO_APPLICATION_EVIDENCE + selected_evidence_id: str | None = None + selected_provider: EvidenceProvider | None = None + evidence_freshness: EvidenceFreshness | None = None + conflict: bool = False + resolved_components: int = 0 + unresolved_components: int = 0 + component_coverage_percent: float = 0 + + +class ProviderStatus(BaseModel): + provider: EvidenceProvider + status: ProviderHealth = ProviderHealth.UNKNOWN + observed_at: datetime + valid_until: datetime + detail: str = Field(default="", max_length=500) + metadata: dict[str, object] = Field(default_factory=dict) + + +class ServiceSnapshot(BaseModel): + service: Service + latest_probe: ProbeResult | None = None + availability_24h: AvailabilitySummary | None = None + dependency_status: HealthStatus = HealthStatus.UNKNOWN + effective_status: HealthStatus = HealthStatus.UNKNOWN + direct_status: HealthStatus = HealthStatus.UNKNOWN + flapping: bool = False + flap_count: int = 0 + next_probe_at: datetime | None = None + dependents: list[str] = Field(default_factory=list) + sources: list[ServiceSource] = Field(default_factory=list) + evidence: list[RelationEvidence] = Field(default_factory=list) + health_evidence: list[HealthEvidence] = Field(default_factory=list) + health_resolution: HealthResolution = Field(default_factory=HealthResolution) + + +class AvailabilitySummary(BaseModel): + hours: int = 24 + total_checks: int = 0 + healthy_checks: int = 0 + degraded_checks: int = 0 + down_checks: int = 0 + uptime_percent: float | None = None + average_latency_ms: float | None = None + last_checked_at: datetime | None = None + target_percent: float | None = None + meets_target: bool | None = None + + +class BulkProbeResult(BaseModel): + requested: int + completed: int + results: list[ProbeResult] + + +class IncidentCandidate(BaseModel): + service_id: str + service_name: str + status: HealthStatus + reason: str + affected_service_ids: list[str] = Field(default_factory=list) + affected_service_names: list[str] = Field(default_factory=list) + + +class IncidentRecord(BaseModel): + id: str + status: IncidentStatus + root_service_id: str + root_service_name: str + started_at: datetime + detected_at: datetime + acknowledged_at: datetime | None = None + resolved_at: datetime | None = None + title: str + root_cause_reason: str + affected_service_ids: list[str] = Field(default_factory=list) + affected_service_names: list[str] = Field(default_factory=list) + severity: Criticality = Criticality.MEDIUM + latest_update_at: datetime + + +class IncidentEvent(BaseModel): + id: str + incident_id: str + event_type: str + message: str + created_at: datetime + service_id: str | None = None + + +class EventRecord(BaseModel): + id: str + event_type: str + service_id: str | None = None + incident_id: str | None = None + from_status: HealthStatus | None = None + to_status: HealthStatus | None = None + message: str + metadata: dict[str, object] = Field(default_factory=dict) + created_at: datetime + + +class IncidentOverview(BaseModel): + generated_at: datetime + active: bool + candidates: list[IncidentCandidate] + degraded_service_ids: list[str] + down_service_ids: list[str] + incidents: list[IncidentRecord] = Field(default_factory=list) + + +class ServiceSource(BaseModel): + id: str + source_type: str + source_id: str + source_name: str | None = None + first_seen_at: datetime + last_seen_at: datetime + lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE + managed: bool = True + metadata: dict[str, object] = Field(default_factory=dict) + + +class RelationEvidence(BaseModel): + id: str + source_service_id: str + target_service_id: str | None = None + target_ref: str | None = None + relation_type: RelationType = RelationType.DEPENDS_ON + evidence_type: str + confidence: DiscoveryConfidence + explanation: str + source_ref: str | None = None + locked: bool = False + active: bool = True + first_seen_at: datetime + last_seen_at: datetime + + +class LogicalRelation(BaseModel): + """One canonical relationship with all independent supporting evidence.""" + + id: str + source_service_id: str + target_service_id: str | None = None + target_ref: str | None = None + relation_type: RelationType = RelationType.DEPENDS_ON + confidence: DiscoveryConfidence + active: bool = True + locked: bool = False + first_seen_at: datetime + last_seen_at: datetime + evidence: list[RelationEvidence] = Field(default_factory=list) + + +class SemanticComponent(BaseModel): + service_id: str + name: str + kind: ServiceKind + application_id: str + domain_id: str + role: ComponentRole = ComponentRole.UNKNOWN + direct_status: HealthStatus + effective_status: HealthStatus + unknown_reason: HealthResolutionReason | None = None + runtime_state: str | None = None + runtime_health: str | None = None + monitoring_state: MonitoringState + latest_latency_ms: float | None = None + dependency_ids: list[str] = Field(default_factory=list) + dependent_ids: list[str] = Field(default_factory=list) + root_cause: bool = False + affected: bool = False + + +class SemanticApplication(BaseModel): + id: str + name: str + domain_id: str + component_ids: list[str] = Field(default_factory=list) + component_count: int = 0 + roles: list[ComponentRole] = Field(default_factory=list) + status: HealthStatus + status_explanation: str + health_resolution: ApplicationHealthResolution = Field(default_factory=ApplicationHealthResolution) + dependency_application_ids: list[str] = Field(default_factory=list) + dependent_application_ids: list[str] = Field(default_factory=list) + monitored_components: int = 0 + unmonitored_components: int = 0 + resolved_components: int = 0 + unresolved_components: int = 0 + component_coverage_percent: float = 0 + root_cause: bool = False + affected: bool = False + + +class SemanticDomain(BaseModel): + id: str + name: str + application_ids: list[str] = Field(default_factory=list) + application_count: int = 0 + status: HealthStatus + resolved_application_count: int = 0 + unresolved_application_count: int = 0 + application_coverage_percent: float = 0 + incident_count: int = 0 + root_cause: bool = False + affected: bool = False + + +class SemanticTopology(BaseModel): + generated_at: datetime + domains: list[SemanticDomain] = Field(default_factory=list) + applications: list[SemanticApplication] = Field(default_factory=list) + components: list[SemanticComponent] = Field(default_factory=list) + relations: list[LogicalRelation] = Field(default_factory=list) + monitoring_coverage: dict[str, int] = Field(default_factory=dict) + + +class DiscoveryObservation(BaseModel): + id: str + run_id: str + source_id: str + source_type: str + source_name: str + lifecycle: DiscoveryLifecycle + change_type: str + observed_at: datetime + metadata: dict[str, object] = Field(default_factory=dict) + + +class TopologySuggestion(BaseModel): + id: str + run_id: str + suggestion_type: str + status: SuggestionStatus = SuggestionStatus.OPEN + source_ref: str | None = None + source_service_id: str | None = None + target_ref: str | None = None + target_service_id: str | None = None + proposed_name: str | None = None + proposed_kind: ServiceKind | None = None + proposed_url: str | None = None + proposed_host: str | None = None + proposed_port: int | None = None + proposed_category: DiscoveryCategory = DiscoveryCategory.UNKNOWN + proposed_group: str | None = None + relation_type: RelationType | None = None + confidence: DiscoveryConfidence = DiscoveryConfidence.LOW + evidence: list[str] = Field(default_factory=list) + created_at: datetime + updated_at: datetime + + +class DiscoveryDiff(BaseModel): + new: int = 0 + changed: int = 0 + missing: int = 0 + unchanged: int = 0 + stale: int = 0 + new_suggestions: int = 0 + + +class DiscoveryRun(BaseModel): + id: str + status: DiscoveryRunStatus + started_at: datetime + completed_at: datetime | None = None + source_counts: dict[str, int] = Field(default_factory=dict) + diff: DiscoveryDiff = Field(default_factory=DiscoveryDiff) + error: str | None = None + + +class TopologySnapshot(BaseModel): + """Compact, immutable capture of the canonical topology after a run.""" + + id: str + run_id: str | None = None + created_at: datetime + revision: str + service_count: int = 0 + relationship_count: int = 0 + summary: dict[str, int] = Field(default_factory=dict) + + +class TopologyChange(BaseModel): + change_type: str + entity_type: str + entity_id: str + name: str + before: dict[str, object] | None = None + after: dict[str, object] | None = None + + +class TopologyDiff(BaseModel): + before_snapshot_id: str | None = None + after_snapshot_id: str + added: list[TopologyChange] = Field(default_factory=list) + removed: list[TopologyChange] = Field(default_factory=list) + changed: list[TopologyChange] = Field(default_factory=list) + summary: dict[str, int] = Field(default_factory=dict) + + +class DiscoveryRunRequest(BaseModel): + docker_host: str | None = None + appops_url: str | None = None + include_stopped: bool = False + + +class DiscoveryReconcileRequest(BaseModel): + run_id: str | None = None + apply_high_confidence: bool = False + include_stopped: bool = False + + +class EdgeMutationRequest(BaseModel): + source_service_id: str + target_service_id: str + relation_type: RelationType = RelationType.DEPENDS_ON + lock: bool = True + explanation: str = Field(default="Manual topology relationship", max_length=500) + + +class EdgeActionRequest(BaseModel): + action: str = Field(pattern="^(confirm|reject|lock|remove)$") + + +class ConfigService(ServiceCreate): + pass + + +class ConfigDocument(BaseModel): + # Version 1 remains accepted. Version 2 is emitted when the document uses + # the richer service fields; the parser keeps the old empty v2 rejection + # compatibility used by the v1 contract tests. + version: int = Field(default=2, ge=1, le=2) + services: list[ConfigService] = Field(default_factory=list) + + +class ConfigImportRequest(BaseModel): + content: str + format: str = Field(pattern="^(yaml|json)$") + mode: str = Field(default="merge", pattern="^(merge|replace)$") + + +class ConfigImportResult(BaseModel): + created: int + updated: int + total: int + + +class DemoState(BaseModel): + active: bool + synthetic: bool = False + scenario_name: str | None = None + service_count: int = 0 + root_cause_service_id: str | None = None + root_cause_service_name: str | None = None + affected_service_ids: list[str] = Field(default_factory=list) + affected_service_names: list[str] = Field(default_factory=list) + affected_count: int = 0 + + +class DemoMutationResult(DemoState): + created: int = 0 + updated: int = 0 + total: int = 0 + restored: bool = False + + +class DockerDiscoveryRequest(BaseModel): + docker_host: str = "unix:///var/run/docker.sock" + + +class DockerPort(BaseModel): + container_port: str + host_ip: str | None = None + host_port: int | None = None + + +class DockerSuggestion(BaseModel): + name: str + kind: ServiceKind + host: str | None = None + port: int | None = None + url: str | None = None + group: str | None = None + evidence: list[str] = Field(default_factory=list) + confidence: str = "low" + depends_on_names: list[str] = Field(default_factory=list) + source_type: str | None = None + source_ref: str | None = None + stable_source_ref: str | None = None + category: DiscoveryCategory = DiscoveryCategory.UNKNOWN + visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE + relation_type: RelationType = RelationType.DEPENDS_ON + + +class ProbeEndpointCandidate(BaseModel): + """A reachable-or-reviewable endpoint discovered for a Docker workload.""" + + source: ProbeEndpointSource + kind: ServiceKind + url: str | None = None + host: str | None = None + port: int | None = Field(default=None, ge=1, le=65535) + reachability: ProbeReachability = ProbeReachability.UNVERIFIED + confidence: DiscoveryConfidence = DiscoveryConfidence.LOW + primary: bool = False + evidence: str = "" + preflight_detail: str | None = None + preflight_latency_ms: float | None = None + + +class DockerCandidate(BaseModel): + container_id: str + name: str + image: str + state: str + health: str | None = None + networks: list[str] + ports: list[DockerPort] + labels: dict[str, str] = Field(default_factory=dict) + import_state: str = "new" + suggestions: list[DockerSuggestion] = Field(default_factory=list) + dependency_suggestions: list[str] = Field(default_factory=list) + stable_source_ref: str | None = None + compose_project: str | None = None + compose_service: str | None = None + category: DiscoveryCategory = DiscoveryCategory.UNKNOWN + visibility: DiscoveryVisibility = DiscoveryVisibility.VISIBLE + lifecycle: DiscoveryLifecycle = DiscoveryLifecycle.ACTIVE + healthcheck_present: bool = False + environment_evidence: list[str] = Field(default_factory=list) + proxy_route_evidence: list[str] = Field(default_factory=list) + appops_app_id: str | None = None + appops_category: str | None = None + appops_criticality: str | None = None + appops_health: str | None = None + appops_scope: EvidenceScope | None = None + appops_discovery_confidence: float | None = Field(default=None, ge=0, le=1) + appops_policy_profile: str | None = None + appops_app_type: str | None = None + probe_candidates: list[ProbeEndpointCandidate] = Field(default_factory=list) + + +class DockerDiscoveryResult(BaseModel): + docker_host: str + candidates: list[DockerCandidate] + proxy_routes: int = 0 + appops_available: bool = False + appops_detail: str = "" + + +class DiscoveryStatus(BaseModel): + current: DiscoveryRun | None = None + last_completed: DiscoveryRun | None = None + active: bool = False + + +class DiscoveryResult(DockerDiscoveryResult): + run: DiscoveryRun + suggestions: list[TopologySuggestion] = Field(default_factory=list) + observations: list[DiscoveryObservation] = Field(default_factory=list) + appops_available: bool = False + appops_mappings: int = 0 + proxy_routes: int = 0 + + +class ReconcileResult(BaseModel): + run: DiscoveryRun + created: int = 0 + updated: int = 0 + unchanged: int = 0 + missing: int = 0 + retired: int = 0 + accepted_relations: int = 0 + open_suggestions: int = 0 + service_ids: list[str] = Field(default_factory=list) + + +class ProbeRepairChange(BaseModel): + service_id: str + service_name: str + previous_monitoring_state: MonitoringState + next_monitoring_state: MonitoringState + previous_endpoint: str | None = None + next_endpoint: str | None = None + next_probe_source: ProbeEndpointSource | None = None + next_probe_confidence: DiscoveryConfidence | None = None + reason: str + + +class ProbeRepairReport(BaseModel): + run_id: str | None = None + dry_run: bool = True + inspected: int = 0 + changed: int = 0 + disabled_unsafe: int = 0 + enabled_published: int = 0 + docker_health_observed: int = 0 + appops_observed: int = 0 + runtime_observed: int = 0 + unmonitored: int = 0 + resolved_incidents: int = 0 + changes: list[ProbeRepairChange] = Field(default_factory=list) + + +class OverviewSummary(BaseModel): + generated_at: datetime + total_services: int + health_counts: dict[str, int] + active_incidents: int + likely_root_causes: list[str] = Field(default_factory=list) + flapping_services: list[str] = Field(default_factory=list) + recent_events: list[EventRecord] = Field(default_factory=list) + monitoring_coverage: dict[str, int] = Field(default_factory=dict) + + +class MaintenanceRequest(BaseModel): + enabled: bool + reason: str | None = Field(default=None, max_length=240) diff --git a/backend/app/observation.py b/backend/app/observation.py new file mode 100644 index 0000000..e6b0740 --- /dev/null +++ b/backend/app/observation.py @@ -0,0 +1,206 @@ +"""Recurring, side-effect-bounded infrastructure health observation.""" + +from __future__ import annotations + +import os +import threading +from dataclasses import dataclass +from datetime import timedelta +from uuid import uuid4 + +from .docker_discovery import appops_inventory, observe_container_health +from .docker_provider import docker_provider_mode +from .evidence import appops_health_status, utc_now +from .models import ( + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthStatus, + ProviderHealth, + ProviderStatus, +) +from .store import Store + + +def observation_ttl(interval_seconds: int) -> timedelta: + """Provider observation TTL with room for one delayed refresh cycle.""" + + return timedelta(seconds=max(150, interval_seconds * 3)) + + +def _appops_status(value: str | None) -> HealthStatus | None: + return appops_health_status(value) + + +def _docker_status(state: str, health: str | None) -> HealthStatus | None: + if (health or "").casefold() in {"unhealthy", "down"} or state.casefold() in {"exited", "dead", "stopped"}: + return HealthStatus.DOWN + if (health or "").casefold() == "degraded" or state.casefold() == "restarting": + return HealthStatus.DEGRADED + if (health or "").casefold() == "healthy": + return HealthStatus.HEALTHY + return None + + +@dataclass(frozen=True) +class ObservationResult: + docker_observations: int = 0 + appops_observations: int = 0 + skipped: bool = False + + +class ObservationManager: + """Refresh provider evidence only; discovery and reconciliation stay manual.""" + + def __init__(self, store: Store, *, interval_seconds: int) -> None: + self.store = store + self.interval_seconds = interval_seconds + self._lock = threading.Lock() + + @staticmethod + def _docker_host() -> str: + return os.getenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock") + + @staticmethod + def _appops_url() -> str | None: + return os.getenv("SERVICEMAP_APPOPS_URL") + + def refresh(self) -> ObservationResult: + if self.store.demo_active() or not self._lock.acquire(blocking=False): + return ObservationResult(skipped=True) + try: + now = utc_now() + valid_until = now + observation_ttl(self.interval_seconds) + services_by_ref = { + service.source_ref: service + for service in self.store.list_services() + if service.source_ref + } + persisted_app_links = self.store.application_evidence_links(EvidenceProvider.APPOPS) + appops = appops_inventory(self._appops_url()) + if appops.available: + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.APPOPS, + status=ProviderHealth.HEALTHY, + observed_at=now, + valid_until=valid_until, + detail=appops.detail, + metadata={"mapping_count": len(appops.by_container)}, + ) + ) + else: + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.APPOPS, + status=ProviderHealth.UNAVAILABLE, + observed_at=now, + valid_until=valid_until, + detail=appops.detail, + ) + ) + + linked_apps: dict[str, set[str]] = { + app_id: set(service_ids) for app_id, service_ids in persisted_app_links.items() + } + docker_count = 0 + try: + docker_observations = observe_container_health(self._docker_host()) + if ( + docker_provider_mode() == "broker" + and not docker_observations + and any(ref.startswith("docker:") for ref in services_by_ref) + ): + raise ValueError("Docker broker returned no containers for configured services") + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.DOCKER, + status=ProviderHealth.HEALTHY, + observed_at=now, + valid_until=valid_until, + detail=f"{len(docker_observations)} containers observed", + metadata={"container_count": len(docker_observations)}, + ) + ) + for observation in docker_observations: + service = services_by_ref.get(observation.stable_source_ref) + if service is None: + continue + docker_count += 1 + appops_info = appops.by_container.get(observation.name.casefold()) + if appops_info: + linked_apps.setdefault(appops_info.app_id, set()).add(service.id) + status = _docker_status(observation.state, observation.health) + if status is None: + continue + self.store.record_health_evidence( + HealthEvidence( + id=str(uuid4()), + subject_type="service", + subject_id=service.id, + service_id=service.id, + provider=EvidenceProvider.DOCKER, + provider_ref=observation.stable_source_ref, + status=status, + scope=EvidenceScope.COMPONENT, + confidence=0.95 if observation.healthcheck_present else 0.8, + observed_at=now, + valid_until=valid_until, + received_at=now, + summary="Docker healthcheck" if observation.healthcheck_present else "Docker runtime state", + metadata={ + "container_id": observation.container_id, + "state": observation.state, + "health": observation.health, + }, + ) + ) + except (OSError, RuntimeError, ValueError) as exc: + self.store.set_provider_status( + ProviderStatus( + provider=EvidenceProvider.DOCKER, + status=ProviderHealth.UNAVAILABLE, + observed_at=now, + valid_until=valid_until, + detail=str(exc)[:500], + ) + ) + + appops_count = 0 + if appops.available: + for app_id, service_ids in linked_apps.items(): + info = next( + (item for item in appops.by_container.values() if item.app_id == app_id), + None, + ) + status = _appops_status(info.health if info else None) + if info is None or status is None or not service_ids: + continue + appops_count += 1 + self.store.record_health_evidence( + HealthEvidence( + id=str(uuid4()), + subject_type="application", + subject_id=f"appops:{app_id}", + provider=EvidenceProvider.APPOPS, + provider_ref=app_id, + status=status, + scope=EvidenceScope.APPLICATION, + confidence=info.discovery_confidence if info.discovery_confidence is not None else 0.5, + observed_at=now, + valid_until=valid_until, + received_at=now, + summary="AppOps application health; never used as component direct health", + metadata={ + "app_id": app_id, + "category": info.category, + "policy_profile": info.policy_profile, + "app_type": info.app_type, + "discovery_confidence": info.discovery_confidence, + }, + ), + service_ids=sorted(service_ids), + ) + return ObservationResult(docker_observations=docker_count, appops_observations=appops_count) + finally: + self._lock.release() diff --git a/backend/app/probes.py b/backend/app/probes.py new file mode 100644 index 0000000..b216b6a --- /dev/null +++ b/backend/app/probes.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +import asyncio +import contextvars +import os +import socket +import ssl +from datetime import UTC, datetime +from time import perf_counter +from uuid import uuid4 + +import httpx + +from .models import HealthStatus, ProbeResult, Service, ServiceKind + +_probe_metadata: contextvars.ContextVar[dict[str, object] | None] = contextvars.ContextVar( + "servicemap_probe_metadata", default=None +) +_service_locks: dict[str, asyncio.Lock] = {} + + +def _lock_for(service_id: str) -> asyncio.Lock: + lock = _service_locks.get(service_id) + if lock is None: + lock = asyncio.Lock() + _service_locks[service_id] = lock + return lock + + +def _http_status(status_code: int, service: Service) -> HealthStatus: + minimum = service.expected_status_min + maximum = service.expected_status_max + if minimum is not None or maximum is not None: + if (minimum is not None and status_code < minimum) or (maximum is not None and status_code > maximum): + return HealthStatus.DOWN if status_code >= 500 else HealthStatus.DEGRADED + return HealthStatus.HEALTHY + if status_code < 400: + return HealthStatus.HEALTHY + if status_code < 500: + return HealthStatus.DEGRADED + return HealthStatus.DOWN + + +async def _probe_once_with_metadata(service: Service, timeout_seconds: float) -> tuple[HealthStatus, str, dict[str, object]]: + if service.kind == ServiceKind.HTTP: + async with httpx.AsyncClient(timeout=timeout_seconds, follow_redirects=True) as client: + response = await client.get(service.url or "") + status = _http_status(response.status_code, service) + return status, f"HTTP {response.status_code}", {"status_code": response.status_code} + + if service.kind == ServiceKind.DNS: + started = perf_counter() + infos = await asyncio.wait_for( + asyncio.get_running_loop().getaddrinfo(service.host, None, type=socket.SOCK_STREAM), + timeout=timeout_seconds, + ) + addresses = sorted({str(info[4][0]) for info in infos if info[4]}) + if not addresses: + return HealthStatus.DOWN, f"DNS {service.host} returned no addresses", {"addresses": []} + return ( + HealthStatus.HEALTHY, + f"DNS {service.host} resolved ({round((perf_counter() - started) * 1000, 2)} ms)", + {"addresses": addresses[:20]}, + ) + + if service.kind == ServiceKind.TLS: + context = ssl.create_default_context() + reader, writer = await asyncio.wait_for( + asyncio.open_connection(service.host, service.port, ssl=context, server_hostname=service.host), + timeout=timeout_seconds, + ) + del reader + ssl_object = writer.get_extra_info("ssl_object") + metadata: dict[str, object] = {"hostname": service.host, "port": service.port} + detail = f"TLS {service.host}:{service.port} certificate reachable" + status = HealthStatus.HEALTHY + if ssl_object is not None: + certificate = ssl_object.getpeercert() or {} + not_after = certificate.get("notAfter") if isinstance(certificate, dict) else None + if not_after: + expires_at = datetime.fromtimestamp(ssl.cert_time_to_seconds(not_after), UTC) + days_remaining = (expires_at - datetime.now(UTC)).total_seconds() / 86400 + metadata.update({"expires_at": expires_at.isoformat(), "days_remaining": round(days_remaining, 2)}) + warning_days = max(1, int(os.getenv("SERVICEMAP_TLS_WARNING_DAYS", "30"))) + if days_remaining <= 0: + status = HealthStatus.DOWN + detail = f"TLS certificate expired on {expires_at.date().isoformat()}" + elif days_remaining <= warning_days: + status = HealthStatus.DEGRADED + detail = f"TLS certificate expires in {round(days_remaining, 1)} days" + writer.close() + await writer.wait_closed() + return status, detail, metadata + + _, writer = await asyncio.wait_for( + asyncio.open_connection(service.host, service.port), + timeout=timeout_seconds, + ) + writer.close() + await writer.wait_closed() + return HealthStatus.HEALTHY, f"TCP {service.host}:{service.port} reachable", {} + + +async def _probe_once(service: Service, timeout_seconds: float) -> tuple[HealthStatus, str]: + status, detail, metadata = await _probe_once_with_metadata(service, timeout_seconds) + _probe_metadata.set(metadata) + return status, detail + + +async def probe_service( + service: Service, + timeout_seconds: float | None = 5.0, + retries: int | None = 1, + *, + trigger: str = "manual", + default_timeout_seconds: float = 5.0, + default_retries: int = 1, +) -> ProbeResult: + if not service.enabled or not service.monitoring_enabled: + return ProbeResult( + id=str(uuid4()), + service_id=service.id, + status=HealthStatus.UNKNOWN, + latency_ms=None, + detail="Monitoring disabled for service", + checked_at=datetime.now(UTC), + trigger=trigger, + ) + timeout = service.timeout_seconds if timeout_seconds is None else timeout_seconds + timeout = timeout if timeout is not None else default_timeout_seconds + retry_count = service.retries if retries is None else retries + retry_count = retry_count if retry_count is not None else default_retries + attempts = max(1, retry_count + 1) + status = HealthStatus.DOWN + detail = "Probe failed" + latency_ms: float | None = None + metadata: dict[str, object] = {} + + async with _lock_for(service.id): + for attempt in range(1, attempts + 1): + _probe_metadata.set({}) + started = perf_counter() + try: + status, detail = await _probe_once(service, timeout) + metadata = _probe_metadata.get() or {} + latency_ms = round((perf_counter() - started) * 1000, 2) + if attempt > 1: + detail = f"{detail} after {attempt} attempts" + break + except (TimeoutError, httpx.HTTPError, OSError, ssl.SSLError, socket.gaierror) as exc: + latency_ms = round((perf_counter() - started) * 1000, 2) + detail = f"{type(exc).__name__}: {exc}" + if isinstance(exc, ssl.CertificateError): + status = HealthStatus.DOWN + detail = f"TLS hostname mismatch: {exc}" + if attempt < attempts: + await asyncio.sleep(min(0.2 * attempt, 1.0)) + + return ProbeResult( + id=str(uuid4()), + service_id=service.id, + status=status, + latency_ms=latency_ms, + detail=detail[:500], + checked_at=datetime.now(UTC), + trigger=trigger, + metadata=metadata, + ) + + +async def probe_many( + services: list[Service], + *, + timeout_seconds: float | None = 5.0, + retries: int | None = 1, + concurrency: int = 10, + trigger: str = "manual", + default_timeout_seconds: float = 5.0, + default_retries: int = 1, +) -> list[ProbeResult]: + semaphore = asyncio.Semaphore(max(1, concurrency)) + + async def run(service: Service) -> ProbeResult: + async with semaphore: + if trigger == "manual" and default_timeout_seconds == 5.0 and default_retries == 1: + return await probe_service(service, timeout_seconds=timeout_seconds, retries=retries) + if trigger == "manual": + return await probe_service( + service, + timeout_seconds=timeout_seconds, + retries=retries, + default_timeout_seconds=default_timeout_seconds, + default_retries=default_retries, + ) + return await probe_service( + service, + timeout_seconds=timeout_seconds, + retries=retries, + trigger=trigger, + default_timeout_seconds=default_timeout_seconds, + default_retries=default_retries, + ) + + return list(await asyncio.gather(*(run(service) for service in services))) diff --git a/backend/app/scheduler.py b/backend/app/scheduler.py new file mode 100644 index 0000000..9a963f4 --- /dev/null +++ b/backend/app/scheduler.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import asyncio +import logging +from time import monotonic + +from .probes import probe_many +from .store import Store + +logger = logging.getLogger(__name__) + + +async def scheduled_probe_loop( + store: Store, + *, + interval_seconds: int, + timeout_seconds: float, + retries: int, + concurrency: int, + retention_days: int, +) -> None: + next_due: dict[str, float] = {} + while True: + sleep_seconds = interval_seconds + try: + # The Explore demo is deliberately synthetic. Pausing scheduled + # probes while it is active keeps its seeded incident and history + # deterministic and avoids any dependency on external services. + if not getattr(store, "demo_active", lambda: False)(): + services = store.list_services() + now = monotonic() + due = [ + service + for service in services + if service.enabled + and service.monitoring_enabled + and now >= next_due.get(service.id, 0) + ] + if due: + results = await probe_many( + due, + timeout_seconds=None, + retries=None, + concurrency=concurrency, + trigger="scheduled", + default_timeout_seconds=timeout_seconds, + default_retries=retries, + ) + for result in results: + store.save_probe(result) + for service in due: + next_due[service.id] = now + (service.interval_seconds or interval_seconds) + next_intervals = [ + service.interval_seconds or interval_seconds + for service in services + if service.enabled and service.monitoring_enabled + ] + sleep_seconds = min([interval_seconds, *next_intervals]) if next_intervals else interval_seconds + else: + sleep_seconds = interval_seconds + store.prune_probes(retention_days) + except asyncio.CancelledError: + raise + except Exception: # pragma: no cover - defensive daemon boundary + logger.exception("Scheduled probe cycle failed") + + await asyncio.sleep(max(1, sleep_seconds)) diff --git a/backend/app/semantic.py b/backend/app/semantic.py new file mode 100644 index 0000000..f2fc4da --- /dev/null +++ b/backend/app/semantic.py @@ -0,0 +1,429 @@ +"""Deterministic semantic topology derived from the canonical service graph. + +The discovery store remains the source of truth. This module deliberately +does not persist a second topology: a domain, application and component view +can therefore evolve without risking probe history or an operator's graph. +""" + +from __future__ import annotations + +import hashlib +import re +from collections import defaultdict +from datetime import UTC, datetime + +from .evidence import with_freshness +from .models import ( + ApplicationHealthResolution, + ComponentRole, + Criticality, + EvidenceFreshness, + EvidenceScope, + HealthResolutionReason, + HealthStatus, + LogicalRelation, + SemanticApplication, + SemanticComponent, + SemanticDomain, + SemanticTopology, + Service, + ServiceSnapshot, +) + +_ROLE_BY_CATEGORY = { + "database": ComponentRole.DATABASE, + "cache": ComponentRole.CACHE, + "proxy": ComponentRole.PROXY, + "background_worker": ComponentRole.WORKER, + "storage": ComponentRole.STORAGE, + "monitoring": ComponentRole.MONITORING, +} +_ROLE_HINTS = ( + (ComponentRole.DATABASE, ("postgres", "mariadb", "mysql", "mongo", "influx", "database", "db")), + (ComponentRole.CACHE, ("redis", "memcache", "cache")), + (ComponentRole.PROXY, ("proxy", "nginx", "traefik", "caddy", "swag", "haproxy", "cloudflare")), + (ComponentRole.WORKER, ("worker", "runner", "queue", "task")), + (ComponentRole.STORAGE, ("minio", "storage", "s3", "volume", "files")), + (ComponentRole.MONITORING, ("grafana", "prometheus", "zabbix", "netdata", "loki", "uptime")), + (ComponentRole.AGENT, ("agent", "exporter", "sidecar")), + (ComponentRole.SCHEDULER, ("scheduler", "cron", "schedule")), + (ComponentRole.FRONTEND, ("frontend", "webui", "dashboard", "ui")), + (ComponentRole.API, ("api", "backend", "server")), +) +_DOMAIN_HINTS = ( + ("Edge", ("proxy", "nginx", "traefik", "caddy", "swag", "cloudflare", "authelia", "auth")), + ("Media", ("plex", "jellyfin", "sonarr", "radarr", "lidarr", "readarr", "bazarr", "media", "tautulli")), + ("Dev", ("gitea", "git", "code", "dev", "ci", "runner")), + ("Monitoring", ("grafana", "prometheus", "zabbix", "netdata", "loki", "uptime", "monitor")), + ("AI", ("ollama", "open webui", "comfy", "ai", "llm")), + ("Storage", ("postgres", "mariadb", "mysql", "mongo", "redis", "minio", "storage", "nextcloud", "file")), + ("Automation", ("n8n", "worker", "scheduler", "cron", "automation", "arr")), + ("Productivity", ("book", "paperless", "vault", "wiki", "project", "calendar", "office")), +) +def _slug(prefix: str, value: str) -> str: + normalized = re.sub(r"[^a-z0-9]+", "-", value.casefold()).strip("-") or "unknown" + digest = hashlib.sha1(value.casefold().encode("utf-8")).hexdigest()[:8] + return f"{prefix}:{normalized[:48]}-{digest}" + + +def component_role(service: Service) -> ComponentRole: + if service.component_role_locked or service.component_role != ComponentRole.UNKNOWN: + return service.component_role + category_role = _ROLE_BY_CATEGORY.get(service.category.value) + if category_role: + return category_role + haystack = " ".join(filter(None, (service.name, service.container_image, service.source_ref, service.description))).casefold() + for role, hints in _ROLE_HINTS: + if any(hint in haystack for hint in hints): + return role + # A monitoring gap says nothing about the component's workload role. Keep + # it unknown until source metadata, a deterministic hint or an operator + # override supplies evidence. + return ComponentRole.UNKNOWN + + +def application_name(service: Service) -> str: + """Use stable provenance before falling back to a standalone application. + + Category is intentionally *not* used as a shared application name. Doing + so was the old "Independent · Category" compaction bug: unrelated services + looked like one stack. Categories still inform domain derivation below. + """ + + if service.application: + return service.application.strip() + if service.compose_project: + return service.compose_project.replace("_", " ").replace("-", " ").title() + ref = service.source_ref or "" + compose = re.match(r"docker:compose:([^:]+):", ref, flags=re.IGNORECASE) + if compose: + return compose.group(1).replace("_", " ").replace("-", " ").title() + name = re.sub(r"(?:[-_ ](?:server|web|api|db|database|redis|worker|frontend|backend))$", "", service.name, flags=re.IGNORECASE).strip() + return name or service.name + + +def application_identity(service: Service) -> tuple[str, str]: + """Return a stable grouping key plus its operator-facing display name. + + Compose provenance wins over a generic display label. This keeps two + unrelated ``PostgreSQL`` containers from being merged merely because an + aggregate provider happens to use the same name for both applications. + Explicit operator grouping remains authoritative. + """ + + name = application_name(service) + if service.application_locked and service.application: + return f"operator:{service.application.casefold().strip()}", name + if service.compose_project: + return f"compose:{service.compose_project.casefold().strip()}", name + ref = service.source_ref or "" + compose = re.match(r"docker:compose:([^:]+):", ref, flags=re.IGNORECASE) + if compose: + return f"compose:{compose.group(1).casefold()}", name + if ref: + return f"source:{ref.casefold()}", name + if service.application: + return f"application:{service.application.casefold().strip()}", name + return f"service:{service.id}", name + + +def domain_name(service: Service, role: ComponentRole, application: str) -> str: + if service.domain: + return service.domain.strip() + haystack = " ".join(filter(None, (application, service.name, service.container_image, service.category.value))).casefold() + for domain, hints in _DOMAIN_HINTS: + if any(hint in haystack for hint in hints): + return domain + if role in {ComponentRole.DATABASE, ComponentRole.CACHE, ComponentRole.STORAGE}: + return "Storage" + if role in {ComponentRole.PROXY}: + return "Edge" + if role in {ComponentRole.MONITORING, ComponentRole.AGENT}: + return "Monitoring" + return "Infrastructure" if service.category.value in {"infrastructure", "system"} else "Productivity" + + +def _application_resolution( + members: list[ServiceSnapshot], roles: dict[str, ComponentRole] +) -> ApplicationHealthResolution: + """Resolve application health without promoting aggregate evidence to a component. + + A fresh, high-confidence application observation can answer the health of + its application even where individual components are intentionally not + probed. It can never change a component's direct status. Conversely a + concrete component failure always outweighs a weak aggregate healthy + claim, which preserves conflict visibility instead of masking incidents. + """ + + resolved = [ + item + for item in members + if item.direct_status not in {HealthStatus.UNKNOWN, HealthStatus.UNMONITORED} + ] + unresolved = len(members) - len(resolved) + coverage = round((len(resolved) / len(members)) * 100, 1) if members else 0.0 + application_evidence = { + item.id: with_freshness(item) + for snapshot in members + for item in snapshot.health_evidence + if item.active and item.scope == EvidenceScope.APPLICATION + } + fresh_claims = [ + item + for item in application_evidence.values() + if item.freshness == EvidenceFreshness.FRESH + ] + selected = max( + fresh_claims, + key=lambda item: (item.confidence, item.observed_at, item.received_at, item.id), + default=None, + ) + + direct_down = [item for item in members if item.direct_status == HealthStatus.DOWN] + impacted = [ + item + for item in members + if item.effective_status in {HealthStatus.DOWN, HealthStatus.DEGRADED, HealthStatus.RECOVERING} + and item.direct_status != HealthStatus.DOWN + ] + conflicting_aggregate = bool( + selected + and ( + (selected.status == HealthStatus.HEALTHY and direct_down) + or (selected.status in {HealthStatus.DOWN, HealthStatus.DEGRADED} and resolved) + ) + ) + common = { + "selected_evidence_id": selected.id if selected else None, + "selected_provider": selected.provider if selected else None, + "evidence_freshness": selected.freshness if selected else None, + "conflict": conflicting_aggregate, + "resolved_components": len(resolved), + "unresolved_components": unresolved, + "component_coverage_percent": coverage, + } + if direct_down: + primary = [ + item + for item in direct_down + if item.service.criticality == Criticality.CRITICAL + or roles[item.service.id] in {ComponentRole.FRONTEND, ComponentRole.API, ComponentRole.PROXY} + ] + status = HealthStatus.DOWN if len(members) == 1 or len(direct_down) == len(resolved) or primary else HealthStatus.DEGRADED + names = ", ".join(item.service.name for item in direct_down[:3]) + return ApplicationHealthResolution( + status=status, + confidence=max((item.health_resolution.confidence or 1.0 for item in direct_down), default=None), + explanation=f"{names} has current component failure evidence; aggregate health does not override it.", + reason_code=HealthResolutionReason.COMPONENT_FAILURE, + **common, + ) + if impacted: + names = ", ".join(item.service.name for item in impacted[:3]) + return ApplicationHealthResolution( + status=HealthStatus.DEGRADED, + confidence=selected.confidence if selected else None, + explanation=f"{names} is impacted through a dependency health signal.", + reason_code=HealthResolutionReason.COMPONENT_IMPACT, + **common, + ) + if selected and selected.confidence >= 0.7 and selected.status not in {HealthStatus.UNKNOWN, HealthStatus.UNMONITORED}: + return ApplicationHealthResolution( + status=selected.status, + confidence=selected.confidence, + explanation=( + f"{selected.provider.value} application observation selected " + f"(confidence {selected.confidence:.2f}); component coverage is {coverage:.0f}% ." + ), + reason_code=HealthResolutionReason.SELECTED_APPLICATION_EVIDENCE, + **common, + ) + if resolved: + status = HealthStatus.HEALTHY + if any(item.direct_status in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} for item in resolved): + status = HealthStatus.DEGRADED + return ApplicationHealthResolution( + status=status, + explanation=f"{len(resolved)} of {len(members)} components have current direct health evidence.", + reason_code=HealthResolutionReason.COMPONENT_COVERAGE, + **common, + ) + stale = list(application_evidence.values()) + if stale: + reason = ( + HealthResolutionReason.STALE_EVIDENCE + if any(item.freshness == EvidenceFreshness.STALE for item in stale) + else HealthResolutionReason.EXPIRED_EVIDENCE + ) + return ApplicationHealthResolution( + status=HealthStatus.UNKNOWN, + explanation="Application evidence is no longer current; waiting for a provider refresh.", + reason_code=reason, + **common, + ) + if members and all(item.direct_status == HealthStatus.UNMONITORED for item in members): + return ApplicationHealthResolution( + status=HealthStatus.UNMONITORED, + explanation="No active health signal is configured for this application.", + reason_code=HealthResolutionReason.NO_APPLICATION_EVIDENCE, + **common, + ) + return ApplicationHealthResolution( + status=HealthStatus.UNKNOWN, + explanation="No current application or component health evidence is available.", + reason_code=HealthResolutionReason.NO_APPLICATION_EVIDENCE, + **common, + ) + + +def build_semantic_topology( + snapshots: list[ServiceSnapshot], + relations: list[LogicalRelation], + root_service_ids: set[str] | None = None, + affected_service_ids: set[str] | None = None, +) -> SemanticTopology: + root_service_ids = root_service_ids or set() + affected_service_ids = affected_service_ids or set() + snapshot_by_id = {item.service.id: item for item in snapshots} + roles = {item.service.id: component_role(item.service) for item in snapshots} + apps_by_key: dict[str, list[ServiceSnapshot]] = defaultdict(list) + app_names: dict[str, str] = {} + app_domain_names: dict[str, str] = {} + component_app_ids: dict[str, str] = {} + components: list[SemanticComponent] = [] + dependent_ids: dict[str, list[str]] = defaultdict(list) + for snapshot in snapshots: + for dependency_id in snapshot.service.depends_on: + dependent_ids[dependency_id].append(snapshot.service.id) + for snapshot in snapshots: + application_key, application = application_identity(snapshot.service) + apps_by_key[application_key].append(snapshot) + app_names[application_key] = application + role = roles[snapshot.service.id] + app_domain_names.setdefault(application_key, domain_name(snapshot.service, role, application)) + component_app_ids[snapshot.service.id] = _slug("app", application_key) + + applications: list[SemanticApplication] = [] + application_by_id: dict[str, SemanticApplication] = {} + domain_name_by_id: dict[str, str] = {} + for application_key, members in sorted(apps_by_key.items(), key=lambda item: app_names[item[0]].casefold()): + name = app_names[application_key] + app_id = _slug("app", application_key) + domain = app_domain_names[application_key] + domain_id = _slug("domain", domain) + domain_name_by_id[domain_id] = domain + resolution = _application_resolution(members, roles) + unmonitored = sum(item.direct_status == HealthStatus.UNMONITORED for item in members) + app = SemanticApplication( + id=app_id, + name=name, + domain_id=domain_id, + component_ids=[item.service.id for item in members], + component_count=len(members), + roles=sorted({roles[item.service.id] for item in members}, key=lambda role: role.value), + status=resolution.status, + status_explanation=resolution.explanation, + health_resolution=resolution, + monitored_components=resolution.resolved_components, + unmonitored_components=unmonitored, + resolved_components=resolution.resolved_components, + unresolved_components=resolution.unresolved_components, + component_coverage_percent=resolution.component_coverage_percent, + root_cause=any(item.service.id in root_service_ids for item in members), + affected=any(item.service.id in affected_service_ids for item in members), + ) + applications.append(app) + application_by_id[app_id] = app + + for snapshot in snapshots: + app_id = component_app_ids[snapshot.service.id] + app = application_by_id[app_id] + components.append(SemanticComponent( + service_id=snapshot.service.id, + name=snapshot.service.name, + kind=snapshot.service.kind, + application_id=app_id, + domain_id=app.domain_id, + role=roles[snapshot.service.id], + direct_status=snapshot.direct_status, + effective_status=snapshot.effective_status, + unknown_reason=( + snapshot.health_resolution.reason_code + if snapshot.direct_status == HealthStatus.UNKNOWN + else None + ), + runtime_state=snapshot.service.runtime_state, + runtime_health=snapshot.service.runtime_health, + monitoring_state=snapshot.service.monitoring_state, + latest_latency_ms=snapshot.latest_probe.latency_ms if snapshot.latest_probe else None, + dependency_ids=[item for item in snapshot.service.depends_on if item in snapshot_by_id], + dependent_ids=dependent_ids.get(snapshot.service.id, []), + root_cause=snapshot.service.id in root_service_ids, + affected=snapshot.service.id in affected_service_ids, + )) + + app_dependencies: dict[str, set[str]] = defaultdict(set) + app_dependents: dict[str, set[str]] = defaultdict(set) + for relation in relations: + if not relation.active or not relation.target_service_id: + continue + source_app = component_app_ids.get(relation.source_service_id) + target_app = component_app_ids.get(relation.target_service_id) + if source_app and target_app and source_app != target_app: + app_dependencies[source_app].add(target_app) + app_dependents[target_app].add(source_app) + for app in applications: + app.dependency_application_ids = sorted(app_dependencies[app.id]) + app.dependent_application_ids = sorted(app_dependents[app.id]) + + apps_by_domain: dict[str, list[SemanticApplication]] = defaultdict(list) + domain_names: dict[str, str] = {} + for app in applications: + apps_by_domain[app.domain_id].append(app) + domain_names[app.domain_id] = domain_name_by_id[app.domain_id] + domains: list[SemanticDomain] = [] + for domain_id, members in sorted(apps_by_domain.items(), key=lambda item: domain_names[item[0]].casefold()): + down = [item for item in members if item.status == HealthStatus.DOWN] + degraded = [ + item for item in members if item.status in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} + ] + healthy = [item for item in members if item.status == HealthStatus.HEALTHY] + resolved = [ + item + for item in members + if item.status not in {HealthStatus.UNKNOWN, HealthStatus.UNMONITORED} + ] + if down: + status = HealthStatus.DOWN + elif degraded: + status = HealthStatus.DEGRADED + elif healthy: + status = HealthStatus.HEALTHY + elif members and all(item.status == HealthStatus.UNMONITORED for item in members): + status = HealthStatus.UNMONITORED + else: + status = HealthStatus.UNKNOWN + domains.append(SemanticDomain( + id=domain_id, + name=domain_names[domain_id], + application_ids=[item.id for item in members], + application_count=len(members), + status=status, + resolved_application_count=len(resolved), + unresolved_application_count=len(members) - len(resolved), + application_coverage_percent=round((len(resolved) / len(members)) * 100, 1) if members else 0, + incident_count=sum(item.root_cause for item in members), + root_cause=any(item.root_cause for item in members), + affected=any(item.affected for item in members), + )) + coverage: dict[str, int] = defaultdict(int) + for snapshot in snapshots: + coverage[snapshot.service.monitoring_state.value] += 1 + return SemanticTopology( + generated_at=datetime.now(UTC), + domains=domains, + applications=applications, + components=components, + relations=relations, + monitoring_coverage=dict(coverage), + ) diff --git a/backend/app/store.py b/backend/app/store.py new file mode 100644 index 0000000..718fd65 --- /dev/null +++ b/backend/app/store.py @@ -0,0 +1,2012 @@ +from __future__ import annotations + +import json +import os +import sqlite3 +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Any +from uuid import uuid4 + +from .events import event_bus +from .evidence import resolve_health, with_freshness +from .models import ( + AvailabilitySummary, + ConfigImportResult, + ConfigService, + EventRecord, + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthResolution, + HealthStatus, + IncidentEvent, + IncidentRecord, + IncidentStatus, + MonitoringState, + ProbeResult, + ProviderStatus, + Service, + ServiceCreate, + ServiceUpdate, +) + +SERVICE_COLUMNS = ( + "id, name, kind, url, host, port, depends_on, description, tags, \"group\", " + "environment, owner, enabled, monitoring_enabled, monitoring_state, probe_source, probe_reachability, probe_confidence, " + "runtime_state, runtime_health, application, domain, component_role, application_locked, domain_locked, component_role_locked, criticality, category, visibility, lifecycle, " + "source_type, source_ref, first_seen_at, last_seen_at, last_discovery_status, container_image, " + "compose_project, layout_x, layout_y, layout_pinned, " + "interval_seconds, timeout_seconds, retries, failure_threshold, recovery_threshold, " + "expected_status_min, expected_status_max, availability_target, maintenance_enabled, " + "maintenance_reason, state, failure_streak, recovery_streak, flapping, flap_count, " + "last_state_change_at, created_at, updated_at" +) + + +def _now() -> datetime: + return datetime.now(UTC) + + +def _iso(value: datetime | None) -> str | None: + return value.isoformat() if value is not None else None + + +def _bool(value: object, default: bool = False) -> bool: + if value is None: + return default + return bool(int(value)) if isinstance(value, (int, str)) and str(value).isdigit() else bool(value) + + +class Store: + def __init__(self, db_path: str | None = None) -> None: + raw_path = db_path or os.getenv("SERVICEMAP_DB", "data/servicemap.db") + self.path = Path(raw_path) + self.path.parent.mkdir(parents=True, exist_ok=True) + self._init_schema() + + def _connect(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path, timeout=5.0) + connection.row_factory = sqlite3.Row + connection.execute("PRAGMA foreign_keys = ON") + connection.execute("PRAGMA busy_timeout = 5000") + return connection + + def _init_schema(self) -> None: + with self._connect() as db: + db.execute("PRAGMA journal_mode = WAL") + db.execute("PRAGMA synchronous = NORMAL") + db.executescript( + """ + CREATE TABLE IF NOT EXISTS services ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + kind TEXT NOT NULL, + url TEXT, + host TEXT, + port INTEGER, + depends_on TEXT NOT NULL DEFAULT '[]', + description TEXT, + tags TEXT NOT NULL DEFAULT '[]', + "group" TEXT, + environment TEXT, + owner TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + monitoring_enabled INTEGER NOT NULL DEFAULT 1, + monitoring_state TEXT NOT NULL DEFAULT 'active_probe', + probe_source TEXT NOT NULL DEFAULT 'manual', + probe_reachability TEXT NOT NULL DEFAULT 'verified', + probe_confidence TEXT NOT NULL DEFAULT 'confirmed', + runtime_state TEXT, + runtime_health TEXT, + application TEXT, + domain TEXT, + component_role TEXT NOT NULL DEFAULT 'unknown', + application_locked INTEGER NOT NULL DEFAULT 0, + domain_locked INTEGER NOT NULL DEFAULT 0, + component_role_locked INTEGER NOT NULL DEFAULT 0, + criticality TEXT NOT NULL DEFAULT 'medium', + category TEXT NOT NULL DEFAULT 'unknown', + visibility TEXT NOT NULL DEFAULT 'visible', + lifecycle TEXT NOT NULL DEFAULT 'active', + source_type TEXT, + source_ref TEXT, + first_seen_at TEXT, + last_seen_at TEXT, + last_discovery_status TEXT, + container_image TEXT, + compose_project TEXT, + layout_x REAL, + layout_y REAL, + layout_pinned INTEGER NOT NULL DEFAULT 0, + interval_seconds INTEGER, + timeout_seconds REAL, + retries INTEGER, + failure_threshold INTEGER NOT NULL DEFAULT 1, + recovery_threshold INTEGER NOT NULL DEFAULT 1, + expected_status_min INTEGER, + expected_status_max INTEGER, + availability_target REAL, + maintenance_enabled INTEGER NOT NULL DEFAULT 0, + maintenance_reason TEXT, + state TEXT NOT NULL DEFAULT 'unknown', + failure_streak INTEGER NOT NULL DEFAULT 0, + recovery_streak INTEGER NOT NULL DEFAULT 0, + flapping INTEGER NOT NULL DEFAULT 0, + flap_count INTEGER NOT NULL DEFAULT 0, + last_state_change_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT + ); + + CREATE TABLE IF NOT EXISTS probe_results ( + id TEXT PRIMARY KEY, + service_id TEXT NOT NULL, + status TEXT NOT NULL, + latency_ms REAL, + detail TEXT NOT NULL, + checked_at TEXT NOT NULL, + trigger TEXT NOT NULL DEFAULT 'manual', + metadata TEXT NOT NULL DEFAULT '{}', + FOREIGN KEY(service_id) REFERENCES services(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_probe_service_time + ON probe_results(service_id, checked_at DESC); + CREATE INDEX IF NOT EXISTS idx_probe_time + ON probe_results(checked_at DESC); + + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + event_type TEXT NOT NULL, + service_id TEXT, + incident_id TEXT, + from_status TEXT, + to_status TEXT, + message TEXT NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_events_time ON events(created_at DESC); + CREATE INDEX IF NOT EXISTS idx_events_service_time ON events(service_id, created_at DESC); + CREATE INDEX IF NOT EXISTS idx_events_incident_time ON events(incident_id, created_at ASC); + + CREATE TABLE IF NOT EXISTS incidents ( + id TEXT PRIMARY KEY, + status TEXT NOT NULL, + root_service_id TEXT NOT NULL, + started_at TEXT NOT NULL, + detected_at TEXT NOT NULL, + acknowledged_at TEXT, + resolved_at TEXT, + title TEXT NOT NULL, + root_cause_reason TEXT NOT NULL, + affected_service_ids TEXT NOT NULL DEFAULT '[]', + severity TEXT NOT NULL DEFAULT 'medium', + latest_update_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_incidents_status ON incidents(status, latest_update_at DESC); + CREATE INDEX IF NOT EXISTS idx_incidents_root ON incidents(root_service_id, started_at DESC); + + CREATE TABLE IF NOT EXISTS demo_sessions ( + id INTEGER PRIMARY KEY CHECK (id = 1), + snapshot_services TEXT NOT NULL, + snapshot_probes TEXT NOT NULL, + snapshot_events TEXT NOT NULL DEFAULT '[]', + snapshot_incidents TEXT NOT NULL DEFAULT '[]', + snapshot_discovery TEXT NOT NULL DEFAULT '{}', + snapshot_health_evidence TEXT NOT NULL DEFAULT '{}', + demo_service_ids TEXT NOT NULL, + started_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS health_evidence ( + id TEXT PRIMARY KEY, + subject_type TEXT NOT NULL, + subject_id TEXT NOT NULL, + service_id TEXT, + provider TEXT NOT NULL, + provider_ref TEXT, + status TEXT NOT NULL, + scope TEXT NOT NULL, + confidence REAL NOT NULL, + observed_at TEXT NOT NULL, + valid_until TEXT NOT NULL, + received_at TEXT NOT NULL, + summary TEXT NOT NULL DEFAULT '', + metadata TEXT NOT NULL DEFAULT '{}', + active INTEGER NOT NULL DEFAULT 1, + superseded_by TEXT, + FOREIGN KEY(service_id) REFERENCES services(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_health_evidence_subject + ON health_evidence(subject_type, subject_id, provider, active, observed_at DESC); + CREATE INDEX IF NOT EXISTS idx_health_evidence_service + ON health_evidence(service_id, active, observed_at DESC); + + CREATE TABLE IF NOT EXISTS health_evidence_service_links ( + evidence_id TEXT NOT NULL, + service_id TEXT NOT NULL, + PRIMARY KEY(evidence_id, service_id), + FOREIGN KEY(evidence_id) REFERENCES health_evidence(id) ON DELETE CASCADE, + FOREIGN KEY(service_id) REFERENCES services(id) ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS idx_health_evidence_link_service + ON health_evidence_service_links(service_id, evidence_id); + + CREATE TABLE IF NOT EXISTS provider_status ( + provider TEXT PRIMARY KEY, + status TEXT NOT NULL, + observed_at TEXT NOT NULL, + valid_until TEXT NOT NULL, + detail TEXT NOT NULL DEFAULT '', + metadata TEXT NOT NULL DEFAULT '{}' + ); + """ + ) + self._migrate_columns(db) + self._backfill_health_evidence(db) + + @staticmethod + def _migrate_columns(db: sqlite3.Connection) -> None: + columns = {row["name"] for row in db.execute("PRAGMA table_info(services)").fetchall()} + additions = { + "updated_at": "TEXT", + "description": "TEXT", + "tags": "TEXT NOT NULL DEFAULT '[]'", + "group": "TEXT", + "environment": "TEXT", + "owner": "TEXT", + "enabled": "INTEGER NOT NULL DEFAULT 1", + "monitoring_enabled": "INTEGER NOT NULL DEFAULT 1", + "monitoring_state": "TEXT NOT NULL DEFAULT 'active_probe'", + "probe_source": "TEXT NOT NULL DEFAULT 'manual'", + "probe_reachability": "TEXT NOT NULL DEFAULT 'verified'", + "probe_confidence": "TEXT NOT NULL DEFAULT 'confirmed'", + "runtime_state": "TEXT", + "runtime_health": "TEXT", + "application": "TEXT", + "domain": "TEXT", + "component_role": "TEXT NOT NULL DEFAULT 'unknown'", + "application_locked": "INTEGER NOT NULL DEFAULT 0", + "domain_locked": "INTEGER NOT NULL DEFAULT 0", + "component_role_locked": "INTEGER NOT NULL DEFAULT 0", + "criticality": "TEXT NOT NULL DEFAULT 'medium'", + "category": "TEXT NOT NULL DEFAULT 'unknown'", + "visibility": "TEXT NOT NULL DEFAULT 'visible'", + "lifecycle": "TEXT NOT NULL DEFAULT 'active'", + "source_type": "TEXT", + "source_ref": "TEXT", + "first_seen_at": "TEXT", + "last_seen_at": "TEXT", + "last_discovery_status": "TEXT", + "container_image": "TEXT", + "compose_project": "TEXT", + "layout_x": "REAL", + "layout_y": "REAL", + "layout_pinned": "INTEGER NOT NULL DEFAULT 0", + "interval_seconds": "INTEGER", + "timeout_seconds": "REAL", + "retries": "INTEGER", + "failure_threshold": "INTEGER NOT NULL DEFAULT 1", + "recovery_threshold": "INTEGER NOT NULL DEFAULT 1", + "expected_status_min": "INTEGER", + "expected_status_max": "INTEGER", + "availability_target": "REAL", + "maintenance_enabled": "INTEGER NOT NULL DEFAULT 0", + "maintenance_reason": "TEXT", + "state": "TEXT NOT NULL DEFAULT 'unknown'", + "failure_streak": "INTEGER NOT NULL DEFAULT 0", + "recovery_streak": "INTEGER NOT NULL DEFAULT 0", + "flapping": "INTEGER NOT NULL DEFAULT 0", + "flap_count": "INTEGER NOT NULL DEFAULT 0", + "last_state_change_at": "TEXT", + } + for name, definition in additions.items(): + if name not in columns: + quoted = f'"{name}"' if name == "group" else name + db.execute(f"ALTER TABLE services ADD COLUMN {quoted} {definition}") + db.execute("UPDATE services SET updated_at = created_at WHERE updated_at IS NULL") + + probe_columns = {row["name"] for row in db.execute("PRAGMA table_info(probe_results)").fetchall()} + for name, definition in {"trigger": "TEXT NOT NULL DEFAULT 'manual'", "metadata": "TEXT NOT NULL DEFAULT '{}'"}.items(): + if name not in probe_columns: + db.execute(f"ALTER TABLE probe_results ADD COLUMN {name} {definition}") + + # v1.0.1 derived health directly from the latest probe. Backfill the + # durable v1.1 state column so an in-place migration does not make an + # existing healthy/down service appear unknown until the next cycle. + db.execute( + """ + UPDATE services + SET state = COALESCE( + (SELECT latest.status FROM probe_results latest + WHERE latest.service_id = services.id + ORDER BY latest.checked_at DESC, latest.rowid DESC LIMIT 1), + state + ), + last_state_change_at = COALESCE( + last_state_change_at, + (SELECT latest.checked_at FROM probe_results latest + WHERE latest.service_id = services.id + ORDER BY latest.checked_at DESC, latest.rowid DESC LIMIT 1) + ) + WHERE state = 'unknown' + AND EXISTS (SELECT 1 FROM probe_results existing WHERE existing.service_id = services.id) + """ + ) + + demo_columns = {row["name"] for row in db.execute("PRAGMA table_info(demo_sessions)").fetchall()} + for name in ("snapshot_events", "snapshot_incidents", "snapshot_discovery", "snapshot_health_evidence"): + if name not in demo_columns: + default = "'{}'" if name in {"snapshot_discovery", "snapshot_health_evidence"} else "'[]'" + db.execute(f"ALTER TABLE demo_sessions ADD COLUMN {name} TEXT NOT NULL DEFAULT {default}") + + @staticmethod + def _backfill_health_evidence(db: sqlite3.Connection) -> None: + """Preserve legitimate recent v1.5 observations during the v1.6 migration. + + This is a one-time projection of existing probe/runtime facts, not a + fabricated monitoring result. Historic observations stay expired and + visible in provenance; only their original interval makes them current. + AppOps rows are deliberately excluded because pre-v1.6 persisted an + application-scoped status as component runtime health. + """ + + if db.execute("SELECT 1 FROM health_evidence LIMIT 1").fetchone(): + return + latest = db.execute( + """ + SELECT p.* FROM probe_results p + WHERE p.rowid = ( + SELECT candidate.rowid FROM probe_results candidate + WHERE candidate.service_id = p.service_id + ORDER BY candidate.checked_at DESC, candidate.rowid DESC LIMIT 1 + ) + """ + ).fetchall() + intervals = { + row["id"]: max(60, int(row["interval_seconds"] or 60) * 2) + for row in db.execute("SELECT id, interval_seconds FROM services").fetchall() + } + for row in latest: + observed = datetime.fromisoformat(row["checked_at"]) + ttl = timedelta(seconds=intervals.get(row["service_id"], 120)) + evidence_id = f"migration-probe-{row['id']}" + db.execute( + """ + INSERT INTO health_evidence ( + id, subject_type, subject_id, service_id, provider, provider_ref, status, scope, + confidence, observed_at, valid_until, received_at, summary, metadata, active + ) VALUES (?, 'service', ?, ?, 'active_probe', ?, ?, 'component', 1.0, ?, ?, ?, ?, ?, 1) + """, + ( + evidence_id, + row["service_id"], + row["service_id"], + row["id"], + row["status"], + observed.isoformat(), + (observed + ttl).isoformat(), + observed.isoformat(), + "Migrated latest active probe result", + row["metadata"] or "{}", + ), + ) + db.execute( + "INSERT OR IGNORE INTO health_evidence_service_links (evidence_id, service_id) VALUES (?, ?)", + (evidence_id, row["service_id"]), + ) + + runtime_rows = db.execute( + """ + SELECT id, runtime_state, runtime_health, updated_at, monitoring_state + FROM services + WHERE COALESCE(monitoring_state, '') != 'appops_observation' + AND (runtime_state IS NOT NULL OR runtime_health IS NOT NULL) + """ + ).fetchall() + for row in runtime_rows: + observed = datetime.fromisoformat(row["updated_at"] or _now().isoformat()) + runtime_state = str(row["runtime_state"] or "").casefold() + runtime_health = str(row["runtime_health"] or "").casefold() + if runtime_health in {"unhealthy", "down"} or runtime_state in {"exited", "dead", "stopped"}: + status = HealthStatus.DOWN + elif runtime_health == "degraded" or runtime_state == "restarting": + status = HealthStatus.DEGRADED + elif runtime_health == "healthy": + status = HealthStatus.HEALTHY + else: + continue + evidence_id = f"migration-runtime-{row['id']}" + db.execute( + """ + INSERT INTO health_evidence ( + id, subject_type, subject_id, service_id, provider, provider_ref, status, scope, + confidence, observed_at, valid_until, received_at, summary, metadata, active + ) VALUES (?, 'service', ?, ?, 'docker', 'legacy-runtime', ?, 'component', 0.7, ?, ?, ?, ?, ?, 1) + """, + ( + evidence_id, + row["id"], + row["id"], + status.value, + observed.isoformat(), + (observed + timedelta(minutes=10)).isoformat(), + observed.isoformat(), + "Migrated Docker runtime observation", + json.dumps({"runtime_state": row["runtime_state"], "runtime_health": row["runtime_health"]}), + ), + ) + db.execute( + "INSERT OR IGNORE INTO health_evidence_service_links (evidence_id, service_id) VALUES (?, ?)", + (evidence_id, row["id"]), + ) + + @staticmethod + def _json_list(value: object, fallback: list[str] | None = None) -> list[str]: + if isinstance(value, list): + return [str(item) for item in value] + if isinstance(value, str): + try: + parsed = json.loads(value) + return [str(item) for item in parsed] if isinstance(parsed, list) else (fallback or []) + except json.JSONDecodeError: + return fallback or [] + return fallback or [] + + @staticmethod + def _json_object(value: object) -> dict[str, object]: + if isinstance(value, dict): + return value + if isinstance(value, str): + try: + parsed = json.loads(value) + return parsed if isinstance(parsed, dict) else {} + except json.JSONDecodeError: + return {} + return {} + + @classmethod + def _row_to_service(cls, row: sqlite3.Row | dict[str, Any]) -> Service: + created_at = datetime.fromisoformat(str(row["created_at"])) + raw_updated = row.get("updated_at") if isinstance(row, dict) else row["updated_at"] + raw_state = row.get("state", "unknown") if isinstance(row, dict) else row["state"] + try: + state = HealthStatus(raw_state or "unknown") + except ValueError: + state = HealthStatus.UNKNOWN + return Service( + id=row["id"], + name=row["name"], + kind=row["kind"], + url=row["url"], + host=row["host"], + port=row["port"], + depends_on=cls._json_list(row["depends_on"]), + description=row.get("description") if isinstance(row, dict) else row["description"], + tags=cls._json_list(row.get("tags", "[]") if isinstance(row, dict) else row["tags"]), + group=row.get("group") if isinstance(row, dict) else row["group"], + environment=row.get("environment") if isinstance(row, dict) else row["environment"], + owner=row.get("owner") if isinstance(row, dict) else row["owner"], + enabled=_bool(row.get("enabled", 1) if isinstance(row, dict) else row["enabled"], True), + monitoring_enabled=_bool( + row.get("monitoring_enabled", 1) if isinstance(row, dict) else row["monitoring_enabled"], + True, + ), + monitoring_state=(row.get("monitoring_state", "active_probe") if isinstance(row, dict) else row["monitoring_state"]) or "active_probe", + probe_source=(row.get("probe_source", "manual") if isinstance(row, dict) else row["probe_source"]) or "manual", + probe_reachability=(row.get("probe_reachability", "verified") if isinstance(row, dict) else row["probe_reachability"]) or "verified", + probe_confidence=(row.get("probe_confidence", "confirmed") if isinstance(row, dict) else row["probe_confidence"]) or "confirmed", + runtime_state=row.get("runtime_state") if isinstance(row, dict) else row["runtime_state"], + runtime_health=row.get("runtime_health") if isinstance(row, dict) else row["runtime_health"], + application=row.get("application") if isinstance(row, dict) else row["application"], + domain=row.get("domain") if isinstance(row, dict) else row["domain"], + component_role=(row.get("component_role", "unknown") if isinstance(row, dict) else row["component_role"]) or "unknown", + application_locked=_bool(row.get("application_locked", 0) if isinstance(row, dict) else row["application_locked"]), + domain_locked=_bool(row.get("domain_locked", 0) if isinstance(row, dict) else row["domain_locked"]), + component_role_locked=_bool(row.get("component_role_locked", 0) if isinstance(row, dict) else row["component_role_locked"]), + criticality=row.get("criticality", "medium") if isinstance(row, dict) else row["criticality"], + category=row.get("category", "unknown") if isinstance(row, dict) else row["category"], + visibility=row.get("visibility", "visible") if isinstance(row, dict) else row["visibility"], + lifecycle=row.get("lifecycle", "active") if isinstance(row, dict) else row["lifecycle"], + source_type=row.get("source_type") if isinstance(row, dict) else row["source_type"], + source_ref=row.get("source_ref") if isinstance(row, dict) else row["source_ref"], + first_seen_at=( + datetime.fromisoformat(row["first_seen_at"]) + if (row.get("first_seen_at") if isinstance(row, dict) else row["first_seen_at"]) + else None + ), + last_seen_at=( + datetime.fromisoformat(row["last_seen_at"]) + if (row.get("last_seen_at") if isinstance(row, dict) else row["last_seen_at"]) + else None + ), + last_discovery_status=( + row.get("last_discovery_status") + if isinstance(row, dict) + else row["last_discovery_status"] + ), + container_image=row.get("container_image") if isinstance(row, dict) else row["container_image"], + compose_project=row.get("compose_project") if isinstance(row, dict) else row["compose_project"], + layout_x=row.get("layout_x") if isinstance(row, dict) else row["layout_x"], + layout_y=row.get("layout_y") if isinstance(row, dict) else row["layout_y"], + layout_pinned=_bool( + row.get("layout_pinned", 0) if isinstance(row, dict) else row["layout_pinned"] + ), + interval_seconds=row.get("interval_seconds") if isinstance(row, dict) else row["interval_seconds"], + timeout_seconds=row.get("timeout_seconds") if isinstance(row, dict) else row["timeout_seconds"], + retries=row.get("retries") if isinstance(row, dict) else row["retries"], + failure_threshold=(row.get("failure_threshold", 1) if isinstance(row, dict) else row["failure_threshold"]) or 1, + recovery_threshold=(row.get("recovery_threshold", 1) if isinstance(row, dict) else row["recovery_threshold"]) or 1, + expected_status_min=row.get("expected_status_min") if isinstance(row, dict) else row["expected_status_min"], + expected_status_max=row.get("expected_status_max") if isinstance(row, dict) else row["expected_status_max"], + availability_target=row.get("availability_target") if isinstance(row, dict) else row["availability_target"], + maintenance_enabled=_bool(row.get("maintenance_enabled", 0) if isinstance(row, dict) else row["maintenance_enabled"]), + maintenance_reason=row.get("maintenance_reason") if isinstance(row, dict) else row["maintenance_reason"], + state=state, + failure_streak=(row.get("failure_streak", 0) if isinstance(row, dict) else row["failure_streak"]) or 0, + recovery_streak=(row.get("recovery_streak", 0) if isinstance(row, dict) else row["recovery_streak"]) or 0, + flapping=_bool(row.get("flapping", 0) if isinstance(row, dict) else row["flapping"]), + flap_count=(row.get("flap_count", 0) if isinstance(row, dict) else row["flap_count"]) or 0, + last_state_change_at=( + datetime.fromisoformat(row["last_state_change_at"]) + if (row.get("last_state_change_at") if isinstance(row, dict) else row["last_state_change_at"]) + else None + ), + created_at=created_at, + updated_at=datetime.fromisoformat(str(raw_updated)) if raw_updated else created_at, + ) + + @classmethod + def _row_to_probe(cls, row: sqlite3.Row | dict[str, Any]) -> ProbeResult: + return ProbeResult( + id=row["id"], + service_id=row["service_id"], + status=HealthStatus(row["status"]), + latency_ms=row["latency_ms"], + detail=row["detail"], + checked_at=datetime.fromisoformat(row["checked_at"]), + trigger=(row.get("trigger", "manual") if isinstance(row, dict) else row["trigger"]) or "manual", + metadata=cls._json_object(row.get("metadata", "{}") if isinstance(row, dict) else row["metadata"]), + ) + + @classmethod + def _row_to_event(cls, row: sqlite3.Row | dict[str, Any]) -> EventRecord: + raw_from = row["from_status"] + raw_to = row["to_status"] + return EventRecord( + id=row["id"], + event_type=row["event_type"], + service_id=row["service_id"], + incident_id=row["incident_id"], + from_status=HealthStatus(raw_from) if raw_from else None, + to_status=HealthStatus(raw_to) if raw_to else None, + message=row["message"], + metadata=cls._json_object(row["metadata"]), + created_at=datetime.fromisoformat(row["created_at"]), + ) + + @classmethod + def _row_to_incident(cls, row: sqlite3.Row | dict[str, Any], services: dict[str, Service] | None = None) -> IncidentRecord: + services = services or {} + root = services.get(row["root_service_id"]) + affected_ids = cls._json_list(row["affected_service_ids"]) + return IncidentRecord( + id=row["id"], + status=IncidentStatus(row["status"]), + root_service_id=row["root_service_id"], + root_service_name=root.name if root else row["root_service_id"], + started_at=datetime.fromisoformat(row["started_at"]), + detected_at=datetime.fromisoformat(row["detected_at"]), + acknowledged_at=datetime.fromisoformat(row["acknowledged_at"]) if row["acknowledged_at"] else None, + resolved_at=datetime.fromisoformat(row["resolved_at"]) if row["resolved_at"] else None, + title=row["title"], + root_cause_reason=row["root_cause_reason"], + affected_service_ids=affected_ids, + affected_service_names=[services[item].name for item in affected_ids if item in services], + severity=row["severity"], + latest_update_at=datetime.fromisoformat(row["latest_update_at"]), + ) + + @staticmethod + def _service_params(service: Service) -> dict[str, Any]: + return { + "id": service.id, + "name": service.name, + "kind": service.kind.value, + "url": service.url, + "host": service.host, + "port": service.port, + "depends_on": json.dumps(service.depends_on), + "description": service.description, + "tags": json.dumps(service.tags), + "group": service.group, + "environment": service.environment, + "owner": service.owner, + "enabled": int(service.enabled), + "monitoring_enabled": int(service.monitoring_enabled), + "monitoring_state": service.monitoring_state.value, + "probe_source": service.probe_source.value, + "probe_reachability": service.probe_reachability.value, + "probe_confidence": service.probe_confidence.value, + "runtime_state": service.runtime_state, + "runtime_health": service.runtime_health, + "application": service.application, + "domain": service.domain, + "component_role": service.component_role.value, + "application_locked": int(service.application_locked), + "domain_locked": int(service.domain_locked), + "component_role_locked": int(service.component_role_locked), + "criticality": service.criticality.value, + "category": service.category.value, + "visibility": service.visibility.value, + "lifecycle": service.lifecycle.value, + "source_type": service.source_type, + "source_ref": service.source_ref, + "first_seen_at": _iso(service.first_seen_at), + "last_seen_at": _iso(service.last_seen_at), + "last_discovery_status": service.last_discovery_status, + "container_image": service.container_image, + "compose_project": service.compose_project, + "layout_x": service.layout_x, + "layout_y": service.layout_y, + "layout_pinned": int(service.layout_pinned), + "interval_seconds": service.interval_seconds, + "timeout_seconds": service.timeout_seconds, + "retries": service.retries, + "failure_threshold": service.failure_threshold, + "recovery_threshold": service.recovery_threshold, + "expected_status_min": service.expected_status_min, + "expected_status_max": service.expected_status_max, + "availability_target": service.availability_target, + "maintenance_enabled": int(service.maintenance_enabled), + "maintenance_reason": service.maintenance_reason, + "state": service.state.value, + "failure_streak": service.failure_streak, + "recovery_streak": service.recovery_streak, + "flapping": int(service.flapping), + "flap_count": service.flap_count, + "last_state_change_at": _iso(service.last_state_change_at), + "created_at": service.created_at.isoformat(), + "updated_at": service.updated_at.isoformat(), + } + + @classmethod + def _insert_service(cls, db: sqlite3.Connection, service: Service) -> None: + params = cls._service_params(service) + db.execute( + f"INSERT INTO services ({SERVICE_COLUMNS}) VALUES (" + ":id, :name, :kind, :url, :host, :port, :depends_on, :description, :tags, :group, " + ":environment, :owner, :enabled, :monitoring_enabled, :monitoring_state, :probe_source, :probe_reachability, :probe_confidence, :runtime_state, :runtime_health, :application, :domain, :component_role, :application_locked, :domain_locked, :component_role_locked, :criticality, :category, :visibility, :lifecycle, " + ":source_type, :source_ref, :first_seen_at, :last_seen_at, :last_discovery_status, :container_image, " + ":compose_project, :layout_x, :layout_y, :layout_pinned, " + ":interval_seconds, :timeout_seconds, :retries, :failure_threshold, :recovery_threshold, " + ":expected_status_min, :expected_status_max, :availability_target, :maintenance_enabled, " + ":maintenance_reason, :state, :failure_streak, :recovery_streak, :flapping, :flap_count, " + ":last_state_change_at, :created_at, :updated_at)", + params, + ) + + @classmethod + def _upsert_service(cls, db: sqlite3.Connection, service: Service) -> None: + params = cls._service_params(service) + db.execute( + f"INSERT INTO services ({SERVICE_COLUMNS}) VALUES (" + ":id, :name, :kind, :url, :host, :port, :depends_on, :description, :tags, :group, " + ":environment, :owner, :enabled, :monitoring_enabled, :monitoring_state, :probe_source, :probe_reachability, :probe_confidence, :runtime_state, :runtime_health, :application, :domain, :component_role, :application_locked, :domain_locked, :component_role_locked, :criticality, :category, :visibility, :lifecycle, " + ":source_type, :source_ref, :first_seen_at, :last_seen_at, :last_discovery_status, :container_image, " + ":compose_project, :layout_x, :layout_y, :layout_pinned, " + ":interval_seconds, :timeout_seconds, :retries, :failure_threshold, :recovery_threshold, " + ":expected_status_min, :expected_status_max, :availability_target, :maintenance_enabled, " + ":maintenance_reason, :state, :failure_streak, :recovery_streak, :flapping, :flap_count, " + ":last_state_change_at, :created_at, :updated_at) " + "ON CONFLICT(id) DO UPDATE SET " + "name=excluded.name, kind=excluded.kind, url=excluded.url, host=excluded.host, port=excluded.port, " + "depends_on=excluded.depends_on, description=excluded.description, tags=excluded.tags, " + '"group"=excluded."group", environment=excluded.environment, owner=excluded.owner, ' + "enabled=excluded.enabled, monitoring_enabled=excluded.monitoring_enabled, monitoring_state=excluded.monitoring_state, " + "probe_source=excluded.probe_source, probe_reachability=excluded.probe_reachability, probe_confidence=excluded.probe_confidence, " + "runtime_state=excluded.runtime_state, runtime_health=excluded.runtime_health, application=excluded.application, domain=excluded.domain, component_role=excluded.component_role, " + "application_locked=excluded.application_locked, domain_locked=excluded.domain_locked, component_role_locked=excluded.component_role_locked, criticality=excluded.criticality, " + "category=excluded.category, visibility=excluded.visibility, lifecycle=excluded.lifecycle, " + "source_type=excluded.source_type, source_ref=excluded.source_ref, first_seen_at=excluded.first_seen_at, " + "last_seen_at=excluded.last_seen_at, last_discovery_status=excluded.last_discovery_status, " + "container_image=excluded.container_image, compose_project=excluded.compose_project, " + "layout_x=excluded.layout_x, layout_y=excluded.layout_y, layout_pinned=excluded.layout_pinned, " + "interval_seconds=excluded.interval_seconds, " + "timeout_seconds=excluded.timeout_seconds, retries=excluded.retries, failure_threshold=excluded.failure_threshold, " + "recovery_threshold=excluded.recovery_threshold, expected_status_min=excluded.expected_status_min, " + "expected_status_max=excluded.expected_status_max, availability_target=excluded.availability_target, " + "maintenance_enabled=excluded.maintenance_enabled, maintenance_reason=excluded.maintenance_reason, " + "state=excluded.state, failure_streak=excluded.failure_streak, recovery_streak=excluded.recovery_streak, " + "flapping=excluded.flapping, flap_count=excluded.flap_count, last_state_change_at=excluded.last_state_change_at, " + "created_at=excluded.created_at, updated_at=excluded.updated_at", + params, + ) + + def list_services(self) -> list[Service]: + with self._connect() as db: + rows = db.execute(f"SELECT {SERVICE_COLUMNS} FROM services ORDER BY name COLLATE NOCASE").fetchall() + return [self._row_to_service(row) for row in rows] + + def get_service(self, service_id: str) -> Service | None: + with self._connect() as db: + row = db.execute(f"SELECT {SERVICE_COLUMNS} FROM services WHERE id = ?", (service_id,)).fetchone() + return self._row_to_service(row) if row else None + + def get_service_by_name(self, name: str) -> Service | None: + with self._connect() as db: + row = db.execute( + f"SELECT {SERVICE_COLUMNS} FROM services WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + return self._row_to_service(row) if row else None + + def create_service(self, payload: ServiceCreate) -> Service: + now = _now() + service = Service(id=str(uuid4()), created_at=now, updated_at=now, **payload.model_dump()) + with self._connect() as db: + self._insert_service(db, service) + self._insert_event_db(db, "topology.changed", None, None, None, None, f"Service {service.name} created", {}) + event_bus.publish({"event_type": "topology.changed", "message": f"Service {service.name} created"}) + return service + + def update_service(self, service_id: str, payload: ServiceUpdate) -> Service | None: + existing = self.get_service(service_id) + if existing is None: + return None + updated = Service( + id=service_id, + created_at=existing.created_at, + updated_at=_now(), + state=existing.state, + failure_streak=existing.failure_streak, + recovery_streak=existing.recovery_streak, + flapping=existing.flapping, + flap_count=existing.flap_count, + last_state_change_at=existing.last_state_change_at, + **payload.model_dump(), + ) + with self._connect() as db: + self._upsert_service(db, updated) + self._insert_event_db(db, "topology.changed", service_id, None, None, None, f"Service {updated.name} updated", {}) + event_bus.publish({"event_type": "topology.changed", "service_id": service_id, "message": "Service updated"}) + return updated + + def delete_service(self, service_id: str) -> bool: + with self._connect() as db: + row = db.execute("SELECT name FROM services WHERE id = ?", (service_id,)).fetchone() + if row is None: + return False + db.execute("DELETE FROM services WHERE id = ?", (service_id,)) + rows = db.execute("SELECT id, depends_on FROM services").fetchall() + for item in rows: + dependencies = self._json_list(item["depends_on"]) + if service_id in dependencies: + db.execute( + "UPDATE services SET depends_on = ?, updated_at = ? WHERE id = ?", + (json.dumps([value for value in dependencies if value != service_id]), _now().isoformat(), item["id"]), + ) + db.execute("UPDATE incidents SET affected_service_ids = replace(affected_service_ids, ?, '') WHERE status != 'resolved'", (service_id,)) + self._insert_event_db(db, "topology.changed", service_id, None, None, None, f"Service {row['name']} deleted", {}) + event_bus.publish({"event_type": "topology.changed", "service_id": service_id, "message": "Service deleted"}) + return True + + def import_config_services(self, definitions: list[ConfigService], mode: str) -> ConfigImportResult: + if mode not in {"merge", "replace"}: + raise ValueError("Import mode must be merge or replace") + + with self._connect() as db: + db.execute("BEGIN IMMEDIATE") + rows = db.execute(f"SELECT {SERVICE_COLUMNS} FROM services").fetchall() + existing = {row["name"].casefold(): self._row_to_service(row) for row in rows} + if mode == "replace": + db.execute("DELETE FROM incidents") + db.execute("DELETE FROM events") + db.execute("DELETE FROM services") + existing = {} + + ids_by_name = {name: service.id for name, service in existing.items()} + for definition in definitions: + ids_by_name.setdefault(definition.name.casefold(), str(uuid4())) + for definition in definitions: + missing = [item for item in definition.depends_on if item.casefold() not in ids_by_name] + if missing: + raise ValueError( + f"Service {definition.name} references unknown dependencies: {', '.join(missing)}" + ) + + created = 0 + updated = 0 + now = _now() + for definition in definitions: + key = definition.name.casefold() + current = existing.get(key) + payload = definition.model_dump() + payload["depends_on"] = [ids_by_name[item.casefold()] for item in definition.depends_on] + if current is None: + service = Service(id=ids_by_name[key], created_at=now, updated_at=now, **payload) + created += 1 + else: + service = Service( + id=current.id, + created_at=current.created_at, + updated_at=now, + state=current.state, + failure_streak=current.failure_streak, + recovery_streak=current.recovery_streak, + flapping=current.flapping, + flap_count=current.flap_count, + last_state_change_at=current.last_state_change_at, + **payload, + ) + updated += 1 + if current is None: + self._insert_service(db, service) + else: + self._upsert_service(db, service) + self._insert_event_db(db, "topology.changed", None, None, None, None, "Configuration imported", {"mode": mode}) + event_bus.publish({"event_type": "topology.changed", "message": "Configuration imported"}) + return ConfigImportResult(created=created, updated=updated, total=len(definitions)) + + def replace_all_services(self, definitions: list[ServiceCreate]) -> list[Service]: + with self._connect() as db: + db.execute("DELETE FROM services") + return [self.create_service(item) for item in definitions] + + @staticmethod + def _row_to_health_evidence(row: sqlite3.Row) -> HealthEvidence: + return with_freshness( + HealthEvidence( + id=row["id"], + subject_type=row["subject_type"], + subject_id=row["subject_id"], + service_id=row["service_id"], + provider=row["provider"], + provider_ref=row["provider_ref"], + status=row["status"], + scope=row["scope"], + confidence=float(row["confidence"]), + observed_at=datetime.fromisoformat(row["observed_at"]), + valid_until=datetime.fromisoformat(row["valid_until"]), + received_at=datetime.fromisoformat(row["received_at"]), + summary=row["summary"] or "", + metadata=Store._json_object(row["metadata"]), + active=_bool(row["active"], True), + superseded_by=row["superseded_by"], + ) + ) + + def record_health_evidence( + self, + evidence: HealthEvidence, + *, + service_ids: list[str] | None = None, + ) -> HealthEvidence: + """Store a current provider claim and retain superseded provenance. + + Repeated discovery cycles renew the same active claim instead of + growing unbounded identical rows. A changed claim deactivates only + the prior claim from that provider/subject; other providers remain + independently auditable and can surface a conflict. + """ + + links = sorted({*(service_ids or []), *({evidence.service_id} if evidence.service_id else set())}) + with self._connect() as db: + prior = db.execute( + """ + SELECT * FROM health_evidence + WHERE subject_type = ? AND subject_id = ? AND provider = ? + AND COALESCE(provider_ref, '') = COALESCE(?, '') AND scope = ? AND active = 1 + ORDER BY observed_at DESC, rowid DESC LIMIT 1 + """, + ( + evidence.subject_type, + evidence.subject_id, + evidence.provider.value, + evidence.provider_ref, + evidence.scope.value, + ), + ).fetchone() + saved = evidence + materially_changed = prior is None or any( + ( + prior[field] != value + for field, value in ( + ("status", evidence.status.value), + ("confidence", evidence.confidence), + ("summary", evidence.summary), + ("metadata", json.dumps(evidence.metadata, sort_keys=True)), + ) + ) + ) + if prior is not None and prior["status"] == evidence.status.value: + saved = evidence.model_copy(update={"id": prior["id"], "active": True, "superseded_by": None}) + db.execute( + """ + UPDATE health_evidence SET service_id = ?, confidence = ?, observed_at = ?, valid_until = ?, + received_at = ?, summary = ?, metadata = ?, active = 1, superseded_by = NULL + WHERE id = ? + """, + ( + saved.service_id, + saved.confidence, + saved.observed_at.isoformat(), + saved.valid_until.isoformat(), + saved.received_at.isoformat(), + saved.summary, + json.dumps(saved.metadata, sort_keys=True), + saved.id, + ), + ) + else: + if prior is not None: + db.execute("UPDATE health_evidence SET active = 0, superseded_by = ? WHERE id = ?", (evidence.id, prior["id"])) + db.execute( + """ + INSERT INTO health_evidence ( + id, subject_type, subject_id, service_id, provider, provider_ref, status, scope, + confidence, observed_at, valid_until, received_at, summary, metadata, active, superseded_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + saved.id, + saved.subject_type, + saved.subject_id, + saved.service_id, + saved.provider.value, + saved.provider_ref, + saved.status.value, + saved.scope.value, + saved.confidence, + saved.observed_at.isoformat(), + saved.valid_until.isoformat(), + saved.received_at.isoformat(), + saved.summary, + json.dumps(saved.metadata, sort_keys=True), + int(saved.active), + saved.superseded_by, + ), + ) + for service_id in links: + db.execute( + "INSERT OR IGNORE INTO health_evidence_service_links (evidence_id, service_id) VALUES (?, ?)", + (saved.id, service_id), + ) + if materially_changed: + event_bus.publish( + { + "event_type": "health.evidence_changed", + "service_id": evidence.service_id, + "provider": evidence.provider.value, + "status": evidence.status.value, + "message": "Health evidence changed", + } + ) + return saved + + def health_evidence_for_service(self, service_id: str, *, limit: int = 100) -> list[HealthEvidence]: + with self._connect() as db: + rows = db.execute( + """ + SELECT DISTINCT e.* FROM health_evidence e + LEFT JOIN health_evidence_service_links link ON link.evidence_id = e.id + WHERE e.service_id = ? OR link.service_id = ? + ORDER BY e.observed_at DESC, e.rowid DESC LIMIT ? + """, + (service_id, service_id, limit), + ).fetchall() + return [self._row_to_health_evidence(row) for row in rows] + + def health_evidence_for_services( + self, service_ids: list[str], *, limit_per_service: int = 100 + ) -> dict[str, list[HealthEvidence]]: + """Fetch linked evidence for a topology in one bounded query. + + A semantic topology can contain many applications. Resolving each + application with a separate evidence query creates an avoidable N+1 + SQLite scan, so this method projects the evidence links once and then + groups them in memory. + """ + + ids = sorted(set(service_ids)) + result = {service_id: [] for service_id in ids} + if not ids: + return result + placeholders = ", ".join("?" for _ in ids) + with self._connect() as db: + rows = db.execute( + f""" + SELECT e.*, e.service_id AS direct_service_id, link.service_id AS linked_service_id + FROM health_evidence e + LEFT JOIN health_evidence_service_links link ON link.evidence_id = e.id + WHERE e.service_id IN ({placeholders}) OR link.service_id IN ({placeholders}) + ORDER BY e.observed_at DESC, e.rowid DESC + """, + [*ids, *ids], + ).fetchall() + seen: dict[str, set[str]] = {service_id: set() for service_id in ids} + for row in rows: + evidence = self._row_to_health_evidence(row) + for service_id in {row["direct_service_id"], row["linked_service_id"]}: + if ( + service_id in result + and evidence.id not in seen[service_id] + and len(result[service_id]) < limit_per_service + ): + result[service_id].append(evidence) + seen[service_id].add(evidence.id) + return result + + def application_evidence_links(self, provider: EvidenceProvider) -> dict[str, set[str]]: + """Return persisted application-claim links keyed by provider reference.""" + + with self._connect() as db: + rows = db.execute( + """ + SELECT e.provider_ref, e.subject_id, link.service_id + FROM health_evidence e + JOIN health_evidence_service_links link ON link.evidence_id = e.id + WHERE e.provider = ? AND e.scope = ? + """, + (provider.value, EvidenceScope.APPLICATION.value), + ).fetchall() + result: dict[str, set[str]] = {} + for row in rows: + key = row["provider_ref"] or row["subject_id"] + if key: + result.setdefault(key, set()).add(row["service_id"]) + return result + + def health_resolution(self, service_id: str) -> HealthResolution: + return resolve_health(self.health_evidence_for_service(service_id)) + + def set_provider_status(self, value: ProviderStatus) -> ProviderStatus: + with self._connect() as db: + previous = db.execute( + "SELECT status, detail, metadata FROM provider_status WHERE provider = ?", + (value.provider.value,), + ).fetchone() + db.execute( + """ + INSERT INTO provider_status (provider, status, observed_at, valid_until, detail, metadata) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(provider) DO UPDATE SET status=excluded.status, observed_at=excluded.observed_at, + valid_until=excluded.valid_until, detail=excluded.detail, metadata=excluded.metadata + """, + ( + value.provider.value, + value.status.value, + value.observed_at.isoformat(), + value.valid_until.isoformat(), + value.detail, + json.dumps(value.metadata, sort_keys=True), + ), + ) + if previous is None or any( + ( + previous[field] != expected + for field, expected in ( + ("status", value.status.value), + ("detail", value.detail), + ("metadata", json.dumps(value.metadata, sort_keys=True)), + ) + ) + ): + event_bus.publish( + { + "event_type": "provider.status_changed", + "provider": value.provider.value, + "status": value.status.value, + "message": "Provider availability changed", + } + ) + return value + + def provider_statuses(self) -> list[ProviderStatus]: + with self._connect() as db: + rows = db.execute("SELECT * FROM provider_status ORDER BY provider").fetchall() + return [ + ProviderStatus( + provider=row["provider"], + status=row["status"], + observed_at=datetime.fromisoformat(row["observed_at"]), + valid_until=datetime.fromisoformat(row["valid_until"]), + detail=row["detail"] or "", + metadata=self._json_object(row["metadata"]), + ) + for row in rows + ] + + @staticmethod + def _legacy_health_from_row(row: sqlite3.Row) -> HealthStatus: + """Compatibility fallback for pre-v1.6 data without component evidence.""" + + runtime_state = str(row["runtime_state"] or "").casefold() + runtime_health = str(row["runtime_health"] or "").casefold() + if runtime_health == "unhealthy" or runtime_state in {"exited", "dead", "stopped"}: + return HealthStatus.DOWN + if runtime_state == "restarting": + return HealthStatus.DEGRADED + if not _bool(row["monitoring_enabled"], True): + if runtime_health == "healthy": + return HealthStatus.HEALTHY + if str(row["monitoring_state"] or "").casefold() in {"unmonitored", "observed"}: + return HealthStatus.UNMONITORED + return HealthStatus.UNKNOWN + try: + return HealthStatus(row["state"] or "unknown") + except ValueError: + return HealthStatus.UNKNOWN + + def service_health(self, service_id: str) -> HealthStatus: + with self._connect() as db: + row = db.execute( + "SELECT state, maintenance_enabled, enabled, monitoring_enabled, monitoring_state, runtime_state, runtime_health " + "FROM services WHERE id = ?", + (service_id,), + ).fetchone() + if row is None or not _bool(row["enabled"], True): + return HealthStatus.UNKNOWN + if _bool(row["maintenance_enabled"]): + return HealthStatus.MAINTENANCE + resolution = self.health_resolution(service_id) + if resolution.selected_evidence_id: + return resolution.status + # Once v1.6 evidence exists, an expired/stale claim must not silently + # keep a historical state alive. Legacy fallback is only for rows + # that predate evidence entirely. Old AppOps-only rows were always + # aggregate application observations, never component health. + if resolution.evidence: + return HealthStatus.UNKNOWN + if str(row["monitoring_state"] or "").casefold() == MonitoringState.APPOPS.value: + return HealthStatus.UNMONITORED + return self._legacy_health_from_row(row) + + def service_healths( + self, + service_ids: list[str], + *, + evidence_by_service: dict[str, list[HealthEvidence]] | None = None, + ) -> tuple[dict[str, HealthStatus], dict[str, HealthResolution]]: + """Resolve direct health for a service set without per-service SQL reads.""" + + ids = sorted(set(service_ids)) + if not ids: + return {}, {} + evidence_by_service = evidence_by_service or self.health_evidence_for_services(ids) + placeholders = ", ".join("?" for _ in ids) + with self._connect() as db: + rows = db.execute( + f""" + SELECT id, state, maintenance_enabled, enabled, monitoring_enabled, + monitoring_state, runtime_state, runtime_health + FROM services WHERE id IN ({placeholders}) + """, + ids, + ).fetchall() + status_by_id: dict[str, HealthStatus] = {} + resolutions: dict[str, HealthResolution] = {} + for row in rows: + service_id = row["id"] + resolution = resolve_health(evidence_by_service.get(service_id, [])) + resolutions[service_id] = resolution + if not _bool(row["enabled"], True): + status_by_id[service_id] = HealthStatus.UNKNOWN + elif _bool(row["maintenance_enabled"]): + status_by_id[service_id] = HealthStatus.MAINTENANCE + elif resolution.selected_evidence_id: + status_by_id[service_id] = resolution.status + elif resolution.evidence: + status_by_id[service_id] = HealthStatus.UNKNOWN + elif str(row["monitoring_state"] or "").casefold() == MonitoringState.APPOPS.value: + status_by_id[service_id] = HealthStatus.UNMONITORED + else: + status_by_id[service_id] = self._legacy_health_from_row(row) + return status_by_id, resolutions + + def set_maintenance(self, service_id: str, enabled: bool, reason: str | None = None) -> Service | None: + service = self.get_service(service_id) + if service is None: + return None + updated = service.model_copy( + update={ + "maintenance_enabled": enabled, + "maintenance_reason": reason if enabled else None, + "state": HealthStatus.MAINTENANCE if enabled else HealthStatus.UNKNOWN, + "failure_streak": 0, + "recovery_streak": 0, + "updated_at": _now(), + } + ) + with self._connect() as db: + self._upsert_service(db, updated) + self._insert_event_db( + db, + "service.maintenance_changed", + service_id, + None, + service.state.value, + HealthStatus.MAINTENANCE.value if enabled else HealthStatus.UNKNOWN.value, + f"Maintenance {'enabled' if enabled else 'disabled'} for {service.name}", + {"reason": reason} if reason else {}, + ) + event_bus.publish( + { + "event_type": "service.maintenance_changed", + "service_id": service_id, + "status": HealthStatus.MAINTENANCE.value if enabled else HealthStatus.UNKNOWN.value, + "message": f"Maintenance {'enabled' if enabled else 'disabled'}", + } + ) + from .incidents import sync_incidents + + sync_incidents(self) + return updated + + def _stable_state(self, row: sqlite3.Row, raw_status: HealthStatus) -> tuple[HealthStatus, int, int]: + if not _bool(row["enabled"], True) or not _bool(row["monitoring_enabled"], True): + monitoring_state = str(row["monitoring_state"] or "").casefold() + return (HealthStatus.UNMONITORED if monitoring_state in {"unmonitored", "observed"} else HealthStatus.UNKNOWN), 0, 0 + if _bool(row["maintenance_enabled"]): + return HealthStatus.MAINTENANCE, 0, 0 + previous = HealthStatus(row["state"] or "unknown") + failure_streak = int(row["failure_streak"] or 0) + recovery_streak = int(row["recovery_streak"] or 0) + failure_threshold = max(1, int(row["failure_threshold"] or 1)) + recovery_threshold = max(1, int(row["recovery_threshold"] or 1)) + if raw_status in {HealthStatus.DOWN, HealthStatus.DEGRADED}: + failure_streak += 1 + recovery_streak = 0 + if failure_streak < failure_threshold: + if previous == HealthStatus.UNKNOWN: + return HealthStatus.UNKNOWN, failure_streak, recovery_streak + if previous == HealthStatus.RECOVERING: + return HealthStatus.RECOVERING, failure_streak, recovery_streak + return previous, failure_streak, recovery_streak + return raw_status, failure_streak, recovery_streak + if raw_status == HealthStatus.HEALTHY: + recovery_streak += 1 + failure_streak = 0 + if previous in {HealthStatus.DOWN, HealthStatus.DEGRADED, HealthStatus.RECOVERING} and recovery_streak < recovery_threshold: + return HealthStatus.RECOVERING, failure_streak, recovery_streak + return HealthStatus.HEALTHY, failure_streak, recovery_streak + return HealthStatus.UNMONITORED if raw_status == HealthStatus.UNMONITORED else HealthStatus.UNKNOWN, 0, 0 + + def set_runtime_observation( + self, + service_id: str, + *, + runtime_state: str | None, + runtime_health: str | None, + monitoring_state: MonitoringState | str, + provider: EvidenceProvider = EvidenceProvider.DOCKER, + provider_ref: str | None = None, + confidence: float = 0.9, + ) -> Service | None: + """Persist Docker runtime truth without fabricating a network probe.""" + + service = self.get_service(service_id) + if service is None: + return None + state_kind = MonitoringState(monitoring_state) + normalized_state = (runtime_state or "").casefold() + normalized_health = (runtime_health or "").casefold() + if service.maintenance_enabled: + status = HealthStatus.MAINTENANCE + elif normalized_health in {"unhealthy", "down"} or normalized_state in {"exited", "dead", "stopped"}: + status = HealthStatus.DOWN + elif normalized_health == "degraded" or normalized_state == "restarting": + status = HealthStatus.DEGRADED + elif normalized_health == "healthy": + status = HealthStatus.HEALTHY + elif state_kind in { + MonitoringState.UNMONITORED, + MonitoringState.OBSERVED, + MonitoringState.APPOPS, + }: + status = HealthStatus.UNMONITORED + else: + status = service.state + updated = service.model_copy( + update={ + "runtime_state": runtime_state, + "runtime_health": runtime_health, + "monitoring_state": state_kind, + "state": status, + "failure_streak": 0 if status in {HealthStatus.UNMONITORED, HealthStatus.HEALTHY} else service.failure_streak, + "recovery_streak": 0 if status == HealthStatus.UNMONITORED else service.recovery_streak, + "last_state_change_at": _now() if service.state != status else service.last_state_change_at, + "updated_at": _now(), + } + ) + with self._connect() as db: + self._upsert_service(db, updated) + if service.state != status: + self._insert_event_db( + db, + "service.runtime_observed", + service_id, + None, + service.state.value, + status.value, + f"Docker runtime observation updated for {service.name}", + {"runtime_state": runtime_state, "runtime_health": runtime_health, "monitoring_state": state_kind.value}, + ) + if service.state != status: + event_bus.publish({"event_type": "service.runtime_observed", "service_id": service_id, "status": status.value, "message": "Docker runtime observation updated"}) + observed_at = _now() + self.record_health_evidence( + HealthEvidence( + id=str(uuid4()), + subject_type="service", + subject_id=service_id, + service_id=service_id, + provider=provider, + provider_ref=provider_ref or service.source_ref, + status=status, + scope=EvidenceScope.COMPONENT, + confidence=confidence, + observed_at=observed_at, + valid_until=observed_at + timedelta(seconds=max(60, int(service.interval_seconds or 60) * 2)), + received_at=observed_at, + summary="Docker runtime observation" if provider == EvidenceProvider.DOCKER else "Runtime observation", + metadata={"runtime_state": runtime_state, "runtime_health": runtime_health}, + ) + ) + return updated + + def save_probe(self, probe: ProbeResult) -> ProbeResult: + events: list[dict[str, Any]] = [] + with self._connect() as db: + row = db.execute("SELECT * FROM services WHERE id = ?", (probe.service_id,)).fetchone() + if row is None: + return probe + db.execute( + """ + INSERT INTO probe_results (id, service_id, status, latency_ms, detail, checked_at, trigger, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + probe.id, + probe.service_id, + probe.status.value, + probe.latency_ms, + probe.detail, + probe.checked_at.isoformat(), + probe.trigger, + json.dumps(probe.metadata), + ), + ) + previous = HealthStatus(row["state"] or "unknown") + next_state, failure_streak, recovery_streak = self._stable_state(row, probe.status) + now = probe.checked_at + state_changed = next_state != previous + last_state_change = now.isoformat() if state_changed else row["last_state_change_at"] + if state_changed: + event = { + "event_type": "service.state_changed", + "service_id": probe.service_id, + "from_status": previous.value, + "to_status": next_state.value, + "message": f"Service changed from {previous.value} to {next_state.value}", + "metadata": {"trigger": probe.trigger, "probe_id": probe.id}, + } + self._insert_event_db( + db, + event["event_type"], + probe.service_id, + None, + previous.value, + next_state.value, + event["message"], + event["metadata"], + created_at=now, + ) + events.append(event) + since = (now - timedelta(minutes=30)).isoformat() + flap_count = int( + db.execute( + "SELECT COUNT(*) FROM events WHERE service_id = ? AND event_type = 'service.state_changed' AND created_at >= ?", + (probe.service_id, since), + ).fetchone()[0] + ) + flapping = flap_count >= int(os.getenv("SERVICEMAP_FLAP_THRESHOLD", "6")) + if flapping and not _bool(row["flapping"]): + self._insert_event_db( + db, + "flapping.detected", + probe.service_id, + None, + None, + None, + f"Flapping detected: {flap_count} state changes in 30 minutes", + {"state_changes": flap_count, "window_minutes": 30}, + created_at=now, + ) + events.append({"event_type": "flapping.detected", "service_id": probe.service_id, "message": "Flapping detected"}) + db.execute( + """ + UPDATE services + SET state = ?, failure_streak = ?, recovery_streak = ?, flapping = ?, flap_count = ?, + last_state_change_at = ?, updated_at = ? + WHERE id = ? + """, + ( + next_state.value, + failure_streak, + recovery_streak, + int(flapping), + flap_count, + last_state_change, + now.isoformat(), + probe.service_id, + ), + ) + event_bus.publish( + { + "event_type": "probe.completed", + "service_id": probe.service_id, + "status": probe.status.value, + "trigger": probe.trigger, + "message": "Probe completed", + } + ) + for event in events: + event_bus.publish(event) + self.record_health_evidence( + HealthEvidence( + id=f"probe-{probe.id}", + subject_type="service", + subject_id=probe.service_id, + service_id=probe.service_id, + provider=EvidenceProvider.PROBE, + provider_ref="active-probe", + # The evidence mirrors the hysteresis-filtered service state, + # while the raw probe result remains intact in history. + status=next_state, + scope=EvidenceScope.COMPONENT, + confidence=1.0, + observed_at=probe.checked_at, + valid_until=probe.checked_at + timedelta(seconds=max(60, int(os.getenv("SERVICEMAP_PROBE_INTERVAL", "60")) * 2)), + received_at=_now(), + summary=probe.detail, + metadata={"probe_id": probe.id, "trigger": probe.trigger, "raw_status": probe.status.value, "latency_ms": probe.latency_ms, **probe.metadata}, + ) + ) + from .incidents import sync_incidents + + sync_incidents(self) + return probe + + def latest_probe(self, service_id: str) -> ProbeResult | None: + with self._connect() as db: + row = db.execute( + "SELECT * FROM probe_results WHERE service_id = ? ORDER BY checked_at DESC, rowid DESC LIMIT 1", + (service_id,), + ).fetchone() + return self._row_to_probe(row) if row else None + + def latest_probes(self, service_ids: list[str] | None = None) -> dict[str, ProbeResult]: + with self._connect() as db: + if service_ids: + placeholders = ",".join("?" for _ in service_ids) + rows = db.execute( + f""" + SELECT p.* FROM probe_results p + WHERE p.service_id IN ({placeholders}) + AND p.rowid = ( + SELECT candidate.rowid FROM probe_results candidate + WHERE candidate.service_id = p.service_id + ORDER BY candidate.checked_at DESC, candidate.rowid DESC LIMIT 1 + ) + """, + service_ids, + ).fetchall() + else: + rows = db.execute( + """ + SELECT p.* FROM probe_results p + WHERE p.rowid = ( + SELECT candidate.rowid FROM probe_results candidate + WHERE candidate.service_id = p.service_id + ORDER BY candidate.checked_at DESC, candidate.rowid DESC LIMIT 1 + ) + """ + ).fetchall() + return {row["service_id"]: self._row_to_probe(row) for row in rows} + + def probe_history(self, service_id: str, limit: int = 50) -> list[ProbeResult]: + with self._connect() as db: + rows = db.execute( + "SELECT * FROM probe_results WHERE service_id = ? ORDER BY checked_at DESC, rowid DESC LIMIT ?", + (service_id, limit), + ).fetchall() + return [self._row_to_probe(row) for row in rows] + + def availability_summary(self, service_id: str, hours: int = 24) -> AvailabilitySummary: + hours = max(1, min(hours, 24 * 90)) + since = _now() - timedelta(hours=hours) + with self._connect() as db: + row = db.execute( + """ + SELECT COUNT(*) AS total, + SUM(CASE WHEN status = 'healthy' THEN 1 ELSE 0 END) AS healthy, + SUM(CASE WHEN status = 'degraded' THEN 1 ELSE 0 END) AS degraded, + SUM(CASE WHEN status = 'down' THEN 1 ELSE 0 END) AS down_count, + AVG(latency_ms) AS avg_latency, MAX(checked_at) AS last_checked + FROM probe_results WHERE service_id = ? AND checked_at >= ? + """, + (service_id, since.isoformat()), + ).fetchone() + service_row = db.execute("SELECT availability_target FROM services WHERE id = ?", (service_id,)).fetchone() + total = int(row["total"] or 0) + healthy = int(row["healthy"] or 0) + degraded = int(row["degraded"] or 0) + down = int(row["down_count"] or 0) + uptime = round(((healthy + degraded) / total) * 100, 2) if total else None + target = service_row["availability_target"] if service_row else None + return AvailabilitySummary( + hours=hours, + total_checks=total, + healthy_checks=healthy, + degraded_checks=degraded, + down_checks=down, + uptime_percent=uptime, + average_latency_ms=round(float(row["avg_latency"]), 2) if row["avg_latency"] is not None else None, + last_checked_at=datetime.fromisoformat(row["last_checked"]) if row["last_checked"] else None, + target_percent=target, + meets_target=(uptime >= target) if uptime is not None and target is not None else None, + ) + + def availability_summaries(self, service_ids: list[str], hours: int = 24) -> dict[str, AvailabilitySummary]: + """Fetch table-ready availability in one grouped query, not N requests.""" + if not service_ids: + return {} + hours = max(1, min(hours, 24 * 90)) + since = _now() - timedelta(hours=hours) + placeholders = ",".join("?" for _ in service_ids) + with self._connect() as db: + rows = db.execute( + f""" + SELECT s.id, s.availability_target, COUNT(p.id) AS total, + SUM(CASE WHEN p.status = 'healthy' THEN 1 ELSE 0 END) AS healthy, + SUM(CASE WHEN p.status = 'degraded' THEN 1 ELSE 0 END) AS degraded, + SUM(CASE WHEN p.status = 'down' THEN 1 ELSE 0 END) AS down_count, + AVG(p.latency_ms) AS avg_latency, MAX(p.checked_at) AS last_checked + FROM services s + LEFT JOIN probe_results p ON p.service_id=s.id AND p.checked_at >= ? + WHERE s.id IN ({placeholders}) + GROUP BY s.id, s.availability_target + """, + [since.isoformat(), *service_ids], + ).fetchall() + result: dict[str, AvailabilitySummary] = {} + for row in rows: + total = int(row["total"] or 0) + healthy = int(row["healthy"] or 0) + degraded = int(row["degraded"] or 0) + target = row["availability_target"] + uptime = round(((healthy + degraded) / total) * 100, 2) if total else None + result[row["id"]] = AvailabilitySummary( + hours=hours, + total_checks=total, + healthy_checks=healthy, + degraded_checks=degraded, + down_checks=int(row["down_count"] or 0), + uptime_percent=uptime, + average_latency_ms=round(float(row["avg_latency"]), 2) if row["avg_latency"] is not None else None, + last_checked_at=datetime.fromisoformat(row["last_checked"]) if row["last_checked"] else None, + target_percent=target, + meets_target=(uptime >= target) if uptime is not None and target is not None else None, + ) + return result + + def next_probe_at(self, service: Service, global_interval: int) -> datetime | None: + if not service.enabled or not service.monitoring_enabled: + return None + last = self.latest_probe(service.id) + if last is None: + return _now() + return last.checked_at + timedelta(seconds=service.interval_seconds or global_interval) + + def prune_probes(self, retention_days: int, event_retention_days: int | None = None) -> int: + cutoff = _now() - timedelta(days=retention_days) + event_cutoff = _now() - timedelta(days=event_retention_days or retention_days) + with self._connect() as db: + cursor = db.execute("DELETE FROM probe_results WHERE checked_at < ?", (cutoff.isoformat(),)) + db.execute("DELETE FROM events WHERE created_at < ? AND event_type NOT LIKE 'incident.%'", (event_cutoff.isoformat(),)) + return cursor.rowcount + + def _insert_event_db( + self, + db: sqlite3.Connection, + event_type: str, + service_id: str | None, + incident_id: str | None, + from_status: str | None, + to_status: str | None, + message: str, + metadata: dict[str, object], + created_at: datetime | None = None, + ) -> str: + event_id = str(uuid4()) + db.execute( + """ + INSERT INTO events (id, event_type, service_id, incident_id, from_status, to_status, message, metadata, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + (event_id, event_type, service_id, incident_id, from_status, to_status, message, json.dumps(metadata), (created_at or _now()).isoformat()), + ) + return event_id + + def add_event( + self, + event_type: str, + message: str, + *, + service_id: str | None = None, + incident_id: str | None = None, + from_status: HealthStatus | None = None, + to_status: HealthStatus | None = None, + metadata: dict[str, object] | None = None, + ) -> EventRecord: + created_at = _now() + with self._connect() as db: + event_id = self._insert_event_db( + db, + event_type, + service_id, + incident_id, + from_status.value if from_status else None, + to_status.value if to_status else None, + message, + metadata or {}, + created_at, + ) + row = db.execute("SELECT * FROM events WHERE id = ?", (event_id,)).fetchone() + result = self._row_to_event(row) + event_bus.publish(result.model_dump(mode="json")) + return result + + def list_events(self, limit: int = 50, service_id: str | None = None) -> list[EventRecord]: + with self._connect() as db: + if service_id: + rows = db.execute("SELECT * FROM events WHERE service_id = ? ORDER BY created_at DESC LIMIT ?", (service_id, limit)).fetchall() + else: + rows = db.execute("SELECT * FROM events ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall() + return [self._row_to_event(row) for row in rows] + + def demo_active(self) -> bool: + with self._connect() as db: + return db.execute("SELECT 1 FROM demo_sessions WHERE id = 1").fetchone() is not None + + @staticmethod + def _snapshot_rows(db: sqlite3.Connection) -> tuple[str, str, str, str]: + services = [dict(row) for row in db.execute("SELECT * FROM services ORDER BY id").fetchall()] + probes = [dict(row) for row in db.execute("SELECT * FROM probe_results ORDER BY id").fetchall()] + events = [dict(row) for row in db.execute("SELECT * FROM events ORDER BY id").fetchall()] + incidents = [dict(row) for row in db.execute("SELECT * FROM incidents ORDER BY id").fetchall()] + return tuple(json.dumps(item, separators=(",", ":")) for item in (services, probes, events, incidents)) # type: ignore[return-value] + + @staticmethod + def _snapshot_discovery_rows(db: sqlite3.Connection) -> str: + tables = ( + "discovery_runs", "discovery_sources", "service_sources", "discovery_observations", + "relation_evidence", "topology_suggestions", "topology_overrides", "topology_change_events", + ) + existing = {row["name"] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()} + snapshot: dict[str, list[dict[str, object]]] = {} + for table in tables: + if table in existing: + snapshot[table] = [dict(row) for row in db.execute(f"SELECT * FROM {table}").fetchall()] + return json.dumps(snapshot, separators=(",", ":")) + + @staticmethod + def _snapshot_health_evidence_rows(db: sqlite3.Connection) -> str: + return json.dumps( + { + "health_evidence": [dict(row) for row in db.execute("SELECT * FROM health_evidence ORDER BY id").fetchall()], + "health_evidence_service_links": [ + dict(row) for row in db.execute("SELECT * FROM health_evidence_service_links ORDER BY evidence_id, service_id").fetchall() + ], + "provider_status": [dict(row) for row in db.execute("SELECT * FROM provider_status ORDER BY provider").fetchall()], + }, + separators=(",", ":"), + ) + + @classmethod + def _raw_service_params(cls, raw: dict[str, Any]) -> dict[str, Any]: + defaults: dict[str, Any] = { + "description": None, "tags": "[]", "group": None, "environment": None, "owner": None, + "enabled": 1, "monitoring_enabled": 1, "monitoring_state": "active_probe", "probe_source": "manual", + "probe_reachability": "verified", "probe_confidence": "confirmed", "runtime_state": None, "runtime_health": None, + "application": None, "criticality": "medium", "category": "unknown", + "visibility": "visible", "lifecycle": "active", "source_type": None, "source_ref": None, + "first_seen_at": None, "last_seen_at": None, "last_discovery_status": None, + "container_image": None, "compose_project": None, "layout_x": None, "layout_y": None, + "layout_pinned": 0, "interval_seconds": None, "timeout_seconds": None, "retries": None, + "failure_threshold": 1, "recovery_threshold": 1, "expected_status_min": None, + "expected_status_max": None, "availability_target": None, "maintenance_enabled": 0, + "maintenance_reason": None, "state": "unknown", "failure_streak": 0, "recovery_streak": 0, + "flapping": 0, "flap_count": 0, "last_state_change_at": None, "updated_at": raw.get("created_at"), + "depends_on": "[]", + } + params = {**defaults, **raw} + params["depends_on"] = json.dumps(params["depends_on"]) if isinstance(params["depends_on"], list) else params["depends_on"] + params["tags"] = json.dumps(params["tags"]) if isinstance(params["tags"], list) else params["tags"] + params["updated_at"] = params["updated_at"] or params["created_at"] + params["metadata"] = params.get("metadata", "{}") + params["trigger"] = params.get("trigger", "manual") + return params + + @classmethod + def _insert_raw_service(cls, db: sqlite3.Connection, raw: dict[str, Any]) -> None: + params = cls._raw_service_params(raw) + cls._insert_service_row(db, params) + + @staticmethod + def _insert_service_row(db: sqlite3.Connection, params: dict[str, Any]) -> None: + db.execute( + f"INSERT INTO services ({SERVICE_COLUMNS}) VALUES (" + ":id, :name, :kind, :url, :host, :port, :depends_on, :description, :tags, :group, " + ":environment, :owner, :enabled, :monitoring_enabled, :monitoring_state, :probe_source, :probe_reachability, :probe_confidence, :runtime_state, :runtime_health, :application, :domain, :component_role, :application_locked, :domain_locked, :component_role_locked, :criticality, :category, :visibility, :lifecycle, " + ":source_type, :source_ref, :first_seen_at, :last_seen_at, :last_discovery_status, :container_image, " + ":compose_project, :layout_x, :layout_y, :layout_pinned, " + ":interval_seconds, :timeout_seconds, :retries, :failure_threshold, :recovery_threshold, " + ":expected_status_min, :expected_status_max, :availability_target, :maintenance_enabled, " + ":maintenance_reason, :state, :failure_streak, :recovery_streak, :flapping, :flap_count, " + ":last_state_change_at, :created_at, :updated_at)", + params, + ) + + @classmethod + def _restore_rows(cls, db: sqlite3.Connection, snapshot_services: str, snapshot_probes: str, snapshot_events: str, snapshot_incidents: str, snapshot_discovery: str = "{}", snapshot_health_evidence: str = "{}") -> None: + services = json.loads(snapshot_services) + probes = json.loads(snapshot_probes) + events = json.loads(snapshot_events or "[]") + incidents = json.loads(snapshot_incidents or "[]") + db.execute("DELETE FROM incidents") + db.execute("DELETE FROM events") + db.execute("DELETE FROM services") + for raw in services: + cls._insert_raw_service(db, raw) + for raw in probes: + params = { + "id": raw["id"], + "service_id": raw["service_id"], + "status": raw["status"], + "latency_ms": raw.get("latency_ms"), + "detail": raw["detail"], + "checked_at": raw["checked_at"], + "trigger": raw.get("trigger", "manual"), + "metadata": raw.get("metadata", "{}"), + } + db.execute( + "INSERT INTO probe_results (id, service_id, status, latency_ms, detail, checked_at, trigger, metadata) VALUES (:id, :service_id, :status, :latency_ms, :detail, :checked_at, :trigger, :metadata)", + params, + ) + for raw in incidents: + db.execute( + "INSERT INTO incidents (id, status, root_service_id, started_at, detected_at, acknowledged_at, resolved_at, title, root_cause_reason, affected_service_ids, severity, latest_update_at) VALUES (:id, :status, :root_service_id, :started_at, :detected_at, :acknowledged_at, :resolved_at, :title, :root_cause_reason, :affected_service_ids, :severity, :latest_update_at)", + raw, + ) + for raw in events: + db.execute( + "INSERT INTO events (id, event_type, service_id, incident_id, from_status, to_status, message, metadata, created_at) VALUES (:id, :event_type, :service_id, :incident_id, :from_status, :to_status, :message, :metadata, :created_at)", + {**raw, "metadata": raw.get("metadata", "{}")}, + ) + + cls._restore_discovery_rows(db, snapshot_discovery) + cls._restore_health_evidence_rows(db, snapshot_health_evidence) + + @staticmethod + def _restore_health_evidence_rows(db: sqlite3.Connection, snapshot: str) -> None: + try: + items = json.loads(snapshot or "{}") + except json.JSONDecodeError: + items = {} + if not isinstance(items, dict): + return + db.execute("DELETE FROM health_evidence") + db.execute("DELETE FROM provider_status") + for table in ("health_evidence", "health_evidence_service_links", "provider_status"): + rows = items.get(table, []) + if not isinstance(rows, list): + continue + for raw in rows: + if not isinstance(raw, dict) or not raw: + continue + columns = list(raw) + quoted = ", ".join(f'"{column}"' for column in columns) + placeholders = ", ".join("?" for _ in columns) + db.execute(f"INSERT INTO {table} ({quoted}) VALUES ({placeholders})", tuple(raw[column] for column in columns)) + + @staticmethod + def _restore_discovery_rows(db: sqlite3.Connection, snapshot: str) -> None: + try: + tables = json.loads(snapshot or "{}") + except json.JSONDecodeError: + tables = {} + if not isinstance(tables, dict) or not tables: + return + order = ( + "topology_change_events", "topology_overrides", "relation_evidence", "service_sources", + "discovery_observations", "topology_suggestions", "discovery_sources", "discovery_runs", + ) + existing = {row["name"] for row in db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()} + for table in order: + if table in existing: + db.execute(f"DELETE FROM {table}") + for table in reversed(order): + rows = tables.get(table, []) + if table not in existing or not isinstance(rows, list): + continue + for raw in rows: + if not isinstance(raw, dict) or not raw: + continue + columns = list(raw) + quoted = ", ".join(f'"{column}"' for column in columns) + placeholders = ", ".join("?" for _ in columns) + db.execute(f"INSERT INTO {table} ({quoted}) VALUES ({placeholders})", tuple(raw[column] for column in columns)) + + @classmethod + def _insert_demo_rows(cls, db: sqlite3.Connection, services: list[Service], probes: list[ProbeResult]) -> None: + db.execute("DELETE FROM incidents") + db.execute("DELETE FROM events") + db.execute("DELETE FROM health_evidence") + db.execute("DELETE FROM provider_status") + db.execute("DELETE FROM services") + latest: dict[str, ProbeResult] = {} + for probe in probes: + if probe.service_id not in latest or probe.checked_at > latest[probe.service_id].checked_at: + latest[probe.service_id] = probe + for service in services: + seeded = service.model_copy(update={"state": latest.get(service.id, ProbeResult(id="", service_id=service.id, status=HealthStatus.UNKNOWN, latency_ms=None, detail="", checked_at=_now())).status}) + cls._insert_service_row(db, cls._service_params(seeded)) + for probe in probes: + db.execute( + "INSERT INTO probe_results (id, service_id, status, latency_ms, detail, checked_at, trigger, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + (probe.id, probe.service_id, probe.status.value, probe.latency_ms, probe.detail, probe.checked_at.isoformat(), probe.trigger, json.dumps(probe.metadata)), + ) + for probe in probes: + service = next((item for item in services if item.id == probe.service_id), None) + interval = service.interval_seconds if service and service.interval_seconds else 60 + evidence_id = f"demo-probe-{probe.id}" + db.execute( + """ + INSERT INTO health_evidence ( + id, subject_type, subject_id, service_id, provider, provider_ref, status, scope, + confidence, observed_at, valid_until, received_at, summary, metadata, active + ) VALUES (?, 'service', ?, ?, 'active_probe', 'active-probe', ?, 'component', 1.0, ?, ?, ?, ?, ?, 1) + ON CONFLICT(id) DO NOTHING + """, + ( + evidence_id, + probe.service_id, + probe.service_id, + probe.status.value, + probe.checked_at.isoformat(), + (probe.checked_at + timedelta(seconds=max(60, interval * 2))).isoformat(), + probe.checked_at.isoformat(), + probe.detail, + json.dumps({"probe_id": probe.id, "trigger": probe.trigger, **probe.metadata}, sort_keys=True), + ), + ) + db.execute( + "INSERT OR IGNORE INTO health_evidence_service_links (evidence_id, service_id) VALUES (?, ?)", + (evidence_id, probe.service_id), + ) + + def activate_demo(self, services: list[Service], probes: list[ProbeResult]) -> ConfigImportResult: + with self._connect() as db: + db.execute("BEGIN IMMEDIATE") + session = db.execute("SELECT * FROM demo_sessions WHERE id = 1").fetchone() + if session is None: + snapshot_services, snapshot_probes, snapshot_events, snapshot_incidents = self._snapshot_rows(db) + snapshot_discovery = self._snapshot_discovery_rows(db) + snapshot_health_evidence = self._snapshot_health_evidence_rows(db) + started_at = _now().isoformat() + else: + snapshot_services = session["snapshot_services"] + snapshot_probes = session["snapshot_probes"] + snapshot_events = session["snapshot_events"] or "[]" + snapshot_incidents = session["snapshot_incidents"] or "[]" + snapshot_discovery = session["snapshot_discovery"] or "{}" + snapshot_health_evidence = session["snapshot_health_evidence"] or "{}" + started_at = session["started_at"] + self._insert_demo_rows(db, services, probes) + db.execute( + """ + INSERT INTO demo_sessions (id, snapshot_services, snapshot_probes, snapshot_events, snapshot_incidents, snapshot_discovery, snapshot_health_evidence, demo_service_ids, started_at) + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET snapshot_services=excluded.snapshot_services, snapshot_probes=excluded.snapshot_probes, + snapshot_events=excluded.snapshot_events, snapshot_incidents=excluded.snapshot_incidents, + snapshot_discovery=excluded.snapshot_discovery, + snapshot_health_evidence=excluded.snapshot_health_evidence, + demo_service_ids=excluded.demo_service_ids, started_at=excluded.started_at + """, + (snapshot_services, snapshot_probes, snapshot_events, snapshot_incidents, snapshot_discovery, snapshot_health_evidence, json.dumps([service.id for service in services]), started_at), + ) + event_bus.publish({"event_type": "topology.changed", "message": "Synthetic demo loaded"}) + return ConfigImportResult(created=len(services), updated=0, total=len(services)) + + def exit_demo(self) -> bool: + with self._connect() as db: + db.execute("BEGIN IMMEDIATE") + session = db.execute("SELECT * FROM demo_sessions WHERE id = 1").fetchone() + if session is None: + return False + self._restore_rows( + db, + session["snapshot_services"], + session["snapshot_probes"], + session["snapshot_events"], + session["snapshot_incidents"], + session["snapshot_discovery"] or "{}", + session["snapshot_health_evidence"] or "{}", + ) + db.execute("DELETE FROM demo_sessions WHERE id = 1") + event_bus.publish({"event_type": "topology.changed", "message": "Synthetic demo exited"}) + return True + + def list_incidents(self, active_only: bool = False, limit: int = 100) -> list[IncidentRecord]: + services = {service.id: service for service in self.list_services()} + with self._connect() as db: + query = "SELECT * FROM incidents" + params: tuple[Any, ...] = () + if active_only: + query += " WHERE status != 'resolved'" + query += " ORDER BY detected_at DESC LIMIT ?" + params += (limit,) + rows = db.execute(query, params).fetchall() + return [self._row_to_incident(row, services) for row in rows] + + def get_incident(self, incident_id: str) -> IncidentRecord | None: + services = {service.id: service for service in self.list_services()} + with self._connect() as db: + row = db.execute("SELECT * FROM incidents WHERE id = ?", (incident_id,)).fetchone() + return self._row_to_incident(row, services) if row else None + + def incident_timeline(self, incident_id: str, limit: int = 200) -> list[IncidentEvent]: + with self._connect() as db: + rows = db.execute( + "SELECT id, incident_id, event_type, message, created_at, service_id FROM events WHERE incident_id = ? ORDER BY created_at ASC LIMIT ?", + (incident_id, limit), + ).fetchall() + return [IncidentEvent(**dict(row)) for row in rows] + + def upsert_incident(self, incident: IncidentRecord) -> IncidentRecord: + with self._connect() as db: + db.execute( + """ + INSERT INTO incidents (id, status, root_service_id, started_at, detected_at, acknowledged_at, resolved_at, + title, root_cause_reason, affected_service_ids, severity, latest_update_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET status=excluded.status, acknowledged_at=excluded.acknowledged_at, + resolved_at=excluded.resolved_at, title=excluded.title, root_cause_reason=excluded.root_cause_reason, + affected_service_ids=excluded.affected_service_ids, severity=excluded.severity, latest_update_at=excluded.latest_update_at + """, + (incident.id, incident.status.value, incident.root_service_id, incident.started_at.isoformat(), incident.detected_at.isoformat(), _iso(incident.acknowledged_at), _iso(incident.resolved_at), incident.title, incident.root_cause_reason, json.dumps(incident.affected_service_ids), incident.severity.value, incident.latest_update_at.isoformat()), + ) + return incident + + def append_incident_event(self, incident_id: str, event_type: str, message: str, service_id: str | None = None) -> IncidentEvent: + created_at = _now() + with self._connect() as db: + event_id = self._insert_event_db(db, event_type, service_id, incident_id, None, None, message, {}, created_at) + payload = {"event_type": event_type, "incident_id": incident_id, "service_id": service_id, "message": message, "created_at": created_at.isoformat()} + event_bus.publish(payload) + return IncidentEvent(id=event_id, incident_id=incident_id, event_type=event_type, message=message, created_at=created_at, service_id=service_id) + + def acknowledge_incident(self, incident_id: str) -> IncidentRecord | None: + incident = self.get_incident(incident_id) + if incident is None: + return None + if incident.status == IncidentStatus.RESOLVED: + raise ValueError("Resolved incidents cannot be acknowledged") + now = _now() + updated = incident.model_copy(update={"status": IncidentStatus.ACKNOWLEDGED, "acknowledged_at": now, "latest_update_at": now}) + self.upsert_incident(updated) + self.append_incident_event(incident_id, "incident.acknowledged", "Incident acknowledged", incident.root_service_id) + return updated diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..e359738 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "servicemap-backend" +version = "1.6.2" +description = "ServiceMap API, topology intelligence and probe engine" +requires-python = ">=3.12" +dependencies = [ + "fastapi>=0.116,<1", + "uvicorn[standard]>=0.35,<1", + "httpx>=0.28,<1", + "pydantic>=2.11,<3", + "pyyaml>=6.0,<7", + "docker>=7.1,<8", + "PyJWT[crypto]>=2.10,<3", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.4,<9", + "ruff>=0.12,<1", +] + +[tool.pytest.ini_options] +pythonpath = ["."] + +[tool.ruff] +line-length = 100 diff --git a/backend/requirements-dev.lock b/backend/requirements-dev.lock new file mode 100644 index 0000000..a8bfcaf --- /dev/null +++ b/backend/requirements-dev.lock @@ -0,0 +1,106 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --extra=dev --output-file=backend/requirements-dev.lock --strip-extras backend/pyproject.toml +# +annotated-doc==0.0.5 + # via fastapi +annotated-types==0.8.0 + # via pydantic +anyio==4.15.1 + # via + # httpx + # starlette + # watchfiles +certifi==2026.7.22 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 + # via cryptography +charset-normalizer==3.5.1 + # via requests +click==8.5.0 + # via uvicorn +colorama==0.4.6 + # via pytest +cryptography==50.0.1 + # via pyjwt +docker==7.2.0 + # via servicemap-backend (backend/pyproject.toml) +fastapi==0.141.1 + # via servicemap-backend (backend/pyproject.toml) +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httptools==0.8.0 + # via uvicorn +httpx==0.28.1 + # via servicemap-backend (backend/pyproject.toml) +idna==3.19 + # via + # anyio + # httpx + # requests +iniconfig==2.3.0 + # via pytest +packaging==26.3 + # via pytest +pluggy==1.6.0 + # via pytest +pycparser==3.0 + # via cffi +pydantic==2.13.5 + # via + # fastapi + # servicemap-backend (backend/pyproject.toml) +pydantic-core==2.46.5 + # via pydantic +pygments==2.21.0 + # via pytest +pyjwt==2.14.0 + # via + # pyjwt + # servicemap-backend (backend/pyproject.toml) +pytest==8.4.2 + # via servicemap-backend (backend/pyproject.toml) +python-dotenv==1.2.3 + # via uvicorn +pywin32==312 ; sys_platform == "win32" + # via docker +pyyaml==6.0.3 + # via + # servicemap-backend (backend/pyproject.toml) + # uvicorn +requests==2.34.2 + # via docker +ruff==0.16.7 + # via servicemap-backend (backend/pyproject.toml) +starlette==1.6.0 + # via fastapi +typing-extensions==4.16.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.4 + # via + # fastapi + # pydantic +urllib3==2.7.0 + # via + # docker + # requests +uvicorn==0.52.4 + # via servicemap-backend (backend/pyproject.toml) +watchfiles==1.2.0 + # via uvicorn +websockets==17.1 + # via uvicorn diff --git a/backend/requirements.lock b/backend/requirements.lock new file mode 100644 index 0000000..3efc68e --- /dev/null +++ b/backend/requirements.lock @@ -0,0 +1,92 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --output-file=backend/requirements.lock --strip-extras backend/pyproject.toml +# +annotated-doc==0.0.5 + # via fastapi +annotated-types==0.8.0 + # via pydantic +anyio==4.15.1 + # via + # httpx + # starlette + # watchfiles +certifi==2026.7.22 + # via + # httpcore + # httpx + # requests +cffi==2.1.1 + # via cryptography +charset-normalizer==3.5.1 + # via requests +click==8.5.0 + # via uvicorn +cryptography==50.0.1 + # via pyjwt +docker==7.2.0 + # via servicemap-backend (backend/pyproject.toml) +fastapi==0.141.1 + # via servicemap-backend (backend/pyproject.toml) +h11==0.16.0 + # via + # httpcore + # uvicorn +httpcore==1.0.9 + # via httpx +httptools==0.8.0 + # via uvicorn +httpx==0.28.1 + # via servicemap-backend (backend/pyproject.toml) +idna==3.19 + # via + # anyio + # httpx + # requests +pycparser==3.0 + # via cffi +pydantic==2.13.5 + # via + # fastapi + # servicemap-backend (backend/pyproject.toml) +pydantic-core==2.46.5 + # via pydantic +pyjwt==2.14.0 + # via + # pyjwt + # servicemap-backend (backend/pyproject.toml) +python-dotenv==1.2.3 + # via uvicorn +pywin32==312 ; sys_platform == "win32" + # via docker +pyyaml==6.0.3 + # via + # servicemap-backend (backend/pyproject.toml) + # uvicorn +requests==2.34.2 + # via docker +starlette==1.6.0 + # via fastapi +typing-extensions==4.16.0 + # via + # anyio + # fastapi + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.4 + # via + # fastapi + # pydantic +urllib3==2.7.0 + # via + # docker + # requests +uvicorn==0.52.4 + # via servicemap-backend (backend/pyproject.toml) +watchfiles==1.2.0 + # via uvicorn +websockets==17.1 + # via uvicorn diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 0000000..403e390 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,182 @@ +from pathlib import Path + +from fastapi.testclient import TestClient + +from app import main as main_module +from app.discovery import DiscoveryManager +from app.discovery_store import DiscoveryStore +from app.models import DiscoveryConfidence, RelationEvidence, RelationType +from app.store import Store + + +def test_discovery_routes_reject_request_controlled_targets(tmp_path: Path, monkeypatch) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + main_module.discovery_store = DiscoveryStore(main_module.store) + main_module.discovery_manager = DiscoveryManager(main_module.store, main_module.discovery_store) + monkeypatch.setenv("SERVICEMAP_DOCKER_HOST", "unix:///var/run/docker.sock") + monkeypatch.setenv("SERVICEMAP_APPOPS_URL", "http://appops.internal:1216") + client = TestClient(main_module.app) + + assert client.post( + "/api/v1/docker/discover", json={"docker_host": "tcp://attacker.internal:2375"} + ).status_code == 403 + assert client.post( + "/api/v1/discovery/run", json={"docker_host": "tcp://attacker.internal:2375"} + ).status_code == 403 + assert client.post( + "/api/v1/discovery/run", json={"appops_url": "http://attacker.internal"} + ).status_code == 403 + + +def test_service_api_crud_cycle_guard_and_export(tmp_path: Path) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + client = TestClient(main_module.app) + + database_response = client.post( + "/api/v1/services", + json={"name": "Database", "kind": "tcp", "host": "db.local", "port": 5432, "depends_on": []}, + ) + assert database_response.status_code == 201 + database = database_response.json() + + api_response = client.post( + "/api/v1/services", + json={ + "name": "API", + "kind": "http", + "url": "https://api.local", + "depends_on": [database["id"]], + }, + ) + assert api_response.status_code == 201 + api_service = api_response.json() + + cycle_response = client.put( + f"/api/v1/services/{database['id']}", + json={ + "name": "Database", + "kind": "tcp", + "host": "db.local", + "port": 5432, + "depends_on": [api_service["id"]], + }, + ) + assert cycle_response.status_code == 422 + assert "cycle" in cycle_response.json()["detail"].lower() + + listing = client.get("/api/v1/services") + assert listing.status_code == 200 + assert len(listing.json()) == 2 + + exported = client.get("/api/v1/config?format=yaml") + assert exported.status_code == 200 + assert "Database" in exported.text + assert "API" in exported.text + + deleted = client.delete(f"/api/v1/services/{database['id']}") + assert deleted.status_code == 204 + api_snapshot = client.get(f"/api/v1/services/{api_service['id']}").json() + assert api_snapshot["service"]["depends_on"] == [] + + +def test_overview_counts_only_visible_services(tmp_path: Path) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + client = TestClient(main_module.app) + for name, visibility in (("Shown", "visible"), ("Retired", "hidden")): + response = client.post( + "/api/v1/services", + json={"name": name, "kind": "tcp", "host": f"{name.lower()}.local", "port": 9000, "visibility": visibility}, + ) + assert response.status_code == 201 + + assert len(client.get("/api/v1/services").json()) == 1 + assert client.get("/api/v1/overview").json()["total_services"] == 1 + + +def test_config_import_endpoint_loads_validated_graph(tmp_path: Path) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + client = TestClient(main_module.app) + content = """ +version: 1 +services: + - name: Redis + kind: tcp + host: redis.local + port: 6379 + depends_on: [] + - name: Worker + kind: tcp + host: worker.local + port: 9000 + depends_on: [Redis] +""" + + response = client.post( + "/api/v1/config/import", + json={"content": content, "format": "yaml", "mode": "replace"}, + ) + + assert response.status_code == 200 + assert response.json() == {"created": 2, "updated": 0, "total": 2} + assert len(client.get("/api/v1/services").json()) == 2 + + +def test_service_api_rejects_invalid_endpoint_and_dependency_references(tmp_path: Path) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + client = TestClient(main_module.app) + + invalid_url = client.post( + "/api/v1/services", + json={"name": "Invalid", "kind": "http", "url": "ftp://example.test"}, + ) + assert invalid_url.status_code == 422 + + missing_dependency = client.post( + "/api/v1/services", + json={ + "name": "API", + "kind": "http", + "url": "https://api.local", + "depends_on": ["missing-service-id"], + }, + ) + assert missing_dependency.status_code == 422 + assert "unknown dependency" in missing_dependency.json()["detail"].lower() + + +def test_semantic_topology_and_logical_relations_are_backend_derived(tmp_path: Path) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + main_module.discovery_store = DiscoveryStore(main_module.store) + main_module.discovery_manager = DiscoveryManager(main_module.store, main_module.discovery_store) + client = TestClient(main_module.app) + database = client.post("/api/v1/services", json={ + "name": "PostgreSQL", "kind": "tcp", "host": "db.local", "port": 5432, + "application": "Zabbix", "domain": "Monitoring", "component_role": "database", + }).json() + api = client.post("/api/v1/services", json={ + "name": "Zabbix API", "kind": "http", "url": "https://zabbix.local", + "application": "Zabbix", "domain": "Monitoring", "component_role": "api", + "depends_on": [database["id"]], + }).json() + now = main_module.store.get_service(database["id"]).created_at + main_module.discovery_store.save_evidence(RelationEvidence( + id="compose-edge", source_service_id=api["id"], target_service_id=database["id"], + relation_type=RelationType.DEPENDS_ON, evidence_type="compose", + confidence=DiscoveryConfidence.HIGH, explanation="Compose dependency", first_seen_at=now, last_seen_at=now, + )) + main_module.discovery_store.save_evidence(RelationEvidence( + id="env-edge", source_service_id=api["id"], target_service_id=database["id"], + relation_type=RelationType.DEPENDS_ON, evidence_type="environment", + confidence=DiscoveryConfidence.MEDIUM, explanation="Environment dependency", first_seen_at=now, last_seen_at=now, + )) + + semantic = client.get("/api/v1/topology/semantic") + relations = client.get("/api/v1/topology/relations") + services = client.get("/api/v1/services") + + assert semantic.status_code == 200 + assert semantic.json()["domains"][0]["name"] == "Monitoring" + assert semantic.json()["applications"][0]["component_count"] == 2 + assert relations.json()[0]["confidence"] == "confirmed" + assert len(relations.json()[0]["evidence"]) == 2 + assert services.json()[0]["availability_24h"]["hours"] == 24 diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py new file mode 100644 index 0000000..a3832ea --- /dev/null +++ b/backend/tests/test_auth.py @@ -0,0 +1,69 @@ +from pathlib import Path + +from fastapi.testclient import TestClient + +from app import auth +from app import main as main_module +from app.store import Store + + +def test_disabled_auth_preserves_v15_api_access(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("SERVICEMAP_AUTH_MODE", "disabled") + main_module.store = Store(str(tmp_path / "servicemap.db")) + client = TestClient(main_module.app) + + response = client.post( + "/api/v1/services", + json={"name": "API", "kind": "http", "url": "https://api.local"}, + ) + + assert response.status_code == 201 + + +def test_oidc_mode_fails_closed_but_health_stays_available(monkeypatch) -> None: + monkeypatch.setenv("SERVICEMAP_AUTH_MODE", "oidc") + monkeypatch.delenv("SERVICEMAP_OIDC_ISSUER", raising=False) + monkeypatch.delenv("SERVICEMAP_OIDC_AUDIENCE", raising=False) + client = TestClient(main_module.app) + + assert client.get("/health").status_code == 200 + assert client.get("/api/v1/services").status_code == 503 + + +def test_oidc_roles_enforce_read_operator_and_admin_boundaries(tmp_path: Path, monkeypatch) -> None: + main_module.store = Store(str(tmp_path / "servicemap.db")) + config = auth.AuthConfig(mode="oidc", issuer="https://id.example", audience="servicemap") + monkeypatch.setattr(auth.AuthConfig, "from_environment", classmethod(lambda cls: config)) + client = TestClient(main_module.app) + + monkeypatch.setattr(auth, "verify_bearer", lambda token, _: {"viewer"}) + assert ( + client.get("/api/v1/services", headers={"Authorization": "Bearer viewer"}).status_code + == 200 + ) + assert ( + client.post( + "/api/v1/services", + headers={"Authorization": "Bearer viewer"}, + json={"name": "Denied", "kind": "http", "url": "https://denied.local"}, + ).status_code + == 403 + ) + + monkeypatch.setattr(auth, "verify_bearer", lambda token, _: {"operator", "viewer"}) + assert ( + client.post( + "/api/v1/services", + headers={"Authorization": "Bearer operator"}, + json={"name": "Allowed", "kind": "http", "url": "https://allowed.local"}, + ).status_code + == 201 + ) + assert ( + client.post( + "/api/v1/config/import", + headers={"Authorization": "Bearer operator"}, + json={"content": "version: 1\nservices: []", "format": "yaml", "mode": "merge"}, + ).status_code + == 403 + ) diff --git a/backend/tests/test_config_io.py b/backend/tests/test_config_io.py new file mode 100644 index 0000000..157ba45 --- /dev/null +++ b/backend/tests/test_config_io.py @@ -0,0 +1,133 @@ +import sqlite3 +from pathlib import Path + +import pytest + +from app.config_io import export_document, import_document, parse_document, serialize_document +from app.models import ServiceCreate, ServiceKind +from app.store import Store + +CONFIG = """ +version: 1 +services: + - name: Database + kind: tcp + host: db.local + port: 5432 + depends_on: [] + - name: API + kind: http + url: https://api.local + depends_on: [Database] +""" + + +def test_yaml_import_export_round_trip(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + result = import_document(store, parse_document(CONFIG, "yaml"), "replace") + + assert result.created == 2 + services = store.list_services() + assert len(services) == 2 + api = store.get_service_by_name("API") + database = store.get_service_by_name("Database") + assert api is not None and database is not None + assert api.depends_on == [database.id] + + exported = export_document(store) + yaml_text = serialize_document(exported, "yaml") + reparsed = parse_document(yaml_text, "yaml") + assert [service.name for service in reparsed.services] == [service.name for service in exported.services] + + +def test_merge_can_reference_existing_service_not_in_document(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db.local", port=5432) + ) + document = parse_document( + """ +version: 1 +services: + - name: API + kind: http + url: https://api.local + depends_on: [Database] +""", + "yaml", + ) + + result = import_document(store, document, "merge") + + assert result.created == 1 + assert result.updated == 0 + api = store.get_service_by_name("API") + assert api is not None + assert api.depends_on == [database.id] + + +def test_import_rejects_missing_dependency_and_cycles(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + missing = parse_document( + """ +version: 1 +services: + - name: API + kind: http + url: https://api.local + depends_on: [Database] +""", + "yaml", + ) + with pytest.raises(ValueError, match="unknown dependencies"): + import_document(store, missing, "replace") + + cyclic = """ +version: 1 +services: + - name: A + kind: http + url: https://a.local + depends_on: [B] + - name: B + kind: http + url: https://b.local + depends_on: [A] +""" + with pytest.raises(ValueError, match="cycle"): + parse_document(cyclic, "yaml") + + with pytest.raises(ValueError, match="less than or equal to 1"): + parse_document("version: 2\nservices: []", "yaml") + + +def test_replace_import_rolls_back_on_mid_transaction_failure( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + store = Store(str(tmp_path / "servicemap.db")) + original = store.create_service( + ServiceCreate(name="Original", kind=ServiceKind.HTTP, url="https://original.local") + ) + document = parse_document( + """ +version: 1 +services: + - name: First + kind: http + url: https://first.local + depends_on: [] + - name: Second + kind: http + url: https://second.local + depends_on: [] +""", + "yaml", + ) + monkeypatch.setattr("app.store.uuid4", lambda: "duplicate-id") + + with pytest.raises(sqlite3.IntegrityError): + import_document(store, document, "replace") + + remaining = store.list_services() + assert [(service.id, service.name) for service in remaining] == [(original.id, "Original")] diff --git a/backend/tests/test_demo.py b/backend/tests/test_demo.py new file mode 100644 index 0000000..3406ec3 --- /dev/null +++ b/backend/tests/test_demo.py @@ -0,0 +1,186 @@ +from datetime import UTC, datetime +from pathlib import Path + +from fastapi.testclient import TestClient + +from app import main as main_module +from app.demo import DEMO_ROOT_CAUSE_NAME, build_demo_records +from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind +from app.store import Store + + +def _client(tmp_path: Path) -> tuple[Store, TestClient]: + store = Store(str(tmp_path / "servicemap.db")) + main_module.store = store + return store, TestClient(main_module.app) + + +def _assert_acyclic(graph: dict[str, set[str]]) -> None: + visiting: set[str] = set() + visited: set[str] = set() + + def visit(service_id: str) -> None: + assert service_id not in visiting + if service_id in visited: + return + visiting.add(service_id) + for dependency_id in graph[service_id]: + visit(dependency_id) + visiting.remove(service_id) + visited.add(service_id) + + for service_id in graph: + visit(service_id) + + +def test_demo_seeds_topology_health_history_availability_and_incident(tmp_path: Path) -> None: + store, client = _client(tmp_path) + + response = client.post("/api/v1/demo") + assert response.status_code == 200 + result = response.json() + assert result["active"] is True + assert result["synthetic"] is True + assert result["scenario_name"] == "Northstar Platform" + assert result["total"] == 9 + + snapshots = client.get("/api/v1/services").json() + assert len(snapshots) == 9 + by_name = {item["service"]["name"]: item for item in snapshots} + assert set(by_name) == { + "Public Endpoint", + "Reverse Proxy", + "Web Frontend", + "Auth Service", + "API Gateway", + "PostgreSQL", + "Redis", + "Background Worker", + "Object Storage", + } + ids = {item["service"]["id"] for item in snapshots} + assert all(set(item["service"]["depends_on"]).issubset(ids) for item in snapshots) + assert by_name[DEMO_ROOT_CAUSE_NAME]["latest_probe"]["status"] == HealthStatus.DOWN + assert by_name[DEMO_ROOT_CAUSE_NAME]["effective_status"] == HealthStatus.DOWN + assert all(item["effective_status"] != HealthStatus.UNKNOWN for item in snapshots) + + semantic = client.get("/api/v1/topology/semantic").json() + assert {item["name"] for item in semantic["domains"]} == {"Edge", "Application", "Data"} + assert {item["name"] for item in semantic["applications"]} == { + "Public Gateway", + "Northstar App", + "Data Platform", + "Worker Platform", + } + + incidents = client.get("/api/v1/incidents").json() + assert incidents["active"] is True + assert incidents["candidates"][0]["service_name"] == DEMO_ROOT_CAUSE_NAME + assert set(incidents["candidates"][0]["affected_service_names"]) == { + "API Gateway", + "Web Frontend", + "Reverse Proxy", + "Public Endpoint", + "Background Worker", + } + + postgres_id = by_name[DEMO_ROOT_CAUSE_NAME]["service"]["id"] + history = client.get(f"/api/v1/services/{postgres_id}/history?limit=50").json() + assert len(history) == 24 + availability = client.get(f"/api/v1/services/{postgres_id}/availability").json() + assert availability["total_checks"] == 24 + assert availability["uptime_percent"] < 100 + assert availability["down_checks"] == 2 + assert store.demo_active() is True + + +def test_demo_reset_is_deterministic_and_exit_restores_topology_and_probes(tmp_path: Path) -> None: + store, client = _client(tmp_path) + original = store.create_service( + ServiceCreate(name="Existing API", kind=ServiceKind.HTTP, url="https://existing.test") + ) + original_probe = ProbeResult( + id="existing-probe", + service_id=original.id, + status=HealthStatus.HEALTHY, + latency_ms=12.5, + detail="existing data", + checked_at=datetime.now(UTC), + ) + store.save_probe(original_probe) + original_service_json = client.get(f"/api/v1/services/{original.id}").json() + + first = client.post("/api/v1/demo").json() + first_services = client.get("/api/v1/services").json() + first_statuses = { + item["service"]["name"]: (item["latest_probe"]["status"], item["effective_status"]) + for item in first_services + } + assert first["restored"] is False + assert client.get(f"/api/v1/services/{original.id}").status_code == 404 + + reset = client.post("/api/v1/demo/reset").json() + reset_services = client.get("/api/v1/services").json() + reset_statuses = { + item["service"]["name"]: (item["latest_probe"]["status"], item["effective_status"]) + for item in reset_services + } + assert reset["active"] is True + assert reset["root_cause_service_name"] == DEMO_ROOT_CAUSE_NAME + assert reset_statuses == first_statuses + assert len(reset_services) == 9 + + exited = client.post("/api/v1/demo/exit").json() + assert exited["active"] is False + assert exited["restored"] is True + assert client.get("/api/v1/demo").json() == { + "active": False, + "synthetic": False, + "scenario_name": None, + "service_count": 0, + "root_cause_service_id": None, + "root_cause_service_name": None, + "affected_service_ids": [], + "affected_service_names": [], + "affected_count": 0, + } + assert client.get(f"/api/v1/services/{original.id}").json() == original_service_json + assert store.probe_history(original.id)[0].id == original_probe.id + + with store._connect() as db: # regression assertion for FK-clean restoration + orphan_count = db.execute( + "SELECT COUNT(*) FROM probe_results p LEFT JOIN services s ON s.id = p.service_id WHERE s.id IS NULL" + ).fetchone()[0] + assert orphan_count == 0 + orphan_evidence_count = db.execute( + "SELECT COUNT(*) FROM health_evidence e LEFT JOIN services s ON s.id = e.service_id " + "WHERE e.service_id IS NOT NULL AND s.id IS NULL" + ).fetchone()[0] + orphan_link_count = db.execute( + "SELECT COUNT(*) FROM health_evidence_service_links link LEFT JOIN health_evidence e " + "ON e.id = link.evidence_id WHERE e.id IS NULL" + ).fetchone()[0] + assert orphan_evidence_count == 0 + assert orphan_link_count == 0 + + +def test_demo_probe_endpoints_remain_external_service_free(tmp_path: Path) -> None: + _, client = _client(tmp_path) + client.post("/api/v1/demo") + snapshots = client.get("/api/v1/services").json() + postgres_id = next(item["service"]["id"] for item in snapshots if item["service"]["name"] == DEMO_ROOT_CAUSE_NAME) + + assert client.post(f"/api/v1/services/{postgres_id}/probe").status_code == 409 + assert client.post("/api/v1/probe-all").json() == {"requested": 0, "completed": 0, "results": []} + + +def test_demo_records_have_valid_graph_and_stable_ids() -> None: + fixed_now = datetime(2026, 9, 12, 10, 0, tzinfo=UTC) + services_a, probes_a = build_demo_records(fixed_now) + services_b, probes_b = build_demo_records(fixed_now) + + assert [service.model_dump(mode="json") for service in services_a] == [service.model_dump(mode="json") for service in services_b] + assert [probe.model_dump(mode="json") for probe in probes_a] == [probe.model_dump(mode="json") for probe in probes_b] + assert len(probes_a) == 9 * 24 + graph = {service.id: set(service.depends_on) for service in services_a} + _assert_acyclic(graph) diff --git a/backend/tests/test_discovery_v120.py b/backend/tests/test_discovery_v120.py new file mode 100644 index 0000000..c141d77 --- /dev/null +++ b/backend/tests/test_discovery_v120.py @@ -0,0 +1,345 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from pathlib import Path +from typing import ClassVar + +import pytest + +from app import docker_discovery +from app.demo import build_demo_records +from app.discovery import DiscoveryManager +from app.discovery_store import DiscoveryStore +from app.models import ( + Criticality, + DiscoveryConfidence, + DiscoveryLifecycle, + DiscoveryReconcileRequest, + DiscoveryRunRequest, + DiscoveryRunStatus, + DockerCandidate, + DockerPort, + DockerSuggestion, + ProbeEndpointCandidate, + ProbeEndpointSource, + ProbeReachability, + RelationEvidence, + RelationType, + ServiceCreate, + ServiceKind, + ServiceUpdate, + SuggestionStatus, + TopologySuggestion, +) +from app.store import Store + + +class _Image: + def __init__(self, tag: str) -> None: + self.tags = [tag] + + +class _Container: + def __init__(self, name: str, container_id: str, image: str, service: str, *, env: list[str] | None = None) -> None: + self.name = name + self.id = container_id + self.status = "running" + self.image = _Image(image) + self.attrs = { + "State": {"Status": "running", "Health": {"Status": "healthy"}}, + "Config": { + "Image": image, + "Env": env or [], + "Healthcheck": {"Test": ["CMD", "true"]}, + "Labels": { + "com.docker.compose.project": "northstar", + "com.docker.compose.service": service, + "com.docker.compose.depends_on": "postgres:service_healthy:false" if service == "api" else "", + }, + }, + "NetworkSettings": { + "Networks": {"northstar_default": {}}, + "Ports": {"8000/tcp": [{"HostIp": "127.0.0.1", "HostPort": "18080"}]} if service == "api" else {"5432/tcp": [{"HostIp": "127.0.0.1", "HostPort": "15432"}]}, + }, + } + + +class _DockerClient: + containers: ClassVar[_DockerClient] + current: ClassVar[list[_Container]] = [] + + def __init__(self, **_: object) -> None: + self.containers = self + + def ping(self) -> bool: + return True + + def list(self, all: bool) -> list[_Container]: + assert all is False + return self.current + + def close(self) -> None: + return None + + +def _manager(tmp_path: Path) -> DiscoveryManager: + store = Store(str(tmp_path / "servicemap.db")) + return DiscoveryManager(store, DiscoveryStore(store)) + + +def test_malformed_multi_host_environment_does_not_abort_discovery() -> None: + evidence = docker_discovery.safe_environment_evidence( + ["UPSTREAM_HOSTS=chimera:6967,localhost:1221,127.0.0.1:1221"] + ) + + assert evidence == ["UPSTREAM_HOSTS hostname=chimera"] + assert docker_discovery.safe_environment_evidence(["BIND_ADDRESS=http://[invalid:8000"]) == [] + + +def test_internal_http_candidate_is_imported_without_an_unsafe_probe_target(tmp_path: Path) -> None: + manager = _manager(tmp_path) + candidate = DockerCandidate( + container_id="a" * 64, + name="internal-web", + image="example/web:1", + state="running", + networks=[], + ports=[DockerPort(container_port="80/tcp")], + stable_source_ref="docker:name:internal-web", + suggestions=[ + DockerSuggestion( + name="internal-web", + kind=ServiceKind.HTTP, + host="internal-web", + port=80, + ) + ], + ) + + payload = manager._payload_for_candidate(candidate) + + assert payload.kind == ServiceKind.HTTP + assert payload.monitoring_enabled is False + assert payload.url is None + assert payload.host is None + assert payload.port is None + + published = candidate.model_copy( + update={ + "name": "published-web", + "suggestions": [ + DockerSuggestion( + name="published-web", + kind=ServiceKind.HTTP, + url="http://127.0.0.1:9494", + port=9494, + ) + ], + "probe_candidates": [ + ProbeEndpointCandidate( + source=ProbeEndpointSource.PUBLISHED_PORT, + kind=ServiceKind.HTTP, + url="http://127.0.0.1:9494", + reachability=ProbeReachability.VERIFIED, + primary=True, + ) + ], + } + ) + published_payload = manager._payload_for_candidate(published) + + assert published_payload.url == "http://127.0.0.1:9494" + assert published_payload.host is None + assert published_payload.port is None + + +def test_new_discovery_run_closes_stale_active_run(tmp_path: Path) -> None: + manager = _manager(tmp_path) + first = manager.discovery_store.start_run() + second = manager.discovery_store.start_run() + + assert manager.discovery_store.get_run(first.id).status == DiscoveryRunStatus.FAILED + assert manager.discovery_store.get_run(second.id).status == DiscoveryRunStatus.DISCOVERING + + +def test_repeated_suggestion_moves_to_latest_discovery_run(tmp_path: Path) -> None: + manager = _manager(tmp_path) + first = manager.discovery_store.start_run() + now = datetime.now(UTC) + suggestion = TopologySuggestion( + id="suggestion-first", + run_id=first.id, + suggestion_type="service", + proposed_name="example", + confidence=DiscoveryConfidence.HIGH, + created_at=now, + updated_at=now, + ) + saved_first = manager.discovery_store.save_suggestion(suggestion, "same-fingerprint") + second = manager.discovery_store.start_run() + saved_second = manager.discovery_store.save_suggestion( + suggestion.model_copy( + update={ + "id": "suggestion-second", + "run_id": second.id, + "status": SuggestionStatus.OPEN, + "updated_at": datetime.now(UTC), + } + ), + "same-fingerprint", + ) + + assert saved_first.id == saved_second.id == "suggestion-first" + assert saved_second.run_id == second.id + + +def test_discovery_sanitizes_environment_and_reconciles_high_confidence_edges( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _DockerClient.current = [ + _Container( + "api", + "a" * 64, + "northstar/api:1", + "api", + env=["DATABASE_URL=postgres://user:supersecret@postgres:5432/app", "API_TOKEN=never-store"], + ), + _Container("postgres", "b" * 64, "postgres:17", "postgres"), + ] + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + + result = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + api = next(item for item in result.candidates if item.name == "api") + assert api.stable_source_ref == "docker:compose:northstar:api" + assert any("hostname=postgres:5432" in item for item in api.environment_evidence) + assert all("supersecret" not in item and "user" not in item for item in api.environment_evidence) + assert all("API_TOKEN" not in item for item in api.environment_evidence) + assert any(item.confidence == DiscoveryConfidence.HIGH for item in result.suggestions if item.suggestion_type == "relation") + + reconciled = manager.reconcile(DiscoveryReconcileRequest(run_id=result.run.id, apply_high_confidence=True)) + assert reconciled.created == 2 + assert reconciled.accepted_relations >= 1 + services = manager.store.list_services() + api_service = next(item for item in services if item.source_ref == "docker:compose:northstar:api") + postgres_service = next(item for item in services if item.source_ref == "docker:compose:northstar:postgres") + assert postgres_service.id in api_service.depends_on + + edge = next(item for item in manager.discovery_store.list_evidence() if item.source_service_id == api_service.id and item.target_service_id == postgres_service.id) + manager.edge_action(edge.id, "reject") + repeated = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=repeated.run.id, apply_high_confidence=True)) + api_after_rejection = next(item for item in manager.store.list_services() if item.id == api_service.id) + assert postgres_service.id not in api_after_rejection.depends_on + assert manager.discovery_store.get_evidence(edge.id) is not None + assert not manager.discovery_store.get_evidence(edge.id).active + + serialized = (tmp_path / "servicemap.db").read_bytes() + assert b"supersecret" not in serialized + assert b"API_TOKEN" not in serialized + + +def test_stable_identity_survives_container_recreation_without_duplicate_service( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _DockerClient.current = [_Container("api", "a" * 64, "northstar/api:1", "api")] + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + first = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=first.run.id, apply_high_confidence=True)) + + _DockerClient.current = [_Container("api", "c" * 64, "northstar/api:2", "api")] + second = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + assert second.run.diff.changed == 1 + manager.reconcile(DiscoveryReconcileRequest(run_id=second.run.id, apply_high_confidence=True)) + services = [item for item in manager.store.list_services() if item.source_ref == "docker:compose:northstar:api"] + assert len(services) == 1 + assert services[0].container_image == "northstar/api:2" + + +def test_missing_container_is_retained_and_becomes_stale(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _DockerClient.current = [_Container("api", "a" * 64, "northstar/api:1", "api")] + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + first = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=first.run.id, apply_high_confidence=True)) + + _DockerClient.current = [] + missing = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + assert missing.run.diff.missing == 1 + reconciled = manager.reconcile(DiscoveryReconcileRequest(run_id=missing.run.id)) + assert reconciled.missing == 1 + service = next(item for item in manager.store.list_services() if item.source_ref == "docker:compose:northstar:api") + assert service.lifecycle == DiscoveryLifecycle.MISSING + assert service.id in {item.id for item in manager.store.list_services()} + + +def test_missing_identity_progresses_to_stale_then_retired_without_delete(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _DockerClient.current = [_Container("api", "a" * 64, "northstar/api:1", "api")] + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + first = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=first.run.id, apply_high_confidence=True)) + _DockerClient.current = [] + lifecycle: list[DiscoveryLifecycle] = [] + for _ in range(3): + run = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=run.run.id)) + lifecycle.append(next(item for item in manager.store.list_services() if item.source_ref == "docker:compose:northstar:api").lifecycle) + assert lifecycle == [DiscoveryLifecycle.MISSING, DiscoveryLifecycle.STALE, DiscoveryLifecycle.RETIRED] + assert len(manager.store.list_services()) == 1 + + +def test_low_confidence_network_hint_is_evidence_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _DockerClient.current = [ + _Container("frontend", "a" * 64, "northstar/frontend:1", "frontend"), + _Container("backend", "b" * 64, "northstar/backend:1", "backend"), + ] + for item in _DockerClient.current: + item.attrs["Config"]["Labels"]["com.docker.compose.depends_on"] = "" + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + run = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=run.run.id, apply_high_confidence=True)) + services = manager.store.list_services() + assert all(not service.depends_on for service in services) + assert any(item.confidence == DiscoveryConfidence.LOW for item in run.suggestions if item.suggestion_type == "relation") + + +def test_manual_group_override_survives_rediscovery(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _DockerClient.current = [_Container("api", "a" * 64, "northstar/api:1", "api")] + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: _DockerClient(**kwargs)) + manager = _manager(tmp_path) + first = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=first.run.id, apply_high_confidence=True)) + before = next(item for item in manager.store.list_services() if item.source_ref == "docker:compose:northstar:api") + payload = ServiceUpdate.model_validate(before.model_dump()) + payload.group = "Manual ownership" + after = manager.store.update_service(before.id, payload) + assert after is not None + manager.record_service_override(before, after) + _DockerClient.current = [_Container("api", "c" * 64, "northstar/api:2", "api")] + second = manager.run(DiscoveryRunRequest(docker_host="tcp://docker.local:2375")) + manager.reconcile(DiscoveryReconcileRequest(run_id=second.run.id, apply_high_confidence=True)) + updated = next(item for item in manager.store.list_services() if item.source_ref == "docker:compose:northstar:api") + assert updated.group == "Manual ownership" + assert updated.container_image == "northstar/api:2" + + +def test_demo_exit_restores_discovery_evidence(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + discovery = DiscoveryStore(store) + database = store.create_service(ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db", port=5432, category="database", criticality=Criticality.HIGH)) + application = store.create_service(ServiceCreate(name="Application", kind=ServiceKind.HTTP, url="https://app.local", depends_on=[database.id])) + source = discovery.save_source(source_type="docker", source_id="docker:compose:stack:app", source_name="app", lifecycle=DiscoveryLifecycle.ACTIVE, metadata={"image": "app:1"}) + discovery.attach_source(application.id, source.id) + now = datetime.now(UTC) + discovery.save_evidence(RelationEvidence(id="evidence-1", source_service_id=application.id, target_service_id=database.id, relation_type=RelationType.DEPENDS_ON, evidence_type="manual", confidence=DiscoveryConfidence.CONFIRMED, explanation="test evidence", first_seen_at=now, last_seen_at=now)) + demo_services, demo_probes = build_demo_records() + store.activate_demo(demo_services, demo_probes) + assert store.demo_active() + assert {item.name for item in store.list_services()} == {item.name for item in demo_services} + assert store.exit_demo() + restored = store.list_services() + assert {item.name for item in restored} == {"Database", "Application"} + assert discovery.get_evidence("evidence-1") is not None diff --git a/backend/tests/test_docker_discovery.py b/backend/tests/test_docker_discovery.py new file mode 100644 index 0000000..8dccf8b --- /dev/null +++ b/backend/tests/test_docker_discovery.py @@ -0,0 +1,135 @@ +from datetime import UTC, datetime +from typing import ClassVar + +import pytest + +from app import docker_discovery +from app.models import Service, ServiceKind + + +class _FakeImage: + tags: ClassVar[list[str]] = ["example/service:1.0"] + + +class _FakeContainer: + id = "abcdef1234567890" + name = "example-service" + status = "running" + image = _FakeImage() + attrs: ClassVar[dict[str, object]] = { + "State": {"Status": "running", "Health": {"Status": "healthy"}}, + "Config": {"Image": "example/service:1.0"}, + "NetworkSettings": { + "Networks": {"default": {}, "monitoring": {}}, + "Ports": { + "8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": 1253}], + "5432/tcp": None, + }, + }, + } + + +class _FakeClient: + def __init__(self, **_: object) -> None: + self.containers = self + self.closed = False + + def ping(self) -> bool: + return True + + def list(self, all: bool) -> list[_FakeContainer]: + assert all is True + return [_FakeContainer()] + + def close(self) -> None: + self.closed = True + + +def test_discovery_normalizes_ports_networks_and_health(monkeypatch: pytest.MonkeyPatch) -> None: + client = _FakeClient() + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: client) + + result = docker_discovery.discover_containers("tcp://docker.local:2375") + + assert result.docker_host == "tcp://docker.local:2375" + assert result.candidates[0].name == "example-service" + assert result.candidates[0].health == "healthy" + assert result.candidates[0].networks == ["default", "monitoring"] + assert {(port.container_port, port.host_port) for port in result.candidates[0].ports} == { + ("8000/tcp", 1253), + ("5432/tcp", None), + } + assert client.closed is True + + +def test_discovery_filters_sensitive_label_keys_but_keeps_discovery_metadata( + monkeypatch: pytest.MonkeyPatch, +) -> None: + token = "do-not-persist-this-token" + labels = { + "servicemap.api-token": token, + "servicemap.password": "do-not-persist-this-password", + "servicemap.name": "Example API", + "servicemap.probe": "http", + "servicemap.port": "8000", + "com.docker.compose.project": "app-stack", + "com.docker.compose.service": "web", + "com.docker.compose.depends_on": "db", + "com.docker.compose.postgres-password": "do-not-persist-this-compose-password", + "org.opencontainers.image.title": "Safe image title", + "other.secret": "not forwarded", + } + attrs = { + **_FakeContainer.attrs, + "Config": {**_FakeContainer.attrs["Config"], "Labels": labels}, + } + monkeypatch.setattr(_FakeContainer, "attrs", attrs) + client = _FakeClient() + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: client) + + result = docker_discovery.discover_containers("tcp://docker.local:2375") + + candidate = result.candidates[0] + serialized = str(candidate.model_dump(mode="json")) + assert token not in serialized + assert "do-not-persist-this-password" not in serialized + assert "do-not-persist-this-compose-password" not in serialized + assert "other.secret" not in candidate.labels + assert candidate.labels == { + "servicemap.name": "Example API", + "servicemap.probe": "http", + "servicemap.port": "8000", + "com.docker.compose.project": "app-stack", + "com.docker.compose.service": "web", + "com.docker.compose.depends_on": "db", + "org.opencontainers.image.title": "Safe image title", + } + assert candidate.stable_source_ref == "docker:compose:app-stack:web" + assert candidate.suggestions[0].kind == ServiceKind.HTTP + + +def test_discovery_rejects_untrusted_scheme() -> None: + with pytest.raises(ValueError, match="must use"): + docker_discovery.discover_containers("file:///var/run/docker.sock") + + +def test_discovery_marks_imported_and_carries_review_metadata(monkeypatch: pytest.MonkeyPatch) -> None: + client = _FakeClient() + monkeypatch.setattr(docker_discovery.docker, "DockerClient", lambda **kwargs: client) + existing = Service( + id="svc-1", + name="example-service", + kind=ServiceKind.HTTP, + url="http://example-service:8000", + source_type="docker", + source_ref="docker:abcdef123456@example/service:1.0", + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + + result = docker_discovery.discover_containers("tcp://docker.local:2375", [existing]) + + candidate = result.candidates[0] + assert candidate.import_state == "already imported" + assert candidate.suggestions[0].source_type == "docker" + assert candidate.suggestions[0].source_ref == "docker:abcdef123456@example/service:1.0" diff --git a/backend/tests/test_docker_provider.py b/backend/tests/test_docker_provider.py new file mode 100644 index 0000000..30c7916 --- /dev/null +++ b/backend/tests/test_docker_provider.py @@ -0,0 +1,244 @@ +import httpx +import pytest + +from app import docker_provider + + +def test_broker_mode_never_falls_back_to_a_socket_or_request_provided_host(monkeypatch) -> None: + monkeypatch.setenv("SERVICEMAP_DOCKER_PROVIDER", "broker") + monkeypatch.delenv("SERVICEMAP_DOCKER_BROKER_URL", raising=False) + monkeypatch.delenv("SERVICEMAP_DOCKER_BROKER_TOKEN_FILE", raising=False) + socket_used = False + + def socket_client(*args, **kwargs): + nonlocal socket_used + socket_used = True + raise AssertionError("broker mode must not create a socket client") + + monkeypatch.setattr(docker_provider.docker, "DockerClient", socket_client) + + with pytest.raises(docker_provider.DockerProviderError, match="Broker mode requires"): + docker_provider.docker_client_for_inventory("https://attacker.invalid") + + assert socket_used is False + + +def test_broker_destination_is_deployment_configuration_only(tmp_path, monkeypatch) -> None: + token_file = tmp_path / "broker-token" + token_file.write_text("test-token", encoding="utf-8") + monkeypatch.setenv("SERVICEMAP_DOCKER_PROVIDER", "broker") + monkeypatch.setenv("SERVICEMAP_DOCKER_BROKER_URL", "https://broker.internal") + monkeypatch.setenv("SERVICEMAP_DOCKER_BROKER_TOKEN_FILE", str(token_file)) + + client = docker_provider.docker_client_for_inventory("https://attacker.invalid") + + assert isinstance(client, docker_provider.BrokerDockerClient) + assert client.base_url == "https://broker.internal" + client.close() + + +def _mocked_broker_client( + monkeypatch, handler, *, base_url="https://broker.internal", token="opaque-token" +): + original_client = httpx.Client + transport = httpx.MockTransport(handler) + options = {} + + def client_factory(**kwargs): + options.update(kwargs) + return original_client(transport=transport, **kwargs) + + monkeypatch.setattr(docker_provider.httpx, "Client", client_factory) + client = docker_provider.BrokerDockerClient(base_url, token) + return client, options + + +def test_inventory_uses_authenticated_fixed_get_and_health_check_has_no_token(monkeypatch) -> None: + requests = [] + + def handler(request): + requests.append(request) + if request.url.path == "/healthz": + return httpx.Response(200) + return httpx.Response(200, json={"items": []}) + + client, options = _mocked_broker_client(monkeypatch, handler) + try: + assert client.ping() is True + assert client.containers.list() == [] + finally: + client.close() + + assert [(request.method, request.url.path) for request in requests] == [ + ("GET", "/healthz"), + ("GET", "/v1/containers"), + ] + assert "authorization" not in requests[0].headers + assert requests[1].headers["authorization"] == "Bearer opaque-token" + assert options["follow_redirects"] is False + assert options["trust_env"] is False + assert 0 < options["timeout"].connect <= 5 + assert 0 < options["timeout"].read <= 5 + + +def test_broker_rejects_other_authenticated_paths_without_requesting_them(monkeypatch) -> None: + requests = [] + client, _ = _mocked_broker_client( + monkeypatch, + lambda request: requests.append(request) or httpx.Response(200, json={"items": []}), + ) + try: + with pytest.raises(docker_provider.DockerProviderError, match="unsupported"): + client._get("/v1/containers/other") + finally: + client.close() + + assert requests == [] + + +@pytest.mark.parametrize( + "base_url", + [ + "ftp://broker.internal", + "https://user:password@broker.internal", + "https://broker.internal?token=secret", + "https://broker.internal/#fragment", + "https:///missing-host", + ], +) +def test_broker_rejects_unsafe_destinations(base_url) -> None: + with pytest.raises(docker_provider.DockerProviderError): + docker_provider.BrokerDockerClient(base_url, "opaque-token") + + +def test_broker_rejects_header_injection_in_token() -> None: + with pytest.raises(docker_provider.DockerProviderError): + docker_provider.BrokerDockerClient("https://broker.internal", "token\r\nX-Leak: yes") + + +@pytest.mark.parametrize( + "payload", + [ + {}, + {"items": "not-a-list"}, + {"items": [{"containerRef": "one"}, "malformed-row"]}, + {"items": [{"containerRef": "one", "name": "one", "state": "running"}]}, + ], +) +def test_broker_rejects_partial_or_malformed_inventory(payload, monkeypatch) -> None: + client, _ = _mocked_broker_client( + monkeypatch, + lambda request: httpx.Response(200, json=payload), + ) + try: + with pytest.raises(docker_provider.DockerProviderError, match="invalid"): + client.containers.list() + finally: + client.close() + + +def test_broker_rejects_non_json_inventory_response(monkeypatch) -> None: + client, _ = _mocked_broker_client( + monkeypatch, + lambda request: httpx.Response( + 200, content=b'{"items":[]}', headers={"content-type": "text/plain"} + ), + ) + try: + with pytest.raises(docker_provider.DockerProviderError, match="invalid"): + client.containers.list() + finally: + client.close() + + +def test_broker_rejects_oversized_inventory_response(monkeypatch) -> None: + monkeypatch.setattr(docker_provider, "_BROKER_MAX_RESPONSE_BYTES", 4) + client, _ = _mocked_broker_client( + monkeypatch, + lambda request: httpx.Response(200, content=b'{"items":[]}'), + ) + try: + with pytest.raises(docker_provider.DockerProviderError, match="invalid"): + client.containers.list() + finally: + client.close() + + +def test_broker_container_preserves_discovery_fields_without_exposing_environment( + monkeypatch, +) -> None: + item = { + "containerRef": "abcdef1234567890", + "name": "/example-service", + "image": "example/service:1.0", + "state": "running", + "health": "healthy", + "healthcheckPresent": True, + "labels": {"servicemap.enabled": "true"}, + "networks": ["default"], + "ports": { + "8000/tcp": [{"hostIp": "0.0.0.0", "hostPort": "1253"}], + "5432/tcp": None, + }, + } + client, _ = _mocked_broker_client( + monkeypatch, + lambda request: httpx.Response(200, json={"items": [item]}), + ) + try: + [container] = client.containers.list() + finally: + client.close() + + assert container.id == item["containerRef"] + assert container.name == "example-service" + assert container.image.tags == [item["image"]] + assert container.attrs["State"] == {"Status": "running", "Health": {"Status": "healthy"}} + assert container.attrs["Config"]["Labels"] == item["labels"] + assert container.attrs["Config"]["Healthcheck"] == {"BrokerReported": True} + assert container.attrs["Config"]["Env"] == [] + assert container.attrs["NetworkSettings"]["Networks"] == {"default": {}} + assert container.attrs["NetworkSettings"]["Ports"] == { + "8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1253"}], + "5432/tcp": [], + } + + +def test_broker_health_status_proves_healthcheck_when_old_schema_omits_flag(monkeypatch) -> None: + item = { + "containerRef": "abcdef1234567890", + "name": "example-service", + "image": "example/service:1.0", + "state": "running", + "health": "healthy", + "labels": {}, + "networks": [], + "ports": {}, + } + client, _ = _mocked_broker_client( + monkeypatch, lambda request: httpx.Response(200, json={"items": [item]}) + ) + try: + [container] = client.containers.list() + finally: + client.close() + + assert container.attrs["Config"]["Healthcheck"] == {"BrokerReported": True} + + +def test_broker_error_messages_do_not_expose_destination_or_credential(monkeypatch) -> None: + def handler(request): + raise httpx.ConnectError( + "failed with opaque-token at https://broker.internal", request=request + ) + + client, _ = _mocked_broker_client(monkeypatch, handler) + try: + with pytest.raises(docker_provider.DockerProviderError) as caught: + client.containers.list() + finally: + client.close() + + assert "Docker broker request failed" in str(caught.value) + assert "opaque-token" not in str(caught.value) + assert "broker.internal" not in str(caught.value) diff --git a/backend/tests/test_health.py b/backend/tests/test_health.py new file mode 100644 index 0000000..9a7c63f --- /dev/null +++ b/backend/tests/test_health.py @@ -0,0 +1,99 @@ +from datetime import UTC, datetime +from pathlib import Path + +import pytest + +from app.health import dependency_status, effective_status, validate_dependencies +from app.incidents import incident_overview +from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind, ServiceUpdate +from app.store import Store + + +def _probe(service_id: str, status: HealthStatus) -> ProbeResult: + return ProbeResult( + id=f"probe-{service_id}-{status.value}", + service_id=service_id, + status=status, + latency_ms=1.0, + detail=status.value, + checked_at=datetime.now(UTC), + ) + + +def test_down_dependency_degrades_healthy_service(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432) + ) + api = store.create_service( + ServiceCreate( + name="API", + kind=ServiceKind.HTTP, + url="https://example.test", + depends_on=[database.id], + ) + ) + store.save_probe(_probe(database.id, HealthStatus.DOWN)) + store.save_probe(_probe(api.id, HealthStatus.HEALTHY)) + + assert dependency_status(api, store) == HealthStatus.DOWN + assert effective_status(api, store) == HealthStatus.DEGRADED + + +def test_service_without_dependencies_has_healthy_dependency_state(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate(name="Standalone", kind=ServiceKind.HTTP, url="https://example.test") + ) + + assert dependency_status(service, store) == HealthStatus.HEALTHY + assert effective_status(service, store) == HealthStatus.UNKNOWN + + +def test_cycle_validation_rejects_recursive_dependencies(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432) + ) + api = store.create_service( + ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id]) + ) + + with pytest.raises(ValueError, match="cycle"): + validate_dependencies([api.id], store, service_id=database.id) + + validate_dependencies([database.id], store, service_id=api.id) + updated = store.update_service( + api.id, + ServiceUpdate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id]), + ) + assert updated is not None + + +def test_incident_overview_finds_root_cause_and_multihop_impact(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432) + ) + api = store.create_service( + ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://example.test", depends_on=[database.id]) + ) + web = store.create_service( + ServiceCreate(name="Web", kind=ServiceKind.HTTP, url="https://web.test", depends_on=[api.id]) + ) + store.save_probe(_probe(database.id, HealthStatus.DOWN)) + store.save_probe(_probe(api.id, HealthStatus.HEALTHY)) + store.save_probe(_probe(web.id, HealthStatus.HEALTHY)) + + assert effective_status(api, store) == HealthStatus.DEGRADED + assert dependency_status(web, store) == HealthStatus.DEGRADED + assert effective_status(web, store) == HealthStatus.DEGRADED + + overview = incident_overview(store) + + assert overview.active is True + assert overview.down_service_ids == [database.id] + assert set(overview.degraded_service_ids) == {api.id, web.id} + assert overview.candidates[0].service_id == database.id + assert api.id in overview.candidates[0].affected_service_ids + assert web.id in overview.candidates[0].affected_service_ids diff --git a/backend/tests/test_probes.py b/backend/tests/test_probes.py new file mode 100644 index 0000000..fdc5ff9 --- /dev/null +++ b/backend/tests/test_probes.py @@ -0,0 +1,156 @@ +import asyncio +from datetime import UTC, datetime +from typing import Self + +import pytest + +from app import probes +from app.models import HealthStatus, ProbeResult, Service, ServiceKind + + +def _service() -> Service: + now = datetime.now(UTC) + return Service( + id="service-1", + name="API", + kind=ServiceKind.HTTP, + url="https://api.local", + created_at=now, + updated_at=now, + ) + + +def _tcp_service() -> Service: + now = datetime.now(UTC) + return Service( + id="tcp-service-1", + name="TCP service", + kind=ServiceKind.TCP, + host="tcp.local", + port=5432, + created_at=now, + updated_at=now, + ) + + +@pytest.mark.parametrize( + ("status_code", "expected"), + [ + (200, HealthStatus.HEALTHY), + (302, HealthStatus.HEALTHY), + (404, HealthStatus.DEGRADED), + (503, HealthStatus.DOWN), + ], +) +def test_http_status_classification( + monkeypatch: pytest.MonkeyPatch, + status_code: int, + expected: HealthStatus, +) -> None: + class FakeResponse: + def __init__(self, code: int) -> None: + self.status_code = code + + class FakeClient: + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *args: object) -> None: + return None + + async def get(self, url: str) -> FakeResponse: + assert url == "https://api.local" + return FakeResponse(status_code) + + monkeypatch.setattr(probes.httpx, "AsyncClient", lambda **kwargs: FakeClient()) + + status, detail = asyncio.run(probes._probe_once(_service(), 1.0)) + + assert status == expected + assert detail == f"HTTP {status_code}" + + +def test_retry_latency_reports_final_attempt_not_backoff( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls = 0 + + async def fake_probe_once(service: Service, timeout_seconds: float) -> tuple[HealthStatus, str]: + nonlocal calls + calls += 1 + if calls == 1: + raise OSError("temporary failure") + return HealthStatus.HEALTHY, "HTTP 200" + + timings = iter([10.0, 10.05, 20.0, 20.02]) + monkeypatch.setattr(probes, "_probe_once", fake_probe_once) + monkeypatch.setattr(probes, "perf_counter", lambda: next(timings)) + + result = asyncio.run(probes.probe_service(_service(), timeout_seconds=1.0, retries=1)) + + assert calls == 2 + assert result.status == HealthStatus.HEALTHY + assert result.latency_ms == pytest.approx(20.0) + assert result.detail == "HTTP 200 after 2 attempts" + + +def test_tcp_probe_success_and_failure(monkeypatch: pytest.MonkeyPatch) -> None: + class FakeWriter: + def close(self) -> None: + return None + + async def wait_closed(self) -> None: + return None + + async def fake_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]: + assert (host, port) == ("tcp.local", 5432) + return object(), FakeWriter() + + monkeypatch.setattr(probes.asyncio, "open_connection", fake_open_connection) + status, detail = asyncio.run(probes._probe_once(_tcp_service(), 1.0)) + assert status == HealthStatus.HEALTHY + assert detail == "TCP tcp.local:5432 reachable" + + async def failing_open_connection(host: str | None, port: int | None) -> tuple[object, FakeWriter]: + raise ConnectionRefusedError("refused") + + monkeypatch.setattr(probes.asyncio, "open_connection", failing_open_connection) + result = asyncio.run(probes.probe_service(_tcp_service(), timeout_seconds=1.0, retries=0)) + assert result.status == HealthStatus.DOWN + assert "ConnectionRefusedError" in result.detail + + +def test_probe_many_respects_concurrency_boundary(monkeypatch: pytest.MonkeyPatch) -> None: + services = [ + _service().model_copy(update={"id": f"service-{index}", "name": f"API-{index}"}) + for index in range(5) + ] + active = 0 + maximum = 0 + + async def fake_probe_service( + service: Service, + *, + timeout_seconds: float, + retries: int, + ) -> ProbeResult: + nonlocal active, maximum + active += 1 + maximum = max(maximum, active) + await asyncio.sleep(0) + active -= 1 + return ProbeResult( + id=f"probe-{service.id}", + service_id=service.id, + status=HealthStatus.HEALTHY, + latency_ms=1.0, + detail="ok", + checked_at=datetime.now(UTC), + ) + + monkeypatch.setattr(probes, "probe_service", fake_probe_service) + + results = asyncio.run(probes.probe_many(services, concurrency=2)) + + assert len(results) == len(services) + assert maximum <= 2 diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py new file mode 100644 index 0000000..0c7bceb --- /dev/null +++ b/backend/tests/test_scheduler.py @@ -0,0 +1,69 @@ +import asyncio +from contextlib import suppress + +from app.scheduler import scheduled_probe_loop + + +class EmptyStore: + def __init__(self) -> None: + self.prune_calls = 0 + + def list_services(self) -> list[object]: + return [] + + def prune_probes(self, retention_days: int) -> int: + assert retention_days == 30 + self.prune_calls += 1 + return 0 + + +class SyntheticStore(EmptyStore): + def demo_active(self) -> bool: + return True + + def list_services(self) -> list[object]: + raise AssertionError("synthetic demo must not start live probes") + + +def test_scheduler_runs_first_cycle_before_initial_sleep() -> None: + async def scenario() -> None: + store = EmptyStore() + task = asyncio.create_task( + scheduled_probe_loop( + store, # type: ignore[arg-type] + interval_seconds=3600, + timeout_seconds=1.0, + retries=0, + concurrency=1, + retention_days=30, + ) + ) + await asyncio.sleep(0) + assert store.prune_calls == 1 + task.cancel() + with suppress(asyncio.CancelledError): + await task + + asyncio.run(scenario()) + + +def test_scheduler_pauses_live_probes_for_synthetic_demo() -> None: + async def scenario() -> None: + store = SyntheticStore() + task = asyncio.create_task( + scheduled_probe_loop( + store, # type: ignore[arg-type] + interval_seconds=3600, + timeout_seconds=1.0, + retries=0, + concurrency=1, + retention_days=30, + ) + ) + await asyncio.sleep(0) + assert store.prune_calls == 1 + task.cancel() + with suppress(asyncio.CancelledError): + await task + + asyncio.run(scenario()) diff --git a/backend/tests/test_store.py b/backend/tests/test_store.py new file mode 100644 index 0000000..7c9d581 --- /dev/null +++ b/backend/tests/test_store.py @@ -0,0 +1,107 @@ +from datetime import UTC, datetime, timedelta +from pathlib import Path + +from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind, ServiceUpdate +from app.store import Store + + +def test_service_round_trip_and_update(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + created = store.create_service( + ServiceCreate(name="Example API", kind=ServiceKind.HTTP, url="https://example.test") + ) + + loaded = store.get_service(created.id) + assert loaded is not None + assert loaded.name == "Example API" + assert loaded.kind == ServiceKind.HTTP + assert loaded.url == "https://example.test" + + updated = store.update_service( + created.id, + ServiceUpdate(name="Example TCP", kind=ServiceKind.TCP, host="127.0.0.1", port=443), + ) + assert updated is not None + assert updated.kind == ServiceKind.TCP + assert updated.host == "127.0.0.1" + assert updated.port == 443 + assert updated.updated_at >= created.updated_at + + +def test_delete_service_cleans_downstream_dependency(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="127.0.0.1", port=5432) + ) + api = store.create_service( + ServiceCreate( + name="API", + kind=ServiceKind.HTTP, + url="https://example.test", + depends_on=[database.id], + ) + ) + + assert store.delete_service(database.id) is True + loaded_api = store.get_service(api.id) + assert loaded_api is not None + assert loaded_api.depends_on == [] + assert store.delete_service(database.id) is False + + +def test_stale_probe_after_service_delete_is_ignored(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate(name="Temporary", kind=ServiceKind.HTTP, url="https://example.test") + ) + assert store.delete_service(service.id) is True + + probe = ProbeResult( + id="stale-probe", + service_id=service.id, + status=HealthStatus.HEALTHY, + latency_ms=1.0, + detail="stale", + checked_at=datetime.now(UTC), + ) + assert store.save_probe(probe) == probe + assert store.probe_history(service.id) == [] + + +def test_availability_summary_and_retention(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://example.test") + ) + now = datetime.now(UTC) + for index, probe_status in enumerate((HealthStatus.HEALTHY, HealthStatus.DEGRADED, HealthStatus.DOWN)): + store.save_probe( + ProbeResult( + id=f"probe-{index}", + service_id=service.id, + status=probe_status, + latency_ms=float(10 + index), + detail=probe_status.value, + checked_at=now - timedelta(minutes=index), + ) + ) + store.save_probe( + ProbeResult( + id="old-probe", + service_id=service.id, + status=HealthStatus.DOWN, + latency_ms=99.0, + detail="old", + checked_at=now - timedelta(days=40), + ) + ) + + summary = store.availability_summary(service.id, hours=24) + assert summary.total_checks == 3 + assert summary.healthy_checks == 1 + assert summary.degraded_checks == 1 + assert summary.down_checks == 1 + assert summary.uptime_percent == 66.67 + assert summary.average_latency_ms == 11.0 + + assert store.prune_probes(30) == 1 diff --git a/backend/tests/test_v110_features.py b/backend/tests/test_v110_features.py new file mode 100644 index 0000000..9dd123c --- /dev/null +++ b/backend/tests/test_v110_features.py @@ -0,0 +1,159 @@ +import json +import sqlite3 +from datetime import UTC, datetime, timedelta +from pathlib import Path +from uuid import uuid4 + +from app.config_io import export_document, import_document, parse_document +from app.incidents import sync_incidents +from app.models import HealthStatus, ProbeResult, ServiceCreate, ServiceKind +from app.store import Store + + +def _probe(service_id: str, status: HealthStatus, number: int) -> ProbeResult: + return ProbeResult( + id=f"probe-{service_id}-{number}", + service_id=service_id, + status=status, + latency_ms=4.0, + detail=status.value, + checked_at=datetime.now(UTC) + timedelta(microseconds=number), + ) + + +def test_v110_schema_migrates_v101_database_without_reset(tmp_path: Path) -> None: + path = tmp_path / "servicemap.db" + with sqlite3.connect(path) as db: + db.executescript( + """ + CREATE TABLE services ( + id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, + url TEXT, host TEXT, port INTEGER, depends_on TEXT NOT NULL DEFAULT '[]', + created_at TEXT NOT NULL, updated_at TEXT + ); + CREATE TABLE probe_results ( + id TEXT PRIMARY KEY, service_id TEXT NOT NULL, status TEXT NOT NULL, + latency_ms REAL, detail TEXT NOT NULL, checked_at TEXT NOT NULL + ); + CREATE TABLE demo_sessions ( + id INTEGER PRIMARY KEY CHECK (id = 1), snapshot_services TEXT NOT NULL, + snapshot_probes TEXT NOT NULL, demo_service_ids TEXT NOT NULL, started_at TEXT NOT NULL + ); + """ + ) + created = datetime.now(UTC).isoformat() + db.execute( + "INSERT INTO services VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + ("legacy", "Legacy API", "http", "https://legacy.local", None, None, "[]", created, created), + ) + db.execute( + "INSERT INTO probe_results VALUES (?, ?, ?, ?, ?, ?)", + ("legacy-probe", "legacy", "healthy", 12.0, "legacy", created), + ) + + store = Store(str(path)) + service = store.get_service("legacy") + assert service is not None + assert service.name == "Legacy API" + assert store.service_health("legacy") == HealthStatus.HEALTHY + assert store.latest_probe("legacy") is not None + assert store.path == path + + +def test_failure_and_recovery_thresholds_are_explicit(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate( + name="Threshold API", + kind=ServiceKind.HTTP, + url="https://threshold.local", + failure_threshold=2, + recovery_threshold=2, + ) + ) + + store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) + assert store.service_health(service.id) == HealthStatus.UNKNOWN + store.save_probe(_probe(service.id, HealthStatus.DOWN, 2)) + assert store.service_health(service.id) == HealthStatus.DOWN + store.save_probe(_probe(service.id, HealthStatus.HEALTHY, 3)) + assert store.service_health(service.id) == HealthStatus.RECOVERING + store.save_probe(_probe(service.id, HealthStatus.HEALTHY, 4)) + assert store.service_health(service.id) == HealthStatus.HEALTHY + + +def test_incident_journal_acknowledges_resolves_and_does_not_duplicate(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service(ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db.local", port=5432)) + api = store.create_service(ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local", depends_on=[database.id])) + + store.save_probe(_probe(database.id, HealthStatus.DOWN, 1)) + store.save_probe(_probe(database.id, HealthStatus.DOWN, 2)) + store.save_probe(_probe(api.id, HealthStatus.HEALTHY, 1)) + active = store.list_incidents(active_only=True) + assert len(active) == 1 + assert active[0].root_service_id == database.id + assert api.id in active[0].affected_service_ids + assert len([event for event in store.list_events(100) if event.event_type == "incident.opened"]) == 1 + + acknowledged = store.acknowledge_incident(active[0].id) + assert acknowledged is not None + assert acknowledged.status.value == "acknowledged" + store.save_probe(_probe(database.id, HealthStatus.HEALTHY, 3)) + resolved = store.get_incident(active[0].id) + assert resolved is not None + assert resolved.status.value == "resolved" + assert resolved.resolved_at is not None + assert any(event.event_type == "incident.resolved" for event in store.list_events(100)) + + +def test_incident_reconciliation_resolves_existing_duplicate_active_roots(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service(ServiceCreate(name="Duplicate root", kind=ServiceKind.TCP, host="db.local", port=5432)) + store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) + store.save_probe(_probe(service.id, HealthStatus.DOWN, 2)) + original = store.list_incidents(active_only=True)[0] + duplicate = original.model_copy(update={"id": str(uuid4()), "detected_at": original.detected_at + timedelta(seconds=1)}) + store.upsert_incident(duplicate) + + sync_incidents(store) + + active = store.list_incidents(active_only=True) + assert len(active) == 1 + assert active[0].root_service_id == service.id + assert store.get_incident(duplicate.id).status.value == "resolved" + assert any(event.event_type == "incident.duplicate_resolved" for event in store.incident_timeline(duplicate.id)) + + +def test_maintenance_suppresses_incident_and_config_v2_round_trips(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + document = parse_document( + """ +version: 2 +services: + - name: DNS edge + kind: dns + host: edge.internal + group: Edge + environment: staging + tags: [dns, edge] + criticality: high + interval_seconds: 15 + depends_on: [] +""", + "yaml", + ) + result = import_document(store, document, "replace") + assert result.created == 1 + service = store.get_service_by_name("DNS edge") + assert service is not None + assert service.group == "Edge" + assert service.interval_seconds == 15 + assert parse_document(json.dumps(export_document(store).model_dump(mode="json")), "json").version == 2 + + updated = store.set_maintenance(service.id, True, "planned work") + assert updated is not None + assert store.service_health(service.id) == HealthStatus.MAINTENANCE + store.save_probe(_probe(service.id, HealthStatus.DOWN, 1)) + sync_incidents(store) + assert store.list_incidents(active_only=True) == [] diff --git a/backend/tests/test_v130_probe_reachability.py b/backend/tests/test_v130_probe_reachability.py new file mode 100644 index 0000000..1d0f9d5 --- /dev/null +++ b/backend/tests/test_v130_probe_reachability.py @@ -0,0 +1,322 @@ +from __future__ import annotations + +from pathlib import Path + +from app.discovery import DiscoveryManager +from app.discovery_store import DiscoveryStore +from app.docker_discovery import _kind_and_endpoint, _preflight_candidate +from app.models import ( + DiscoveryCategory, + DiscoveryConfidence, + DiscoveryRunStatus, + DockerCandidate, + DockerPort, + DockerSuggestion, + HealthStatus, + MonitoringState, + ProbeEndpointCandidate, + ProbeEndpointSource, + ProbeReachability, + ProbeResult, + ServiceCreate, + ServiceKind, +) +from app.store import Store + + +def _manager(tmp_path: Path) -> DiscoveryManager: + store = Store(str(tmp_path / "servicemap.db")) + return DiscoveryManager(store, DiscoveryStore(store)) + + +def _candidate( + name: str, + reference: str, + *, + project: str | None = None, + compose_service: str | None = None, + labels: dict[str, str] | None = None, + networks: list[str] | None = None, +) -> DockerCandidate: + return DockerCandidate( + container_id=reference[-12:], + name=name, + image="example/service:1", + state="running", + networks=networks or [], + ports=[], + labels=labels or {}, + stable_source_ref=reference, + compose_project=project, + compose_service=compose_service, + category=DiscoveryCategory.APPLICATION, + ) + + +def test_internal_docker_dns_is_evidence_not_an_active_probe() -> None: + kind, url, host, port, evidence = _kind_and_endpoint( + "postgres", + [DockerPort(container_port="5432/tcp")], + {}, + ) + + assert kind == ServiceKind.TCP + assert url is None + assert host == "postgres" + assert port == 5432 + assert "internal container endpoint candidate" in evidence + + +def test_published_port_requires_an_explicit_probe_host() -> None: + ports = [DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=1226)] + labels = {"servicemap.probe": "http"} + _, unsafe_url, unsafe_host, _, _ = _kind_and_endpoint("app", ports, labels) + kind, url, host, port, evidence = _kind_and_endpoint("app", ports, labels, "monitoring-host") + + assert unsafe_url is None + assert unsafe_host == "app" + assert kind == ServiceKind.HTTP + assert url == "http://monitoring-host:1226" + assert host is None and port is None + assert "host-published endpoint" in evidence + + +def test_running_container_without_reachable_endpoint_is_runtime_observed(tmp_path: Path) -> None: + manager = _manager(tmp_path) + candidate = _candidate("worker", "docker:name:worker") + candidate.suggestions = [DockerSuggestion(name="worker", kind=ServiceKind.TCP, host="worker", port=9000)] + candidate.probe_candidates = [ + ProbeEndpointCandidate( + source=ProbeEndpointSource.CONTAINER_INTERNAL, + kind=ServiceKind.TCP, + host="worker", + port=9000, + reachability=ProbeReachability.INTERNAL_ONLY, + ) + ] + + payload = manager._payload_for_candidate(candidate) + + assert payload.monitoring_enabled is False + assert payload.monitoring_state.value == "observed" + assert payload.probe_reachability == ProbeReachability.INTERNAL_ONLY + + +def test_docker_healthcheck_observation_does_not_enable_network_probe(tmp_path: Path) -> None: + manager = _manager(tmp_path) + candidate = _candidate("database", "docker:name:database") + candidate.healthcheck_present = True + candidate.probe_candidates = [ + ProbeEndpointCandidate( + source=ProbeEndpointSource.DOCKER_HEALTHCHECK, + kind=ServiceKind.TCP, + reachability=ProbeReachability.VERIFIED, + confidence=DiscoveryConfidence.HIGH, + ) + ] + + payload = manager._payload_for_candidate(candidate) + + assert payload.monitoring_enabled is False + assert payload.monitoring_state.value == "docker_health" + assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK + + +def test_appops_application_health_is_provenance_not_component_health(tmp_path: Path) -> None: + manager = _manager(tmp_path) + candidate = _candidate("application", "docker:name:application") + candidate.healthcheck_present = True + candidate.appops_health = "healthy" + candidate.appops_app_id = "northstar" + candidate.appops_discovery_confidence = 0.9 + + payload = manager._payload_for_candidate(candidate) + + assert payload.monitoring_enabled is False + assert payload.monitoring_state == MonitoringState.DOCKER_HEALTH + assert payload.probe_source == ProbeEndpointSource.DOCKER_HEALTHCHECK + assert payload.runtime_health is None + + service = manager.store.create_service(payload) + manager._record_candidate_evidence(candidate, service) + resolution = manager.store.health_resolution(service.id) + + assert resolution.selected_evidence_id is None + assert resolution.evidence[0].scope.value == "application" + assert "not used as component" in resolution.explanation + + +def test_compose_reference_prefers_same_project_over_duplicate_stack_name(tmp_path: Path) -> None: + manager = _manager(tmp_path) + run = manager.discovery_store.start_run() + source = _candidate( + "api-a", + "docker:compose:stack-a:api", + project="stack-a", + compose_service="api", + labels={"com.docker.compose.depends_on": "postgres:service_healthy:false"}, + ) + postgres_a = _candidate("postgres-a", "docker:compose:stack-a:postgres", project="stack-a", compose_service="postgres") + postgres_b = _candidate("postgres-b", "docker:compose:stack-b:postgres", project="stack-b", compose_service="postgres") + + suggestions = manager._relation_suggestions(run, [source, postgres_a, postgres_b], {}) + + assert len(suggestions) == 1 + assert suggestions[0].target_ref == postgres_a.stable_source_ref + assert suggestions[0].confidence == DiscoveryConfidence.HIGH + + +def test_ambiguous_global_name_stays_review_only(tmp_path: Path) -> None: + manager = _manager(tmp_path) + run = manager.discovery_store.start_run() + source = _candidate( + "api", + "docker:name:api", + labels={"servicemap.depends_on": "redis"}, + ) + redis_a = _candidate("redis-a", "docker:compose:a:redis", project="a", compose_service="redis") + redis_b = _candidate("redis-b", "docker:compose:b:redis", project="b", compose_service="redis") + + suggestions = manager._relation_suggestions(run, [source, redis_a, redis_b], {}) + + assert len(suggestions) == 1 + assert suggestions[0].target_ref is None + assert suggestions[0].confidence == DiscoveryConfidence.LOW + assert "Manual review required" in " ".join(suggestions[0].evidence) + + +def test_reachability_repair_is_dry_run_first_and_preserves_probe_history(tmp_path: Path) -> None: + manager = _manager(tmp_path) + candidate = _candidate("legacy-db", "docker:name:legacy-db") + candidate.suggestions = [DockerSuggestion(name="legacy-db", kind=ServiceKind.TCP, host="legacy-db", port=5432)] + candidate.probe_candidates = [ + ProbeEndpointCandidate( + source=ProbeEndpointSource.CONTAINER_INTERNAL, + kind=ServiceKind.TCP, + host="legacy-db", + port=5432, + reachability=ProbeReachability.INTERNAL_ONLY, + ) + ] + legacy = manager.store.create_service( + ServiceCreate( + name="legacy-db", + kind=ServiceKind.TCP, + host="legacy-db", + port=5432, + source_type="docker", + source_ref=candidate.stable_source_ref, + ) + ) + manager.store.save_probe( + ProbeResult( + id="legacy-probe", + service_id=legacy.id, + status=HealthStatus.DOWN, + latency_ms=None, + detail="legacy unreachable internal DNS target", + checked_at=legacy.created_at, + ) + ) + run = manager.discovery_store.start_run() + source = manager.discovery_store.save_source( + source_type="docker", + source_id=candidate.stable_source_ref or "", + source_name=candidate.name, + lifecycle=candidate.lifecycle, + metadata={"candidate": candidate.model_dump(mode="json")}, + ) + manager.discovery_store.save_observation( + run_id=run.id, + source=source, + source_name=candidate.name, + lifecycle=candidate.lifecycle, + change_type="changed", + metadata={"candidate": candidate.model_dump(mode="json")}, + ) + manager.discovery_store.finish_run(run.id, status=DiscoveryRunStatus.COMPLETED, source_counts={}, diff=run.diff) + + preview = manager.repair_probe_reachability(run_id=run.id, dry_run=True) + assert preview.changed == 1 + assert manager.store.get_service(legacy.id).monitoring_enabled is True # type: ignore[union-attr] + + applied = manager.repair_probe_reachability(run_id=run.id, dry_run=False) + updated = manager.store.get_service(legacy.id) + assert applied.disabled_unsafe == 1 + assert updated is not None and updated.monitoring_enabled is False + assert updated.monitoring_state.value == "observed" + assert len(manager.store.probe_history(legacy.id)) == 1 + + +def test_container_http_port_is_not_misclassified_by_a_custom_host_port() -> None: + kind, url, host, port, evidence = _kind_and_endpoint( + "adminer", + [DockerPort(container_port="8080/tcp", host_ip="0.0.0.0", host_port=8070)], + {}, + "127.0.0.1", + ) + + assert kind == ServiceKind.HTTP + assert url == "http://127.0.0.1:8070" + assert host is None and port is None + assert "HTTP-like container port" in evidence + + +def test_published_preflight_requires_a_real_tcp_connection(monkeypatch) -> None: + candidate = ProbeEndpointCandidate( + source=ProbeEndpointSource.PUBLISHED_PORT, + kind=ServiceKind.TCP, + host="127.0.0.1", + port=3306, + ) + + class _Connection: + def __enter__(self): + return self + + def __exit__(self, *_: object) -> None: + return None + + monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection()) + verified = _preflight_candidate(candidate) + + assert verified.primary is True + assert verified.reachability == ProbeReachability.VERIFIED + assert verified.preflight_detail == "TCP connect succeeded" + + +def test_http_preflight_records_response_without_scanning_paths(monkeypatch) -> None: + candidate = ProbeEndpointCandidate( + source=ProbeEndpointSource.PUBLISHED_PORT, + kind=ServiceKind.HTTP, + url="http://127.0.0.1:8070", + ) + + class _Connection: + def __enter__(self): + return self + + def __exit__(self, *_: object) -> None: + return None + + class _Response: + status_code = 204 + + class _Client: + def __enter__(self): + return self + + def __exit__(self, *_: object) -> None: + return None + + def request(self, method: str, url: str) -> _Response: + assert method == "HEAD" + assert url == candidate.url + return _Response() + + monkeypatch.setattr("app.docker_discovery.socket.create_connection", lambda *_args, **_kwargs: _Connection()) + monkeypatch.setattr("app.docker_discovery.httpx.Client", lambda **_kwargs: _Client()) + verified = _preflight_candidate(candidate) + + assert verified.primary is True + assert verified.preflight_detail == "HTTP 204 responded" diff --git a/backend/tests/test_v140_semantic.py b/backend/tests/test_v140_semantic.py new file mode 100644 index 0000000..0d1123b --- /dev/null +++ b/backend/tests/test_v140_semantic.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from pathlib import Path + +from app.discovery_store import DiscoveryStore +from app.models import ( + DiscoveryConfidence, + HealthStatus, + MonitoringState, + RelationEvidence, + RelationType, + Service, + ServiceCreate, + ServiceKind, + ServiceSnapshot, + TopologySuggestion, +) +from app.semantic import build_semantic_topology +from app.store import Store + + +def _service(identifier: str, name: str, *, application: str | None = None, category: str = "application") -> Service: + now = datetime.now(UTC) + return Service( + id=identifier, + name=name, + kind=ServiceKind.HTTP, + url=f"https://{identifier}.local", + application=application, + category=category, + created_at=now, + updated_at=now, + ) + + +def _snapshot(service: Service, direct: HealthStatus, effective: HealthStatus | None = None) -> ServiceSnapshot: + return ServiceSnapshot( + service=service, + direct_status=direct, + dependency_status=effective or direct, + effective_status=effective or direct, + ) + + +def test_semantic_topology_keeps_singletons_as_distinct_applications() -> None: + snapshots = [ + _snapshot(_service("one", "One", category="application"), HealthStatus.HEALTHY), + _snapshot(_service("two", "Two", category="application"), HealthStatus.HEALTHY), + ] + + topology = build_semantic_topology(snapshots, []) + + assert {app.name for app in topology.applications} == {"One", "Two"} + assert all(app.component_count == 1 for app in topology.applications) + + +def test_semantic_app_health_is_explainable_and_ignores_unmonitored_auxiliaries() -> None: + postgres = _service("postgres", "Zabbix PostgreSQL", application="Zabbix", category="database") + server = _service("server", "Zabbix Server", application="Zabbix", category="monitoring") + agent = _service("agent", "Zabbix Agent", application="Zabbix", category="monitoring") + agent.monitoring_enabled = False + agent.monitoring_state = MonitoringState.UNMONITORED + topology = build_semantic_topology([ + _snapshot(postgres, HealthStatus.DOWN), + _snapshot(server, HealthStatus.HEALTHY), + _snapshot(agent, HealthStatus.UNMONITORED), + ], [], root_service_ids={"postgres"}, affected_service_ids={"server"}) + + app = next(item for item in topology.applications if item.name == "Zabbix") + assert app.status == HealthStatus.DEGRADED + assert app.root_cause is True + assert "PostgreSQL" in app.status_explanation + assert app.monitored_components == 2 + assert app.unmonitored_components == 1 + components = {item.service_id: item for item in topology.components} + assert components["postgres"].root_cause is True + assert components["server"].root_cause is False + assert components["server"].affected is True + + +def test_logical_relation_merges_independent_evidence_and_improves_confidence(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + source = store.create_service(ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local")) + target = store.create_service(ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db", port=5432)) + discovery = DiscoveryStore(store) + now = datetime.now(UTC) + for evidence_type, confidence, explanation in ( + ("compose", DiscoveryConfidence.MEDIUM, "Compose depends_on declares database"), + ("environment", DiscoveryConfidence.HIGH, "DATABASE_HOST resolves to database"), + ): + discovery.save_evidence(RelationEvidence( + id=f"evidence-{evidence_type}", + source_service_id=source.id, + target_service_id=target.id, + relation_type=RelationType.DEPENDS_ON, + evidence_type=evidence_type, + confidence=confidence, + explanation=explanation, + source_ref=f"docker:compose:stack:{evidence_type}", + first_seen_at=now, + last_seen_at=now, + )) + + relations = discovery.list_logical_relations() + + assert len(relations) == 1 + assert relations[0].confidence == DiscoveryConfidence.CONFIRMED + assert {item.evidence_type for item in relations[0].evidence} == {"compose", "environment"} + + +def test_repeated_suggestion_merges_changed_evidence_without_a_duplicate(tmp_path: Path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + discovery = DiscoveryStore(store) + run = discovery.start_run() + now = datetime.now(UTC) + base = { + "id": "suggest-rel", "run_id": run.id, "suggestion_type": "relation", "source_ref": "docker:api", + "target_ref": "docker:db", "relation_type": RelationType.DEPENDS_ON, + "confidence": DiscoveryConfidence.HIGH, "created_at": now, "updated_at": now, + } + discovery.save_suggestion(TopologySuggestion(**base, evidence=["Compose relationship"]), "canonical-relation") + second = {**base, "id": "suggest-rel-second"} + saved = discovery.save_suggestion(TopologySuggestion(**second, evidence=["Environment relationship"]), "canonical-relation") + + assert saved.evidence == ["Compose relationship", "Environment relationship"] + assert len(discovery.list_suggestions()) == 1 diff --git a/backend/tests/test_v160_evidence_changes.py b/backend/tests/test_v160_evidence_changes.py new file mode 100644 index 0000000..2184f06 --- /dev/null +++ b/backend/tests/test_v160_evidence_changes.py @@ -0,0 +1,132 @@ +from datetime import UTC, datetime, timedelta + +from app.discovery_store import DiscoveryStore +from app.evidence import resolve_health +from app.models import ( + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthStatus, + ServiceCreate, + ServiceKind, +) +from app.store import Store + + +def _evidence( + evidence_id: str, + *, + provider: EvidenceProvider, + status: HealthStatus, + scope: EvidenceScope = EvidenceScope.COMPONENT, + service_id: str | None = "svc", + observed_at: datetime | None = None, +) -> HealthEvidence: + observed = observed_at or datetime.now(UTC) + return HealthEvidence( + id=evidence_id, + subject_type="service" if scope == EvidenceScope.COMPONENT else "application", + subject_id=service_id or "appops:demo", + service_id=service_id, + provider=provider, + provider_ref=provider.value, + status=status, + scope=scope, + confidence=0.8, + observed_at=observed, + valid_until=observed + timedelta(minutes=5), + received_at=observed, + summary="test evidence", + ) + + +def test_application_scope_evidence_never_becomes_component_direct_health(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local") + ) + store.record_health_evidence( + _evidence( + "appops-application", + provider=EvidenceProvider.APPOPS, + status=HealthStatus.DOWN, + scope=EvidenceScope.APPLICATION, + service_id=None, + ), + service_ids=[service.id], + ) + + resolution = store.health_resolution(service.id) + + assert resolution.selected_evidence_id is None + assert resolution.status == HealthStatus.UNKNOWN + assert "application-scoped" in resolution.explanation + + +def test_evidence_conflict_and_priority_are_visible(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + ServiceCreate(name="API", kind=ServiceKind.HTTP, url="https://api.local") + ) + store.record_health_evidence( + _evidence( + "docker-down", + provider=EvidenceProvider.DOCKER, + status=HealthStatus.DOWN, + service_id=service.id, + ) + ) + store.record_health_evidence( + _evidence( + "probe-healthy", + provider=EvidenceProvider.PROBE, + status=HealthStatus.HEALTHY, + service_id=service.id, + ) + ) + + resolution = store.health_resolution(service.id) + + assert resolution.status == HealthStatus.HEALTHY + assert resolution.selected_provider == EvidenceProvider.PROBE + assert resolution.conflict is True + + +def test_expired_evidence_is_explained_but_not_selected() -> None: + old = datetime.now(UTC) - timedelta(hours=2) + resolution = resolve_health( + [ + _evidence( + "expired", + provider=EvidenceProvider.DOCKER, + status=HealthStatus.DOWN, + observed_at=old, + ), + ] + ) + + assert resolution.selected_evidence_id is None + assert resolution.evidence[0].freshness.value == "expired" + + +def test_topology_snapshots_diff_canonical_service_and_dependency_changes(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + discovery = DiscoveryStore(store) + database = store.create_service( + ServiceCreate(name="Database", kind=ServiceKind.TCP, host="db.local", port=5432) + ) + first = discovery.capture_snapshot(run_id=None) + store.create_service( + ServiceCreate( + name="API", + kind=ServiceKind.HTTP, + url="https://api.local", + depends_on=[database.id], + ) + ) + second = discovery.capture_snapshot(run_id=None) + + diff = discovery.topology_diff(after_snapshot_id=second.id, before_snapshot_id=first.id) + + assert diff.summary["added"] == 2 # API service and its canonical dependency + assert {item.entity_type for item in diff.added} == {"service", "relationship"} diff --git a/backend/tests/test_v161_health_resolution.py b/backend/tests/test_v161_health_resolution.py new file mode 100644 index 0000000..b3712be --- /dev/null +++ b/backend/tests/test_v161_health_resolution.py @@ -0,0 +1,347 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +from app import observation as observation_module +from app import store as store_module +from app.docker_discovery import AppOpsInfo, AppOpsInventory, DockerHealthObservation +from app.evidence import freshness_for +from app.models import ( + EvidenceProvider, + EvidenceScope, + HealthEvidence, + HealthResolutionReason, + HealthStatus, + ServiceCreate, + ServiceKind, + ServiceSnapshot, +) +from app.observation import ObservationManager, observation_ttl +from app.semantic import build_semantic_topology +from app.store import Store + + +def _service(identifier: str, name: str, *, application: str | None = "Northstar", source_ref: str | None = None): + return ServiceCreate(name=name, kind=ServiceKind.HTTP, url=f"https://{identifier}.test", application=application, source_ref=source_ref) + + +def _application_evidence( + evidence_id: str, + *, + status: HealthStatus = HealthStatus.HEALTHY, + confidence: float = 0.9, + observed_at: datetime | None = None, + valid_for: timedelta = timedelta(minutes=3), + app_id: str = "northstar", +) -> HealthEvidence: + now = observed_at or datetime.now(UTC) + return HealthEvidence( + id=evidence_id, + subject_type="application", + subject_id=f"appops:{app_id}", + provider=EvidenceProvider.APPOPS, + provider_ref=app_id, + status=status, + scope=EvidenceScope.APPLICATION, + confidence=confidence, + observed_at=now, + valid_until=now + valid_for, + received_at=now, + summary="AppOps aggregate health", + ) + + +def _component_evidence(evidence_id: str, service_id: str, status: HealthStatus) -> HealthEvidence: + now = datetime.now(UTC) + return HealthEvidence( + id=evidence_id, + subject_type="service", + subject_id=service_id, + service_id=service_id, + provider=EvidenceProvider.DOCKER, + provider_ref=service_id, + status=status, + scope=EvidenceScope.COMPONENT, + confidence=0.95, + observed_at=now, + valid_until=now + timedelta(minutes=3), + received_at=now, + summary="Docker healthcheck", + ) + + +def _snapshot(service, *, direct: HealthStatus, effective: HealthStatus | None = None, evidence: list[HealthEvidence] | None = None) -> ServiceSnapshot: + return ServiceSnapshot( + service=service, + direct_status=direct, + dependency_status=effective or direct, + effective_status=effective or direct, + health_evidence=evidence or [], + ) + + +def test_appops_health_resolves_application_without_promoting_component_scope(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + api = store.create_service(_service("api", "Northstar API")) + worker = store.create_service(_service("worker", "Northstar Worker")) + aggregate = _application_evidence("appops-northstar") + store.record_health_evidence(aggregate, service_ids=[api.id, worker.id]) + + snapshots = [ + _snapshot(api, direct=HealthStatus.UNKNOWN, evidence=store.health_evidence_for_service(api.id)), + _snapshot(worker, direct=HealthStatus.UNKNOWN, evidence=store.health_evidence_for_service(worker.id)), + ] + topology = build_semantic_topology(snapshots, []) + application = topology.applications[0] + + assert application.status == HealthStatus.HEALTHY + assert application.health_resolution.reason_code == HealthResolutionReason.SELECTED_APPLICATION_EVIDENCE + assert application.health_resolution.confidence == 0.9 + assert all(component.direct_status == HealthStatus.UNKNOWN for component in topology.components) + assert all(component.unknown_reason is not None for component in topology.components) + + +def test_component_failure_overrides_healthy_aggregate_and_exposes_conflict(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + database = store.create_service(_service("db", "Northstar PostgreSQL")) + api = store.create_service(_service("api", "Northstar API")) + aggregate = _application_evidence("appops-northstar") + store.record_health_evidence(aggregate, service_ids=[database.id, api.id]) + store.record_health_evidence(_component_evidence("db-down", database.id, HealthStatus.DOWN)) + + topology = build_semantic_topology( + [ + _snapshot(database, direct=HealthStatus.DOWN, evidence=store.health_evidence_for_service(database.id)), + _snapshot(api, direct=HealthStatus.UNKNOWN, effective=HealthStatus.DEGRADED, evidence=store.health_evidence_for_service(api.id)), + ], + [], + ) + application = topology.applications[0] + + assert application.status in {HealthStatus.DOWN, HealthStatus.DEGRADED} + assert application.health_resolution.reason_code == HealthResolutionReason.COMPONENT_FAILURE + assert application.health_resolution.conflict is True + + +def test_application_and_domain_coverage_treat_partial_unknown_as_coverage_not_failure() -> None: + now = datetime.now(UTC) + from app.models import Service + + healthy = Service(id="healthy", **_service("healthy", "Known", application="App A").model_dump(), created_at=now, updated_at=now) + unknown = Service(id="unknown", **_service("unknown", "Unresolved", application="App A").model_dump(), created_at=now, updated_at=now) + topology = build_semantic_topology( + [_snapshot(healthy, direct=HealthStatus.HEALTHY), _snapshot(unknown, direct=HealthStatus.UNKNOWN)], + [], + ) + application = topology.applications[0] + domain = topology.domains[0] + + assert application.status == HealthStatus.HEALTHY + assert application.resolved_components == 1 + assert application.unresolved_components == 1 + assert application.component_coverage_percent == 50 + assert domain.status == HealthStatus.HEALTHY + assert domain.application_coverage_percent == 100 + + +def test_missing_stale_and_expired_application_evidence_have_structured_reasons() -> None: + now = datetime.now(UTC) + from app.models import Service + + service = Service(id="app", **_service("app", "No signal").model_dump(), created_at=now, updated_at=now) + no_evidence = build_semantic_topology([_snapshot(service, direct=HealthStatus.UNKNOWN)], []).applications[0] + assert no_evidence.health_resolution.reason_code == HealthResolutionReason.NO_APPLICATION_EVIDENCE + + stale_evidence = _application_evidence( + "stale", observed_at=now - timedelta(minutes=2, seconds=30), valid_for=timedelta(minutes=2) + ) + stale = build_semantic_topology([_snapshot(service, direct=HealthStatus.UNKNOWN, evidence=[stale_evidence])], []).applications[0] + assert stale.health_resolution.reason_code == HealthResolutionReason.STALE_EVIDENCE + + expired_evidence = _application_evidence( + "expired", observed_at=now - timedelta(minutes=8), valid_for=timedelta(minutes=2) + ) + expired = build_semantic_topology([_snapshot(service, direct=HealthStatus.UNKNOWN, evidence=[expired_evidence])], []).applications[0] + assert expired.health_resolution.reason_code == HealthResolutionReason.EXPIRED_EVIDENCE + + +def test_same_named_postgres_sources_do_not_share_application_or_appops_claim(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + first = store.create_service(_service("one", "PostgreSQL one", application="PostgreSQL", source_ref="docker:name:postgres-one")) + second = store.create_service(_service("two", "PostgreSQL two", application="PostgreSQL", source_ref="docker:name:postgres-two")) + store.record_health_evidence(_application_evidence("first", status=HealthStatus.HEALTHY, app_id="postgres-one"), service_ids=[first.id]) + store.record_health_evidence(_application_evidence("second", status=HealthStatus.DOWN, app_id="postgres-two"), service_ids=[second.id]) + + topology = build_semantic_topology( + [ + _snapshot(first, direct=HealthStatus.UNKNOWN, evidence=store.health_evidence_for_service(first.id)), + _snapshot(second, direct=HealthStatus.UNKNOWN, evidence=store.health_evidence_for_service(second.id)), + ], + [], + ) + assert len(topology.applications) == 2 + assert sorted(item.status for item in topology.applications) == [HealthStatus.DOWN, HealthStatus.HEALTHY] + + +def test_observation_refresh_renews_evidence_without_duplicates_and_uses_bounded_ttl(tmp_path, monkeypatch) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service(_service("api", "Northstar API", source_ref="docker:name:northstar-api")) + first_now = datetime(2026, 1, 1, tzinfo=UTC) + current = {"value": first_now} + monkeypatch.setattr(observation_module, "utc_now", lambda: current["value"]) + monkeypatch.setattr( + observation_module, + "appops_inventory", + lambda _: AppOpsInventory( + {"northstar-api": AppOpsInfo("northstar", None, None, "healthy", "Northstar", 0.9)}, + True, + "1 mapping loaded", + ), + ) + monkeypatch.setattr( + observation_module, + "observe_container_health", + lambda _: [DockerHealthObservation("abc", "northstar-api", "docker:name:northstar-api", "running", "healthy", True)], + ) + events: list[dict[str, object]] = [] + monkeypatch.setattr(store_module.event_bus, "publish", lambda event: events.append(event)) + manager = ObservationManager(store, interval_seconds=60) + first = manager.refresh() + events.clear() + current["value"] = first_now + timedelta(minutes=20) + second = manager.refresh() + + evidence = store.health_evidence_for_service(service.id) + active = [item for item in evidence if item.active] + assert first.docker_observations == second.docker_observations == 1 + assert observation_ttl(60) == timedelta(seconds=180) + assert len(active) == 2 + assert all(item.valid_until - item.observed_at == timedelta(seconds=180) for item in active) + assert all(freshness_for(item, now=current["value"]).value == "fresh" for item in active) + assert store.provider_statuses()[0].valid_until - store.provider_statuses()[0].observed_at == timedelta(seconds=180) + assert events == [] + + +def test_appops_warning_is_degraded_application_evidence_only(tmp_path, monkeypatch) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + _service("postgres", "PostgreSQL", source_ref="docker:name:postgresql15") + ) + monkeypatch.setattr( + observation_module, + "appops_inventory", + lambda _: AppOpsInventory( + {"postgresql15": AppOpsInfo("postgres", "database", "critical", "warning", "PostgreSQL", 1.0)}, + True, + "1 mapping loaded", + ), + ) + monkeypatch.setattr( + observation_module, + "observe_container_health", + lambda _: [ + DockerHealthObservation( + "abc", "postgresql15", "docker:name:postgresql15", "running", None, False + ) + ], + ) + + result = ObservationManager(store, interval_seconds=60).refresh() + evidence = store.health_evidence_for_service(service.id) + topology = build_semantic_topology( + [_snapshot(service, direct=HealthStatus.UNKNOWN, evidence=evidence)], [] + ) + + assert result.appops_observations == 1 + assert len(evidence) == 1 + assert evidence[0].scope == EvidenceScope.APPLICATION + assert evidence[0].status == HealthStatus.DEGRADED + assert topology.applications[0].status == HealthStatus.DEGRADED + assert topology.components[0].direct_status == HealthStatus.UNKNOWN + + +def test_loaded_evidence_reports_expired_freshness(tmp_path) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service(_service("api", "Northstar API")) + old = datetime.now(UTC) - timedelta(minutes=10) + store.record_health_evidence( + _application_evidence("old-claim", observed_at=old, valid_for=timedelta(minutes=2)), + service_ids=[service.id], + ) + + assert store.health_evidence_for_service(service.id)[0].freshness.value == "expired" + assert store.health_evidence_for_services([service.id])[service.id][0].freshness.value == "expired" + + +def test_empty_broker_inventory_is_unavailable_with_existing_docker_services( + tmp_path, monkeypatch +) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service( + _service("api", "Northstar API", source_ref="docker:name:northstar-api") + ) + store.record_health_evidence( + _component_evidence("existing-docker-claim", service.id, HealthStatus.HEALTHY), + service_ids=[service.id], + ) + monkeypatch.setenv("SERVICEMAP_DOCKER_PROVIDER", "broker") + monkeypatch.setattr( + observation_module, + "appops_inventory", + lambda _: AppOpsInventory({}, False, "AppOps unavailable"), + ) + monkeypatch.setattr(observation_module, "observe_container_health", lambda _: []) + + result = ObservationManager(store, interval_seconds=60).refresh() + + assert result.docker_observations == 0 + assert next( + item for item in store.provider_statuses() if item.provider == EvidenceProvider.DOCKER + ).status.value == "unavailable" + assert [item.id for item in store.health_evidence_for_service(service.id)] == [ + "existing-docker-claim" + ] + + +def test_single_provider_outage_ages_only_its_claim_and_recovers_without_duplicates(tmp_path, monkeypatch) -> None: + store = Store(str(tmp_path / "servicemap.db")) + service = store.create_service(_service("api", "Northstar API", source_ref="docker:name:northstar-api")) + now = datetime(2026, 1, 1, tzinfo=UTC) + current = {"value": now} + mode = {"available": True} + monkeypatch.setattr(observation_module, "utc_now", lambda: current["value"]) + monkeypatch.setattr( + observation_module, + "appops_inventory", + lambda _: ( + AppOpsInventory( + {"northstar-api": AppOpsInfo("northstar", None, None, "healthy", "Northstar", 0.9)}, + True, + "1 mapping loaded", + ) + if mode["available"] + else AppOpsInventory({}, False, "AppOps inventory unavailable: ConnectError") + ), + ) + monkeypatch.setattr( + observation_module, + "observe_container_health", + lambda _: [DockerHealthObservation("abc", "northstar-api", "docker:name:northstar-api", "running", "healthy", True)], + ) + manager = ObservationManager(store, interval_seconds=60) + manager.refresh() + mode["available"] = False + current["value"] += timedelta(seconds=60) + manager.refresh() + + evidence_during_outage = store.health_evidence_for_service(service.id) + assert any(item.provider == EvidenceProvider.APPOPS and freshness_for(item, now=current["value"]).value == "fresh" for item in evidence_during_outage) + assert any(item.provider == EvidenceProvider.DOCKER and freshness_for(item, now=current["value"]).value == "fresh" for item in evidence_during_outage) + assert next(item for item in store.provider_statuses() if item.provider == EvidenceProvider.APPOPS).status.value == "unavailable" + + mode["available"] = True + current["value"] += timedelta(seconds=60) + manager.refresh() + active = [item for item in store.health_evidence_for_service(service.id) if item.active] + assert len(active) == 2 + assert next(item for item in store.provider_statuses() if item.provider == EvidenceProvider.APPOPS).status.value == "healthy" diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..6836d56 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,66 @@ +name: servicemap + +services: + backend: + build: + context: . + dockerfile: backend/Dockerfile + container_name: servicemap-backend + restart: unless-stopped + environment: + SERVICEMAP_DB: /data/servicemap.db + SERVICEMAP_PROBE_INTERVAL: ${SERVICEMAP_PROBE_INTERVAL:-60} + SERVICEMAP_PROBE_TIMEOUT: ${SERVICEMAP_PROBE_TIMEOUT:-5} + SERVICEMAP_PROBE_RETRIES: ${SERVICEMAP_PROBE_RETRIES:-1} + SERVICEMAP_PROBE_CONCURRENCY: ${SERVICEMAP_PROBE_CONCURRENCY:-10} + SERVICEMAP_RETENTION_DAYS: ${SERVICEMAP_RETENTION_DAYS:-30} + SERVICEMAP_EVENT_RETENTION_DAYS: ${SERVICEMAP_EVENT_RETENTION_DAYS:-90} + SERVICEMAP_TLS_WARNING_DAYS: ${SERVICEMAP_TLS_WARNING_DAYS:-30} + SERVICEMAP_FLAP_THRESHOLD: ${SERVICEMAP_FLAP_THRESHOLD:-6} + SERVICEMAP_OBSERVATION_INTERVAL_SECONDS: ${SERVICEMAP_OBSERVATION_INTERVAL_SECONDS:-60} + SERVICEMAP_PROBE_HOST: ${SERVICEMAP_PROBE_HOST:-} + volumes: + - ./data:/data + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).read()"] + interval: 20s + timeout: 5s + start_period: 10s + retries: 5 + labels: + net.unraid.docker.managed: "dockerman" + security_opt: + - no-new-privileges:true + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + + frontend: + build: + context: . + dockerfile: frontend/Dockerfile + container_name: servicemap + restart: unless-stopped + depends_on: + backend: + condition: service_healthy + ports: + - "${SERVICEMAP_PORT:-1253}:80" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/health >/dev/null || exit 1"] + interval: 20s + timeout: 5s + start_period: 10s + retries: 5 + labels: + net.unraid.docker.managed: "dockerman" + net.unraid.docker.webui: "http://${SERVICEMAP_WEBUI_HOST:-127.0.0.1}:${SERVICEMAP_PORT:-1253}" + security_opt: + - no-new-privileges:true + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..60a1b6f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +name: servicemap + +services: + servicemap: + build: + context: . + dockerfile: Dockerfile.unraid + container_name: servicemap + restart: unless-stopped + environment: + SERVICEMAP_DB: /data/servicemap.db + SERVICEMAP_STATIC_DIR: /app/static + SERVICEMAP_PROBE_INTERVAL: ${SERVICEMAP_PROBE_INTERVAL:-60} + SERVICEMAP_PROBE_TIMEOUT: ${SERVICEMAP_PROBE_TIMEOUT:-5} + SERVICEMAP_PROBE_RETRIES: ${SERVICEMAP_PROBE_RETRIES:-1} + SERVICEMAP_PROBE_CONCURRENCY: ${SERVICEMAP_PROBE_CONCURRENCY:-10} + SERVICEMAP_RETENTION_DAYS: ${SERVICEMAP_RETENTION_DAYS:-30} + SERVICEMAP_EVENT_RETENTION_DAYS: ${SERVICEMAP_EVENT_RETENTION_DAYS:-90} + SERVICEMAP_TLS_WARNING_DAYS: ${SERVICEMAP_TLS_WARNING_DAYS:-30} + SERVICEMAP_FLAP_THRESHOLD: ${SERVICEMAP_FLAP_THRESHOLD:-6} + SERVICEMAP_DISCOVERY_INTERVAL_SECONDS: ${SERVICEMAP_DISCOVERY_INTERVAL_SECONDS:-0} + SERVICEMAP_OBSERVATION_INTERVAL_SECONDS: ${SERVICEMAP_OBSERVATION_INTERVAL_SECONDS:-60} + SERVICEMAP_DOCKER_HOST: ${SERVICEMAP_DOCKER_HOST:-unix:///var/run/docker.sock} + # "broker" is opt-in and has no socket fallback. It requires a dedicated + # registered ServiceMap identity and token file before activation. + SERVICEMAP_DOCKER_PROVIDER: ${SERVICEMAP_DOCKER_PROVIDER:-socket} + SERVICEMAP_DOCKER_BROKER_URL: ${SERVICEMAP_DOCKER_BROKER_URL:-} + SERVICEMAP_DOCKER_BROKER_TOKEN_FILE: ${SERVICEMAP_DOCKER_BROKER_TOKEN_FILE:-} + SERVICEMAP_APPOPS_URL: ${SERVICEMAP_APPOPS_URL:-} + SERVICEMAP_PROBE_HOST: ${SERVICEMAP_PROBE_HOST:-} + # Disabled is backwards-compatible. OIDC mode is fail-closed and needs + # issuer, audience and explicit role-group mappings before activation. + SERVICEMAP_AUTH_MODE: ${SERVICEMAP_AUTH_MODE:-disabled} + SERVICEMAP_OIDC_ISSUER: ${SERVICEMAP_OIDC_ISSUER:-} + SERVICEMAP_OIDC_AUDIENCE: ${SERVICEMAP_OIDC_AUDIENCE:-} + SERVICEMAP_OIDC_ROLE_CLAIMS: ${SERVICEMAP_OIDC_ROLE_CLAIMS:-groups,roles} + SERVICEMAP_OIDC_VIEWER_GROUPS: ${SERVICEMAP_OIDC_VIEWER_GROUPS:-} + SERVICEMAP_OIDC_OPERATOR_GROUPS: ${SERVICEMAP_OIDC_OPERATOR_GROUPS:-} + SERVICEMAP_OIDC_ADMIN_GROUPS: ${SERVICEMAP_OIDC_ADMIN_GROUPS:-} + volumes: + - servicemap_data:/data + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - "${SERVICEMAP_PORT:-1253}:8000" + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).read()"] + interval: 20s + timeout: 5s + start_period: 10s + retries: 5 + labels: + net.unraid.docker.managed: "dockerman" + net.unraid.docker.webui: "http://${SERVICEMAP_WEBUI_HOST:-127.0.0.1}:${SERVICEMAP_PORT:-1253}" + net.unraid.docker.icon: "http://${SERVICEMAP_WEBUI_HOST:-127.0.0.1}:${SERVICEMAP_PORT:-1253}/icons/servicemap-dockerman-256.png" + security_opt: + - no-new-privileges:true + cap_drop: + - ALL + read_only: true + tmpfs: + - /tmp:rw,noexec,nosuid,size=32m + logging: + driver: json-file + options: + max-size: "20m" + max-file: "3" + +volumes: + servicemap_data: + name: servicemap_data diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..81a47c6 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,119 @@ +# ServiceMap architecture + +## Runtime shape + +ServiceMap remains deliberately small: one FastAPI process, one React single-page application and one SQLite database. Compose serves React through Nginx; the Unraid image bundles the same frontend and serves it from FastAPI. + +```text +Browser + │ REST + SSE + ▼ +React workspaces + │ + ▼ +FastAPI + ├── service inventory and dependency validation + ├── probe engine (HTTP, TCP, DNS, TLS) + ├── state machine and flapping observation + ├── health-evidence resolution and provider health + ├── incident correlation and event journal + ├── SSE fan-out bus + ├── atomic config import/export + ├── Docker/AppOps discovery and reconciliation + ├── evidence-backed canonical topology overrides + └── optional per-service scheduler + │ + ▼ + SQLite on /data +``` + +## Data ownership and migration + +`Store` owns SQLite access and opens short-lived connections with WAL mode, a 5-second busy timeout and foreign keys enabled. Startup creates missing discovery tables and adds versioned metadata in place. v1.6 adds health evidence, evidence-to-service links and provider-status rows; the one-time migration projects only legitimate existing latest probe/runtime facts with their original timestamps. Existing rows receive safe defaults; the migration never deletes or rewrites topology data. + +v1.6.1 adds no schema migration. Its health-only observation loop is distinct from discovery: every configured cadence it reads Docker runtime/healthcheck facts and AppOps application inventory, renews only source-scoped evidence/provider status, and skips demo state. It does not create services, suggestions, relationship evidence, snapshots, probe endpoints or operator overrides. The shared observation TTL is at least 150 seconds and three normal cycles, so a delayed provider cycle ages evidence rather than immediately turning the topology unknown. Provider failures are isolated; they update that provider's availability only and leave other current evidence intact. + +v1.6.2 keeps the same schema and evidence scopes. An AppOps `warning` observation is degraded application evidence; it cannot resolve a component. Evidence freshness is recomputed when persisted claims are read, so an expired claim cannot appear fresh in the raw inspector/API payload. The current MCP Hub Docker read broker cannot replace the production socket provider yet: its registration covers two unrelated projects, its ServiceMap-scoped inventory is empty, and its short-lived HMAC service-token contract rejects a raw bearer secret. Cutover requires a dedicated ServiceMap read identity, full bounded inventory parity and an explicit network path; broker mode remains fail-closed with no socket fallback. + +The HTTP discovery routes accept only the Docker host and AppOps URL selected in deployment configuration. A request body cannot turn the container into a general-purpose client for another Docker daemon or internal HTTP endpoint. The private-LAN deployment currently keeps OIDC disabled because it has no dedicated browser client or tested owner login path; mutation APIs therefore depend on the LAN perimeter. Broker mode must be deployed without the legacy Docker socket bind even though the current socket-mode Compose file retains it. + +The v1.4 semantic projection is deliberately not a parallel datastore. `semantic.py` derives Domain → Application → Component from canonical services, relations, runtime/monitoring state and incident analysis. Logical relations are grouped at query time so one graph edge retains all independent evidence records and their first/last-seen provenance. v1.6.1 resolves application health separately: fresh aggregate AppOps evidence can answer an application but never a component; direct component failures override weak aggregate health and conflicting claims remain visible. Semantic application identity uses locked operator grouping, Compose provenance and stable source references before a display name, preventing generic provider labels from collapsing unrelated services. Bulk evidence lookup serves the complete semantic projection in one bounded SQL query rather than an application-by-application scan. + +The service record stores descriptive metadata, group/environment/tags, criticality, source reference, application, runtime state/health, monitoring state, probe source/reachability/confidence, enablement, maintenance state, probe policy and state-machine counters. Probe rows remain immutable and retain `trigger` and bounded JSON `metadata` fields. `HealthEvidence` retains provider, subject, scope, confidence, observation/receipt/expiry timestamps, metadata and supersession. Application-scoped claims may be linked to component inspectors but cannot resolve a component's direct health. + +Indexes cover service/time probe history, global probe time, event time/service/incident and incident status/root lookups. Availability is calculated with SQL aggregates over a bounded time window rather than loading all rows into Python. Evidence timestamp renewals retain a stable row and do not publish SSE; material provider status, evidence status, confidence, summary or metadata changes do. + +## Health state and dependency semantics + +The direct state machine is backend-owned, then exposed through an evidence policy: + +```text +unknown ──success──> healthy +healthy ──failure threshold──> degraded/down +down/degraded ──success #1──> recovering +recovering ──recovery threshold──> healthy +any monitored state ──maintenance──> maintenance +``` + +The first failure can be held at the prior/unknown state when a failure threshold is greater than one. Repeated state changes in a bounded 30-minute query set the `flapping` observation and its count; flapping is not a competing health state. + +Effective health is calculated recursively from direct state and dependencies. A down dependency makes a healthy dependent degraded; the stored probe observation is never rewritten. Cycles and missing dependencies are rejected before writes. + +Maintenance and disabled monitoring do not produce a new incident. A running Docker component without a verified endpoint or Docker healthcheck is `unmonitored`, not `down`; this state cannot fabricate a root cause. Docker `unhealthy` and stopped/dead runtime observations are valid direct-down evidence without a synthetic network probe. The resolver selects fresh component-scoped evidence deterministically, exposes conflicts rather than collapsing them and excludes stale/expired claims. AppOps inventory health is application-scoped provenance, not a component probe. + +## Probe engine + +`probes.py` uses bounded async operations and one asyncio lock per service. Scheduler, probe-all and manual probe calls therefore coalesce at the service boundary rather than running duplicate network checks concurrently. + +- HTTP follows redirects, supports expected status ranges and records status/latency. +- TCP opens and closes a bounded connection. +- DNS resolves a hostname, returns at most 20 unique addresses and reports no-answer/timeout failures. +- TLS performs a direct hostname-verified handshake, reports expiry metadata and applies a configurable warning threshold (`SERVICEMAP_TLS_WARNING_DAYS`, default 30). + +Targets are explicitly typed. HTTP accepts only `http`/`https` URLs; TCP/TLS require host and port; DNS requires a host and does not accept arbitrary URI schemes. There is no `file://` or generic URL fetcher. + +Per-service interval, timeout and retry settings override the global defaults. Scheduled observations are marked `scheduled`; UI-triggered observations are marked `manual`. + +## Incidents and events + +`incidents.py` deterministically finds a down service with no effectively-down dependency, then traverses the reverse graph for blast radius. `sync_incidents()` reconciles candidates with the durable journal: + +- one active incident per root service; +- repeated probe cycles update impact instead of opening duplicates; +- acknowledgement changes `open` to `acknowledged` and persists a timestamp; +- recovery changes the record to `resolved` and retains `resolved_at`; +- state transitions, flapping, incident lifecycle and maintenance changes are journaled. + +Incident timeline entries are events tied to an incident. Raw probe rows are retained independently under the configured probe retention; non-incident events use `SERVICEMAP_EVENT_RETENTION_DAYS` (default 90). Incident lifecycle events are retained with the incident record. + +## SSE and frontend refresh + +The durable SQLite event journal is the source of truth. A small thread-safe bounded queue is allocated per SSE client and receives notifications for probe completion, state changes, topology changes and incident lifecycle changes. Slow clients lose only transient wakeups, not durable history. The stream emits keepalives and unregisters queues on disconnect/shutdown. + +React subscribes with SSE, debounces event refreshes and reconnects with backoff. It enables bounded fallback refresh only while the stream is unavailable; a healthy quiet stream does not trigger periodic full reloads. Authenticated clients use the same SSE protocol through `fetch` with a bearer header. The frontend never invents monitoring state optimistically; every node, counter and inspector value comes from the API. + +## Production discovery and reconciliation + +`DiscoveryStore` owns durable runs, source records, observations, relation evidence, suggestions, overrides, topology-change events and compact canonical topology snapshots. `DiscoveryManager` reads the configured Docker endpoint and optional read-only AppOps inventory, then separates observed candidates from inferred relationships and canonical services. Candidates include stable Compose/name identity, image, state, health, labels, networks, ports, Compose metadata, healthcheck presence and sanitized endpoint-host evidence. ServiceMap labels (`servicemap.*`), Compose `depends_on`, safe environment hostnames and AppOps mappings produce explainable suggestions. Numeric AppOps `discovery_confidence` gates classification strength, while its app health remains scoped provenance. Shared networks are low-confidence review hints only. + +Discovery runs do not mutate canonical topology. Reconciliation updates existing observed records and only imports new services or accepts high-confidence dependency relationships when the user explicitly requests it. Compose references resolve only inside the same project; environment references use project/network/global-unique scope and ambiguous matches remain review suggestions. Manual overrides and locked edges are preserved across later runs. Missing Docker identities move through `missing`, `stale` and `retired` without deleting probe history. Health propagation uses canonical dependency edges only; evidence-only or rejected relationships cannot affect health. + +Published ports become active candidates only when `SERVICEMAP_PROBE_HOST` is explicitly configured. Internal Docker names are retained as evidence with `internal_only` reachability, never sent to the network probe engine by default. The reachability-repair endpoint provides a dry-run report before it disables unsafe legacy candidates or selects a verified published endpoint; it does not delete history and resolves false incidents with an endpoint-superseded event reason. + +After reconciliation, a revision-hashed snapshot is captured only when canonical services or dependency edges change. The Changes workspace compares two snapshots and makes a service/relationship change actionable without auto-applying anything. The production UI starts with the canonical topology, exposes discovery diffs and evidence, supports individual accept/reject decisions, and offers an explicit edit mode for pinned node layout and manual dependency edges. Hidden/ignored lifecycle records remain available through the `include_hidden` API for reconciliation and audit context. ELK route sections are passed to `LivingEdge`, so its directed SVG path follows the actual orthogonal layout. + +## Configuration + +Config v1 is still accepted. Config v2 serializes the service metadata and policy fields by human-readable service name. Parsing validates endpoint fields, duplicate names, duplicate/self edges and document-contained cycles. Import then validates the projected merge or replace graph and commits it in one `BEGIN IMMEDIATE` transaction. + +## Demo safety + +The Northstar demo is built from deterministic UUIDs and synthetic probe records. Entering demo stores JSON snapshots of services, probes, events, incidents, discovery evidence, health evidence and provider state in the durable `demo_sessions` row. Reset replaces only the synthetic workspace while preserving the original snapshot. Exit restores the snapshot and deletes the session. Scheduled and manual live probes and discovery reconciliation are blocked while the demo is active. + +## Deployment trust boundary + +The Unraid target uses `/data` on the named `servicemap_data` volume, port mapping `1253:8000`, a healthcheck on `/health`, `no-new-privileges`, dropped capabilities, a read-only root filesystem and bounded `/tmp` tmpfs. Managed validation gates exact-SHA autoredeploy. Socket discovery remains a high-trust Docker API boundary even with a read-only bind. The opt-in broker provider requires a dedicated short-lived ServiceMap token, registration-scoped read capability and shared network, and never falls back to the socket. AppOps access is read-only enrichment. + +## Optional authorization + +`SERVICEMAP_AUTH_MODE=disabled` preserves the deployed v1.5 behavior. In `oidc` mode FastAPI validates issuer/audience/signed bearer JWTs against OIDC discovery and JWKS; it maps explicit roles or configured Authentik groups to viewer, operator and admin. `/health` remains anonymous for deployment recovery, while APIs fail closed when OIDC configuration is incomplete. No client credentials, issuer secrets or tokens are stored in ServiceMap's SQLite database or frontend configuration. diff --git a/docs/screenshots/demo-health.png b/docs/screenshots/demo-health.png new file mode 100644 index 0000000000000000000000000000000000000000..814efafe42cb11c6a1d11c53f6309f1e4198d8b2 GIT binary patch literal 312563 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 aMuw4(FYa7HwS)j)7<4rB)eBWE|Nam5QmZ)t literal 0 HcmV?d00001 diff --git a/docs/screenshots/demo-landscape.png b/docs/screenshots/demo-landscape.png new file mode 100644 index 0000000000000000000000000000000000000000..c774424ecf5e10b20c66f66cb2110b450985503f GIT binary patch literal 406522 zcma&tWmFq&+dg_+iWDmzD8=1_1Sk|MTC7kk1%eYOMS_(UEADQgxLXKLpg?d4E&+-Z zF9a(ETH4={(mk!NVgi z#=|3OCL_4N5&;Qr#>1n*)6q~f3oOL_rTF8u;wP5;)R$uQ8BC0fE>q*nn=Yea1!cO- zz1Ah4wb|{V3SO%crp#&w4;}Gxvct~3Od2cp(=;+6dD81T9tK|8IFE?bbv`++tAG2> z^C5{Yi3*2SCx6f29Ebmor_adT25-q8N{>p9NRJl!o(7z^UY&Kkc+R3wk$E+uTTb-) zb@o`;V%={v(ng%~JnXJ^PQf3)6}9*N*TQ$xU#flFFX9@axsuj2^twI=GMZ7}=3G~X zmi{_^PYRtsxHw|(i~q}p#hTZF(6c#P?37aR{CDUugY}L}!}W#}wxXV|>fyi)rr~G> zvI_Nv8T+WOxztMM|V*cWU&0id@3QlgqUgE4u-h8ps%<^T{UCjZf&t{XfzvmW&W7D-Kx&ohG<6PV%`|Mj4b%%yDRH1 zX9m$83VXo6b6a=Wu6MP^yHmRlBd@YBB{QhisX4If--9_WdO|mXCcb4Iz3LM?8c%E) zp7s|NClC$`+K2+sl*12}C6%%O?y`?PTbuI^KI$$Up2SOGQBS`<%>pQiD^=MoZUw`( z1=s3V9HW#SqlsU&i9~jLF$%SZRb+4{cE(;5tsYNme#+R?nYqj(ynZAtw$NI6Fcr`G zy)a!6zpJRNKzC`EZ0XKHy$J25;s@jf`xI3z*@BhY8O1|4s8P8NATaB(j1&H|=Vc_{ zZg{u>%20`U4f#PbkDNxoM&wJOhF26IvO0_2Elb)%rSxIXYVf+_kohiq1HEO@LTk`1 z=#`gaAKgi|OuW~V(ccT#LmE>}FZ`jmF0kg^#wUcwA2NY~nTqs7y^*YWY^D4kL|yAx z0*=B_M52e*d#6A-E`hQ(I@7AiK;FDtpuWyX$L`!Pdm6JtTctNU$I4Uuz|i zn5Kj#81QuzlAxHP)P-nZb#@0@IY5%1a-k}v*>A4ViKJzmMjAgpx_g2NrkBaOFZ}rh zOf?e8xcC8ALF9lA@1AHh_;9v4B^D744a^{O6R#gI6ds%4jQ&Npuo7If`^F-Klzzm^ z?Ych98(Rp_9~moT$%fYtOtKa&EV;LVKFnm>A*4q!pe5T`4n8F{$Z!T;3b%S$Eb8T?xy^jD>JNeh>?iewb$MP&XSMqa0)tjftVQ4aLqwHr{;mD%(0XRu^_2HEiziVyDfqh?yyyFE z4~V&7qH$?HpSh+CC-d;OjXH6O_Cz?0&Hz$ewv<;w! zSi84H>ZE4wz7C{ESY)Ct?(rIJ^J;RUUL>$f5T+vO0CG| zS(1{GvXCVUrTKKPp6z!II3$x>KuBz^MO7*}z0`!_F;@tMDprlAkc&;2ERMEjw7-<> zTFV&9AW*W{3*Im=0!y)r)0h#8tJ@q^+vW~jnl$GDj)uthzX%p^&+PD`Nk3U72J%6R zm;k?v$z^ULnqR3-4{g_vqnf1{AWp=45$Zu|@hI<_5`9LzPaLY}#k3)g_@l9lZdMFh zLj*JwL@+ZdhtyS@er&Z243!eeJYM?H0lz}{QI<2*xXyyNuq?^FEx-v5jxdjGQ~bWu zJcrFV97b|u->DC!O`Ffz_nNOOc#csKh{sxqlFdD-EGkQQ19~5RM7YEDks>8yf0npZ zEB=KKMy1UnK^ZeR@V(7ANv$8n8Z{eG%rS_-LD(Jf)v~rljbw&GnhITFcQX#rq1%T7 zy!;XbrjeP6ZwIxkYFX>nJR~12;zfxv`|Z2MF=%~03)G2fP=M7?fZ~%zH%mFC&Ru5o zoP@VG-VMa3h%-KWUg7l7{OB`7(BYbQq*@Xw)!uQrOj9INU3?nY~ z6R!cZP!~npK0y%YABW|F9&cq5zwOfnACHRp?^oC%p;1z;djxArRSxCDhQkzcGJ^?| z#-2qEV7dzO=!S8M&PG)6c*1eJ`}?0Xm^L1a;8enb`@zRSW;H|t;E8yu^P`=zqzF)(xvLLjB+BQQ3EV77L&eke3qp%{SF%l+z`R_sY^@!!BK+a zswU`zuaCHPuWgos4!JrQij80Q4V3(&=Aw2SUS{z(4#b+dVSJW4`@qh`++_;J^g!G^e|-wRde==Yk6U$_AB}BqxS85_gkTogRed zh-_ao@$nr|kuQ`L-4wo`@*9zo97+mIKIIz&@zIMl_hKdD3Z@PKPIvh+4a0gZ8+Ekc z+P*t`C8qU@yeZ+Ai84EWi_9;@P0_9@vS_>9G6j0cOny7`UDnX-W#)M+Gm<-6oD0yBw?tt=RI^kr@fTUE1p!!QVi z?w@2w-?F@QB0N8`i^`GVv_?}athh(VC0Bu5x%`Y*I@H(eTrOG|2U~8M7YP1!T+*$| zxu!Af=!|f7;bHbYy}aQAA<3Lzd?XB}jglD#Wwor8#cD>iEdTV1iQg=WfKm?=J*J7; zy}4zdQtGy4=gB2wG+ll9D#+kP+BW=2(OQzL$+cE?&;Hb7+&-++t~?(qHy+NFWGSZt zenu^`b??3l49viVMw^~6z)0(SQa?%^mUQi!oC%-Fn3Hb3@cS&2?-o*}$bGz|S7xSN zr}E`!acL3Nl&8`BOKCQ1?@ueFV=%|hq6tf19G*>$0*qy@nx!Adafs4VC_Y@=XkyNd zplbg-#>xe|z&^&h0;)s4)*+gjc?T+=Y?~X@TxM%f_u*fMkCiE^@Xw(#Mq#NnFgH7Y8R!f7OpvcafbS6QJMu zE6x!T31}u%go-VieiWqzif){dNDpFLV4CO6WIzpQ+*Fy^BxI(D=_kTe&=TUtIT0(Q zj!AGE3=>Vu;I&W$ABmDgQDz|qbPHZ9i#v-@t?UKPeJlMpIV>yMV3Sq=N7kd`XKxZK80hMTKUmZ=_kS543v9T9+hfENLn1-#=MwL5r zqa2ep{DQ(U1UfESfMrd@Sx0N$|m7KMWF8>g?>M(}AEr0xO`VD-aQ9QG^|5Hw9DCgq27uhobpft*! z(B*NqBr6a@6Cvf@M>^=t*xaZ~Ka~(5KAsfEWTl;JTRD0#j0x>UBq>5KXkYO&$dBQaLcBtR5yI@r#W; zed28|jS5o5QrD$VjLKkzpVfUS;ENM*yj}yoegO)qNt#-fNDKhDX79UTLONM6c-sg} zTULs%7a6AHG1mU4;_p{6^LO9!u)0eQ^)UN$PB^$5o$Fl1#y%1|-NYST=n~FSBu!Vm zNv*CtimIaWQijI01WjrxLf)*ZskqH6Jhc8azoSs0@*`6_LcDeH*@mQ_?q$@w@~opz z9vAaovhozeWS`8csMBdZuouhU3+=)UQ+X}@XWu=2K{ujQ6}sM3O50N#?T{aN+MjK* zdqjN0>z(!G9xKxJ%>GlsbdS`L zMrKkD_g9mLhvA+!i)!WBtfy9C2w9O-VN4w75I-NnX^km?r^9;c)OSt{9-c$e9*j#A zr6|rOFAz;i=}k)V3!ImxkRJh}|14lplSJSj?A4&rVelO~_eWu)%fyryWAi(x)K5jm zqlxH_u+in)sR#pEmt@l)eP*PR5N@r?AMvD?3ES0&@)fKPrQALtFpv3e{d*|#5KdD; z=vY=OHfwFXkdhpUwMV$A%z3;m8$&$m#Lth(TjPMK0SdBxFVJIwPw^8C{N)LT@ zN!Q)t8}B_WKs$2KU-j0mQ^ou^$jo-8B-tzU=YhoRD{b4DO>%!MJWcQB*nGJ8nYD_O z|Hi=Zc4-SFNA^A4A#X6>!6{$wY@nM+v_hAZrR7Z`1#k`7N^+z`GY%)6FCY+(@`lp_ zEz6?+u1LXR0r9Zst2dIYV!sF<3~nppb($GXC13|G>6%v!m5{r)1PL)r6b!|-u=H$( z!9g+tA(wF6DtQ$Vm5vdA0(|Zp5j%*6;%7S@e7tov?WMM4kco?I4i$FjHVV!@QnbCf zA%Gjh4pYZd{rTq^c|&|oY|`{BPL#YeLyLFea}WL`10hg;zzDn@q%)C_Xe(l56bB$G zxG-rBl9m=XQ3OxDVql!0_=2@CGNY2uWXU>;e1$ibt>(l3mFEjq@(J92j37BDlZwmd zopwKR7okAWnXov=%+&zvFsS1S&UJ4%YbN~8&XXUB%_1*ZaGx=3SJ32L_GrVT1cR%p zThd;jBnILq++^3)oQYzESb;x-c)T;`@_R~L6AX;Ds?OOyFZK!2N%eBu@xKY?V4ff< zy0EGFGS15&2u%G*m8C%{N9celF)?nx>Ng2CGr0v1F{}IuM8d_5rL1LQ~*3SZAVqg){%fpG7BTdOzwW{= z`hP8eBD3+w29L*KvOFjq`>Hxz9C~oS(AoqJ^#jS;IQ3 z=JCk_#sc!1u%Pyx7Wcr3Qd>4YAFJjlL01q5w&JaNAsrBI?EF)=s_6`Ug4h*FozJ`s z-kAC0@VllDuqf@s={NDJulL)4sb9H15|@bZSDm^p)LIYK1pw=1n>Wc6u{I1-moKi4 z^9&_de0SnmFaLh*VmvzfN*T4FmaWIfYEFPeBn29xHjHY&vH~VrIi?4)t#+*DRI2`} zw7bQex?acjPtpCT6>;nQtbC(?h#~G{P1)ZJ0Ocs_!(1h$mj6yQ3M`!}t+lFRBRvZ0 z*XP%;`4V-FA_bomnC~WJfShquU-Lqhy)H|=rR^KZIXnRZ$-D)1W4 zZ!Ir%rL9@Epgj6&DmLSwvTf~lbN1L+t2vv)Yby($5A#>{e*^&)~F$f<^ zu)8uz7buM93#5<$)l&Z8Acbe9zh~trQ8>X8v)PPChr^EulHo)JzhANtv5~RizVtsE zMsRbbxmJjFi{b*I&vK3)S>wI)q4pI!_=pYX`oU#8LQ$taYKY4fdUB>16cx_IV3rri zmEUW=fJGiw*iozoU7Czi2*bW#*GN^FzqncRO^t8|6*m&@N4m0P$Us)P zcs{6Nv!?$5QzfgXNJbjiAebllQlIupMXVk-n||i*XY{tp6nyfyLc`#ZK{*i2X(p-+ z4BNEC^AKMhKya_|w(j?*N#|;`*xk=6xW_0wdt6>C?PF+pT`fZCygZ)ivjZZi{a{#E zqJ2!ynRua#F9givQ9DikM10C`oXxZuuPi<>%-I@~{CNN3+Wh@6Khd9TjXB3l1?L!N zVH#i?9%Rxc=7aX*LgzdcGEn}ba(UP|*OK6rQ_xJKK%@)5aUajb_IG6(AodFn;9!DU ztHhK9%^36+R}GXBw_BRHE+CkqaEET2hTiB`{cCXlO`f!0xY5fq!k97&JrXCZj1>)m zE;=^)t$WB?<6U&0x_U`U2@pzgL2+Ky;2Pp5+$O;dH@MuxF>FQlxA%?bF0T(ooZv6p zlY1?g@N1=sWKb11h2YTg|CysMyo!c}pqsz^x%DuhDwQR~#;4VR4F02CElFgYmz^c2Y;#%Z)lGyM>Q@XV;? z(um>>Zaiq=siU3>L5_qY%|Z8G)q^UJe$4`!%Wn{F35D+CyTtH)wY(~5r55)HG}P6GW@%BIq7v(U(0IDis6@`UiB*6964NQ)pIhVfl6`3Sd7n4 zpEe(Ky1ja-j&6D`!n_VVKq1D`C~-f)X}#(YEur)Mjx}xEYEo1}KBGU&R7ZpXMrD=Q5c*aGvfQdy-*)gtq67DBT zvzer&*=u=0WWy~fZ<~qxIRq-)V}r%rp{!Zs328)CL$P+qTpw0ftcjhFhS&qsv2Zw% zZn`GRTCu^E)8`j`|4}T=Di`r66O4f^rI)VlE4u!ebwmtz#M)<|DPiqk^4Ul{1*w)DODsm5>nG)-0&t)`G&4p3 z)NWXU``DaEO!m;B*N%ek3dI1OmX=PjJuW5b;?8vMFSg!uMZU0OCD36x*wdU3Q=7L&N*`|=PT z^Ti}gYvX-nWPr+Zd+nw@VujoD7ujDtL$_FM>mvO}S_XvUGJuc$ydax-Y)@xpC-%^1 zQ=&sb(6=ygeCybo%ch0=lFRHNqNx99*0JNIA5trg*Kckd=H;~2ML^&-9aG&KAS0-vLWY( zzfr;h5)Y>+THG)!nw^Y$(ZpIwy<+W#Q6q)*HS0*4w%_^t3V-&Z0J+@eNb0mu?G?r~ z&Jo+Hvwe;$`u;DI`Ni+oQ9l%mHpnZwgGRzo09;9>a*rrRCemKod9*tpzw|Qe8fU)B zGrjt1?1&x{IXW=Me|%p$(@BNL*^1gEBKHSi5R$Hvg973=3paYYak4Ijk42=z_h#s%T|n7 z)6KaU{l**Gcftw!Fx-}V%Gm~`y{jGy?+D9|Xn6HFxM{51*5`o$q-dX9j2 zwi-3Yu^o@WUh*w9<8uf68EW{F0C%x|?Cs}392Kme`lwl9EgIlDl1)~Soirt4anD#; zBCi>s)y-)^W{Ikhhp7G(_=E^(ncT-5C{a3*`kaRyNSr3ayw{$YC^TnHNyZ`siOgC~ zJseKd44*{N0FhxM#Ge9r6uGa1d>MPwk)XxuSiqhQ3kj_B#4z}j2>a2(erP8d`!yqm z8pOyE93hHx{Ffxgj>5_yj|DQhmv^v}*TOmSAI=Jg@W&l$G>AF*Aa$m}9ZUUrf%YKB zBBZKs_5}f*(mt7(x@@xI`CCVx00+~zEAmTm{7OT8Cv}^gOof~6PQ{8NjwdWm6G2V3 zK+w9*<57ZIQ#EB@Lf3;w&V;+pr}GvAHAQ6{nwuXdSf`~Eo~CvOQ#3OMK`{srsPh%K zvwo{5gsQ7{tsvY#g#r>PO$~* z$Y@Y2Royw0!byF%iQiK1Th@2Beq8>ZgI;zyO~ zGa6YCHqqwN4qa0e5-V}@-sz67rYTC&Z0$bI!Awnu2{l8y4S0{`h__kh8g_G~eKzs)445kzV6%m}7!c&Z39;O~8<8R&k3!x1%gOD0| z$X*fr?9hwu(?!QQdqS%6_rD%6P}D>SX~I(7Vd+U5x|6S-fG#JxEI(QOa#z;)QmB~v zY=bXyO_-HqXM+8t`gqLLsVy>md~5r;=4}_4?33jukqYV8Ink9T^i*xfpfjlgNAuEY z&!2GAqSgVFd8CJ0#yWhgfS`{B{^q}w<|?#)&%R(kUmT zvnUZM?Q!Dnq_N8CA*=q}=iv&&;1ahev~4Abv4x6Sl+676{p^4nN4kG6m;UPBmKVZT zY>hpU%8t_d^ig8cK_;KLzaHCwmg1LxyOVnBIi!28qkdKW9kpv&d;h-{fXNaCA0G%; z9=-T$uODo9c2siKPWj<^_j-a`m30oB@(q4U!++w#m8_M40~ULLFXYp*7>6UnDEZ7= zUD>m-`MyJ^XOFbWRA0;}w;y235-zbTBaFILOAXP!vOSn~)gs3HZ1BsX)%@b)r&Dh+ zvQ1wSHY#}&I`vmvRbEz8l z5iKH-2ylog1(>vlCPwT1W8{0e|(!FX$mAn#HhP6n)GjVas!SHCp7^!GgpWg7y*;hUA z0lvhleWbEa0cH@hixacKOC-f%tPg*6!$bGOLyx|2HitJ0f*j}YUzorgaSq`Tx})Ic z{ReDRAA@w1V8XOhFK`S7-Y&VdB{=xklXzHrlL*j-rexiDTC9;@=k^|GG9)*a8mrW=%o{vuRe5f)G4FC(y0qsRDTEltzcD%Vw#fKuV>7ouq zS-&w@wni4OktG<#0`x7E+sBeS)a^)1dFub%T=yr~mnnQKgqM;>C5?L7;}COS4o9gQ z>Dx!J(%(?koR<;IJ__FlrIYEiXu*y}{?ew7+L59a69hN8CG*D8ARSr~n4>c(GID~T zqAnud;5?8P>P+guzl8H{cT=Ae8M7x0N@fcY943gs1UW|ws1H-yb8Gozz#Z^I0wNEo zsiogyeXReOE)^f}uBa{Vemp*VfMOu{!)kC{I12XYoaRndM7b+~{}eugM~_l>__zrJ zk!8V0EaUIN=1ksRhgjoYcX_B`87`L33|DOq(kd4xg1J``ctmij>6$cw8|o8Cm#@E6 zqgQB^%56& z$S4DsOqRa+Zxz2Ug0Y3f??#k%^XDchp`oB^m%EIwmg+VS3#Oa899%l@P(NR^qkZo( z6`{F(&dk-37|j9pe<^7@NW|%JKcj669nP|}et0JzCK9ng(E0ZC!EtgUem!Lt8-_5- z_90V`oQtk0;N1g^!Ba>`JyOY>WOm`E@NYonWZ@T;^TmCRN}hllO=*AVNLF-ZTxbjz z)G87NKTyq0!YDhcQ@ZFmBeU^wz_fT*9eYpNr!4~vW&cm6~1A?RMuod|cP1oD{ZNkHUYO9!MV|kvTDmMgkSP|#>0CB2O ze%*#3ER7K9VAGX_2!QCUp+21usu>>x_eS0bNUl$o<{mXS+Z_6|N6Et1tU>*X6;s~U z6Mv0BboykRhE#M9X;{LH?5?5yNmoYH(=V4YmgefM>FNDlLz$(6{gVrErLo6(R3ftL z#tG+M&vpI}%8H@J^0n*65LMv(9<*qrG#ZuIP5x6T8XN5pd8$*8koSbYkzX`pODzmU zxF|>LhkL)Ys6+_8(}{e|+A|knX1~xf_1E&imWk9cTmv+)bEtaZzON~QH$#mgPKU&d zH0G@yfe7{(v$sH6_4<$6iNwOytT6h8mfblkhN4B}{nVKvJ*_S>9iRbeb(^;YSm0 zSy>Yaib*qUDN_LC=)j1g%XxsGl0FY8VRw>Dz0q3j`Fsh=B3`Y3s*cLi&% ze-DQCTl*89C8^{G(}jNv337wwe$p8;oJF*syh^p20j?cN_uKq>$o!!O^f2>s@C#f- z9hx8Ga(hM+L7Gmk=HexDrP~i08Rz72Cbs8`Vjq%{f0K8V7kf_DOst>q1St3@-!$@OKmXJQ zBclT)n|yKoydN0wq}J)$r~Kz*T=A(0oVAe=45MNS<%%D>>LL6YfGD^lR;J1KS5dO^ zFd_pM1WvWdLyl%|-;-9Xlss4!n|dNg><(rJUeu_BI9D7#5)V$^Z+80~pL9@Z)j{~b zqQx^~)aDgJk&7Sb{C57Fg}mF~itHKVz*XQJx)3eYlCMq1CIY8pb4_Y_ym$Bh#(&zR z(tLTiR?Bx8f5o5nv3q5})67*UwmKzqiJFA(LuBOPQ%N`K1NdvXmPORx-{zl=v1c!2 zq5vP}-s$|QSY+#-218v^rl=%!!gQlLijE#Hu#3rUF?jnA0$0p=ld=?LqYjK*AA){$ zFgwq7N(`MAt(*tcHEgfp&Erdf(v@PY#*Os=mbm9r#aCk7 zFQ?RmgS=>Wv*R}P2$P(x=-I}o<1xtP_%DX9yL@hd?IXydS5_dM(7;l(vFDsINIoiv zMBEEk>Ks&wumZfW=LXS;2e->@KDw52$Q6X;r|?0lRQ;+e)xDKx=0acmibpTA>bBQE zj{ZT$4Z$9xtOyimoOyn|l2&I%AJeVXC0y z>)F)&w{lHiPWwi`p09b}j=G0@(Y*&*+Dmn{*5$?DN5H*Xj*YS(dKClq&jSJ^D&I#E zwrZze|Fb8imzMnMH9ot*E&$N|sWaJR#&3)D8lpms0$rfZ@*VbO@KtA> zRYENm(2;U7G-XYZt4mHk{kE>?Da~imz9_~)G)gt-t8cW)vw+MwJMCW93mUpE`8-)S z7Ao^;MqT9)3Ot3r(X@A#lj_5UBoV|?g~WU1#C;7zJ^!fTOAWvGQ{~*`DEP^NB;)`= zC?GFbD>>L41C|%JY?6Wmv2+cdds7d>iX!5<3KERyyqVDT5Pnu%c!-K*dgimD)RRg*FJuja5vl#^jK$sa< zOQwQ}3kp87k`UAe5b5TX0%DnzlMZPWlLpu$*==jTX=C4blig#MBk9k&1E*PZX}poF_oD1_x=etCxI;J(ta@u>EUaLd z!E(D|YP-$hce=Tj9W26&4WyiJaGw}Xt`Zy^3MhcoDy+^qzIb|7Mw=tRlt#${KXx zL#d-GzM7o|$Zk%;xPSbZ2fx~`F!-4Wj`;UTF{9}CYDc~Wd^h6muXHXHoV%fK)n?L$ z#YX_hdhRVNCyz`1fy1J2%c(Vn{lTftl++)anK&!8ImLG#B%^FMg>Il!fn>oM5vT!L zV;fMxdg#=H=pyqxz%rEh9_C=bh|F+lO%VXJw9GDpd%xdZT^Rwc=T8M=Jd_pc10;^* zYOZiEGXuisBV2nm)qOxZ4PEo59-bBKldVKAxFr2C}o^#cA6JpPv^Ra0H z)PM9Uej|cjpdCP@}7DH%1#zBnW3tAa^QivW=9s3Sy^dJf#9L1^vJR< zh&i-5P;idFgl$?Piz!q)?1fT#$cOMPv2n{)UcyBj1FPz4gNj!5wx}vgSZ%hQdV~XE zPJFxo!aW_2(K9M}4WgF@$R++V9(140OY6Yr1)Q~?b$5CW8(McH#O?v8s;!;MUQcG+tF zzZS4YHITE|?s}#Xsa0~BC9;e~cS+OUmfGS(1p^sE@`oS$4EMb%tFG&a9)3S`;GZZP zg|kCw!NM}x8|n@PN<9G1c|502W=ktZjQOAy;q03X-dMcFHxC$~P}0uUkptNR(_AL8 zxpx8F4$>bV-Va<3ey>wT?I+Wih7>YgBvU`IZYgj%iVP-~nTp^!kxm&pn|UpY!qTUI zW|EH$Gj8XxX3yxo=3s;Go2Zbx;E$M#&}`C@K`fv@!dfy0ur#NSu?|`>cLw-@T9j-L zo}n2BMHbMNR+(LMcLdoX+Ti{!$(;g%a3)oRf8cnOfyxg^N8zVKTEC#TzNWQ7mmc&8 zby*l5xPx08P3heQ_hOy2I z(x(cW)$b0n6%)-cIl=LI_EYwwcRS7_`694b-v^ba#YwEI8S=f{+P^6uW;FOTubbu7 z!F=k((RMl9l|^CF%Uvz-F2qO7Kg4lMfbSeyi6u)~I_(9o&n?FvvbObOh7P=sYprp1 zYtJ)2x5=Zault>)U#^EE<_hPbmFt&Q*gMoB%m`esGI1YkMt@^?yn-4!U=ux%v?#b? zN4z^RhGX`P75I--YeqX5LFpBHu_Ye(-%TU|`)-0^eQrG(p|yH|q0i<5$>{ryhRaxvi}ipPpS46D&+ zi8hfRPLf&ATjw^{ALmM3W`_3Qa}8&m`H}Qwfm==)=3ljDif2n%x`m8%r-+5>5XM38 zcOcl-axas9^AYM?Vj^o`REvus)=CAq!#3dmSbsmBF-rLxE-((OD-OK9u%zk~VQe1b z3E)T0_t4$=+Y@oT^kEzezkfe$zj)|}8wZ8^@g+y)7hIk#ik|!1V9l&WrHNc&N!YNLgEhd zuVW(W?eUu&BJ6CFGQVB!>#dk!=it+HWi@{hVsTwRj^|))^HMqaAqZTSE=c~v_D*T0 zmr28ug!{EOHo>wg&R9%l;#+05S~ASwM&Y(Cpxq!xJVY>(U6ZS%$)Qx`aergo2#d}jv~ z$xM*P%Zr3mT0~Qer(Tv$CWwD?zuH-GC0ZDv&N&1rzv*T+`aEji2t9iiZao6N9Gp;% zo)Xr&&eC954n2j z{FMuMt?$=%IP5gRthpD#~QfurBpKe@9xYMm6FY#ql{G43k_pqGV7>VeT zZZ(&F6fN5zM`+Cqa&n_y7f51r$ZqZ`gbrFL_WIE_#@q*~zfeKlc`UEbx+ip zuG>PV>+3eh%=GVbvSJ<)0rdGd=TAY%I?E(rYT9sgO8ts+zl=&Q#oJ$@>SIgqEqsIB zq-7}zK4eb6ai;BqV<(Uqtgu|*Xm);8 zykwBLY&qi9_5H?vS7ds5-f+Pgyf~R(B<+CDNVpo8%LeqYpEOL5Y@cDDfI-;YdS@B5-fk0u){IfmJH^iV@Q%2K zd9GdEDAAYL=yw(~gwk-*gJxRtRsYCU^IL%bk=Y9?zR!_(0cZUDdrmmCk-)St)b^>{ zg&w4%g!?5n{nQJ4=08SQNQ^5V{DXQG7G2_`OJW7v!e393`%LSQu^Mi?#3-Q7KtYKU zIL^!kh|)KhMA3>-2({pyOXhv3EXLz19d=?Vn9tsWlsu6J`Auh=j!o&JGGm`V^BLBo z13v*iz1it>4)3QBKRVKA93j!vbXScqcF>kB`58tWx$Y{&NCVBA&P2I5k3?(yQ^TvH z4}^rz1V9$$ONN9 z-i1d=yj9jdtCbS0`WqY=zwY<`+4EMJ!o;UV_|8!^1kO}0lFBI#|2~`xpE}p>@8!nR z^swdSl-NPYAg$rKbe|oRc@nzh60xi{*$*8?2(X3e!k||y4pe7_{hF>dOz;vpB#JsG zT+&qL#zv65prIww)D3;W{sp7v#W{QR%)h|Y6}L_2rL8g`7}=Yk&7NiX;`-A8vCqD~ zv`M3Uy2)a$%q*Oq2c#fewBPj3mwvX zXIlI{jwe1&MkR6ahrW9a`F;P&gFg?g6$iHur=HFnqn{GiBFIE79xB7RqO~^ zpBb*l)y{Q?Uz4al>^Qw2rAlLJ1CzdjzbWN=M+8RZH-R|}6n?lPzttghMH7fpAQ)R`yQ!QT$ z(myJXaR7XnV#KV>0!rA0N})FUIc^=_r)ODFAj@#VFa@&bn?wXZIlb<-|lx{!TO zQc3y4m|AV3=7Da~&e;k|DDG&v_K9C*Go^GPV?+(}Go-+%z@`MGq;n~5a z=#AB?h4J8`1-NN^z}C_ znc+`)JnLOKrmp|`ciMhf!*_x<;62d7htO|9VEvE|#X&6cf0*o`a=r3nZ@+7p3)J2Cat6T2?K>=i|QV4oUS}HR$p5D0+l*(NjQ-_dhk;3{{I9D z$2a(MCbpIXS^M_Rq~URZ=stK@2phYTr6r>d8l zS2@zHiEyONu0R@-W^(trmp2@+aP@?w6dt?h!VU;=A|2I>aIchr(>klFzwF76wrs?b zOSdd$>BBJ@`~9J?%ylC9B44|zpv}6VvQI1tRJ8AuV*=z-jXre9a5hewNvWDE(SFOs!8ee-Ss?sT3^nz-&e;3;o2If|g5qN3=Nj(N?|N5$uGn zwZw}@vfwfBZ@}|^jbAyBgxth856NFXm6H!VjLfwEF+W~qxy7~r zYE+uDNG_WStW(&Ee?L^Yxd+Co7dmAgpyS$9M!GUEZDhqw7IP{p3<$j6*0>r9`#$36 zCN|*sOgC!kTi;dIHTEr|>&cs$)K@V%lF+}o-+uOMmHO-mkKdfHy;=6N`u0xh())K! zBS6Mu3Lfx^FGen|`C4s|9rT0O`HuB>y5ZJGfYpb_mGN@2o6k=j!jnyB)E9?FjJi8k zTGji3;q`y7{?`Jy%YPfRs(82uS}xU(o6!4xn72Y|+CGvwneg8QEimoAl}GySTsI{i z7dE(gdi1)9OsSt0cqp&2y56V=J~Y6~OG-shZB-E$D)=v%Px6IQ5G^kc9N<#F{fS=A zTX-?^OLwcRn;MHmi^y3nym2DlajugD=Lhn~NK*CIEv-&JKcMomok2W&rZFXP&H z8hLK@f*u&?uUzqZ+OI7s8{L-84ut&=OZ~Hvq@|$$uI!rfVo_CiswTLlTjuy=`G@QO z#nxK|#qmbn-XR1@2o@3~Fpz`*A-Kz+L4yW&5AHU&TX1(xaEHO&-F?u(b#P~ZIrD$N zdaJ%ubuQ*+y1H+=pWgdf`?prNMZ_PP&4rL=HtQz*rCw}6&BoZhar<-(Iasr(cB`i8 zwHXY`F~eZp@s*XY2qXO}M1iyiF8>vcGW8|P1J9s?|GEafC00(ZHvQTWfQK{(iYw-{ zJR1};+$AP4tVuf=`^VGin#uEC!Z}!PU*M=zzgYD9_re+!Ht=10*(;<$$N~0S$R%pk z+b1#K_{P%}i(C9f7j>z6By{PPNXL&gWp#6p?)~yT;Gg{!;jHXv0M{rXkV- z1d`{Y8jL0q(k@|Ic;A5~xS<&%rbRd85|h@3H2d{|P9}v0t0urdM4`b8WYtn%_0rt2 zB-W^lH^WU>vPSgh&sI==lf>XItHUCqpkwB_dXqgqBu0=x)1Im?c$gGR4u^YOXx45BD`xW@Hc`46 zS+xWe;`zs}YlP+-Z=mmSjsnHpCyc%`DMi>ikN-hjF*~h#hD%u_xr7PE&NrSYRyXCF zw9&LrkabhtGsdRKRioUWU(VuMUAdz+t@3Ns1Bi?;0^r8I=O z-gRfCOupQ+E{m_m@sJg?m+uu_&flBJGp{fgJ22A!KQqAGG6L;3ymmkLnuPHOXY(Q* zz1+{RGrHn0r2|^d`}6W-n?J|L$y006O)}nXl+!Ft-j&f)EkxWUaSlOtmr6n-Ndb<2 zQGn(I8)REpz&DKb9qX>GbMgZ7hea{s+h5jITjtHTp~Fct0Cs*F9MHt;sW5t&C$K#tnlDVFD6-f#81Lx+MndXRst&QiKK!7GJMtCIIP%?T`W(wzn zMDbr@w)bVr){czn2Ris(rQ>UB@Tj?EHEXASl1$e~x|wl$4Tmy=AL)Dgh2!6`)XZ8? zk2BFtC34_7Q`0gj^?Qjm(e0d;8}ze+lfLVP+q6~{Bs%=#RC?Lt_|7c>1@;oI2M|B! zcFRU@X<6X!@lBW&t-PqJsVgPUv=Fy-l_)F)je65QI2lycG2e9-95AnE+HSJ+C+fE` zx2Qc=!n?-;I=_f9ti_Mb{HG;efk!HD#rolgEHy*bc$pOgtD(eb`2t9Yy-7jhN2x#& zL;qoQLR)`mGy80Xsqe&qHozI+PBUxZ(qEBy>?FZ(5`a^pVECF)t=XvIhopTX@RaFo z+nJn!5&e?1a8g+I`tK3Xp#WK45hqce)S~P)n3M|+a`vH zQWUu=_sPgaUAzPUolfV^zqGx#$^vvqs4q1PDkF=KgN+htSw!Y9Dk2SkJh`>RToh9M zFtP(RSr%mW!S?%)=h!&bO@=h0&5oVx;MBZ7@{wSk;R<-Ss0U9w%*E zDak8xc!^Cb#GF)rqYnbM``Y@Imn&CjR;gf}EZ{-S;8TMe3@v?+YTUnjL?1;3{XOv8 zCvTxe6k^hBPC6!D`vyq0!`x4#v3MW~i+MusOJIMvXm*vz_3yvbz{Fmh%*`JPb!?nO zHr?c<>B!ZB6)ARPT8X{lzy{){92YPRuZBmHMTL(5E zWsRKjgMlk9{Bceo05HwDn~FqB#guw!;8mA0L!yH)=j* zA{8QFuh8e6>U)2l>83X-NqTFbeT-S1XfMER2Ww53uh4g zJ?d`pR^k~A6*0VFQQaC;Y#vj;IgElENhxZ2JS+)=*+ry32qwgbk%j(YW~K4CN}>M? z;V&yIzZ7%z;1_+n{*+ZpLfpXF>2a9)2}4#f;iR0D8n$kpLe|SZ14^cxrcfkB>PK%x zgf`NI^jC|hg%c@C=52q*&@Gd5uy<;vNWdpsur6$P$VM{luKD?xVDLja>%&~Z%jyBY(l9@8}q)}4YQXE0|s3#j0w z4M6*;eRs7&+0szGE9a;awwTwYP;P#-l-IqoZ6iwq=~APbQ8Woz?7PyGWs*)yUjmUc z9vY2#FAwf{t&XKHK3F=;TAQzl^(1{zXrL44M*kP?$Zs`{lR9o&Fczxe73sDB`PC1r zGUCznni|VG?={Rz#IBjKZn4Bz_DSVwlw)G6+L?cSUsP>w-$Y*j%=Ix0-=%*~hx9h% z^+W<<#;ba%p@BjRq`2zlXpolN^m(}Ts-`CtzL0*86_8X8zOGRFT!hi%&Y*=#8{OG^(k@OzH{tY z^J1wxp;!GMj_TLVd*K$B#rcMRQWkT6jcwzX`hoRj8V3c~u|lcSsn$P|G?Yn$X!H zSmZT7N+&aDZnG075j4JDLjZnJ6VMseZ#R9f*x=(l`|!aOWjE;J@AjhKr3}Tv1M3KHzGiiRv2B-^D~x;I@G-ns|KD11(j< zpTuuC-)Fq>cVh{W-sQ=e`MSEbP;I&!1QYr$`%a|Q_oW)_Q<^@$s4WI2WoxA$IdDF7 zUdh<+o4=u-Vca&p$?dK`GSTEySB()%%Fm6(mw8B?!Ne~iA~$DQR7au#zse2xM;Ms{ zKs&1cJ(gpbo|2OVyYlYMo*FxpMNh~3RHAUitSW7z36D( zw$mu3SUJ0_pR|o@xlF?JG%@J2spYSUKo6JPjCwat1>*dJB*S`FOg9{fc(>#`Er%nj zIxU5FHSc7n4mS?(*I4BS^zHK?zY%~?xk05!Y!YFk_;9C8x}WQT4k{6vKRnG9#* z4f6O!tS%a?k2JeLMsaut#G=4^eWwm4U=b6nZE}eMzTY`zWYrdPr}pn{%j7T{UoE0& zTl8--2&D#SbXFT-vgI0L1;)u?j=ub7D-;fpdZxd1yl37QT`&|*3C{xQI>H<^a z|M*ZWgI==u_w88mt3=xNj$ap!J*dVK*OA;8t%H0&AmF|Hv>GA9Vz$hSf0y9~rLOpu zN#f&Sww~d^ME1C#s=SN0(Na=e2?g)ozcLl zO<<~Kz}r8igYTWvt5I|tNoD#@@d7x~d$2)k?Nf$Uh z6uSOmQCZadOrP!Gu{k!sejnQ4Ub+SH`uA7W3&?cDh41rohO`1#LAE`LI;~=LTkGQ` z!Jt(+6^UI#sxY?KKs?}Ucs5RbJFqb`iiDk0FvS_4>6ik?2Zw(7Qi{}{TugFfhPv|Rb+QHPHu_67gQ%s4t?@S~IHRivG*0rpn-71wZ)?=o@i>OOyLC515u9MUe}@Bzcav-i6RWkL7B? zgDV+&|3aTRZafl8f2k+7mQb0a?-=<5uNlCeM&|qnTX*%wv6q0%rA^L^aD1^$ZDKuA z&VjCg*;aIBFdd!P6WThp1?wDQK>65ZPJi?r^PLwDm$>xx_)lkkVpHwGY*NTWRE}j~ zY7}!1j;uUe28mIt6O4I8_JN_UibRYAr1*nuj#8$_C7s=AvAE?_^AisH9(N0eLEnvp zuoHdifsAo))V32(1)!c(E3@fm-fwbv1ozukrymtT<>nYYlzRVsN7(w;y83bPDA#IJ zcZXqnlhjO_|07R5k)6QZVZJVfzZRzG z;0vFO>Y?U9>srCEvOP_)oOi=fnadM&o)AXmDlL*aT7cm;jT$u)Z4a-0?edqz=Y(tJ zv!(iI%Q(HB{H)pH2lB&jn-Tv2!7&Rr*c}#kB7K#6;bthO;z)}vE^)zI+RthGpe|<^ z11P7}cafx@kx=Lcoz4R?8dc-lKA=#IdV3=UuEyP{Sp8G*^^$}enJV>sq`9C2TEf7N zUBmoDap*s$(OE-D$FdK{a$l;k5L9b@D^Q$u0M&ajQ;hc#sX`@;5?7rWMuGJS^mhoL z|EmS)z9kjbi2(G=QR_C+A5$?ZVH%eo zg_UQ>=O)!jj?CQogLL7S)S04gqbJ zV`kc594NGh{V=KAK%<$XYkx73e8@wu`_>u<=Oyt{lt@ZF?p^!_k#o=kNOg%zSM@l2 zb;Z|m0IhYU%Fh{Qm@0bu%zZ@VQojpi5jSL~4S@OP8<}K0Ch>hG#rpUyi~y`UI!=l; z9!ORz2HUj>-NV|!DG>W*Oc2gJ+3RnJA&e=|^m>XRwdgHi9x=kDRLphfx!fr+HtV^W$_E7=;*rWCvZ!6UenK zwca^v+%qHT)e7k=H{Be_I*&yQI4Lh?6syu$rcRs0M8#MFf2j{<_NIF*G`OZG3~kb| z@Kf2>80dP@Vlf7^ja}bUCl<&#@}%uEuhVpmnTu+LuGhGV;4%Y$+(j2Uqh*5CToy$*L($qx z-kN1^jID3UR2$p6>Gg+o)dF%FHO}TF;8;{)753bUt4r=lo`ro6jGcaxL%n#9X)V-dXy8~Ueo zUz8?|G9u!pu3%5*V$&B~FR%9@K|w^mNYz$I;d5bR&`)4(8d=rYTp(w6>SxO)rx`&% z`!S7){|E%n=Hq0_ih9c74bAW*_l3Elk zA@ViTasWXqjQaSR7o?MmZsq?y_Wk~A-?pw4kJBo4Pv2ZRf{c~DppkC5-COQgpVLk; zgx#ofCwp}ikA#(hgWkXZLwJHxPuZ^9Yh zawz3Y3F?Ar#V;IX$J9WcD7wpI0!sv0S7(Z6Wgj7meg2gUS_S8D&W3@FcxZ(csHx z07|{@NaZFR>kRf{3Q6#1^*IEMU3RyeS%dU7NRCoWF`@wKYjlKXkZ&SN8q?ZQ=)87A4$)+s1r+7_lTwMgsB1oCW1K;L zb`2M8xJ}hiQHpo6Fy-*bsQ~7H^u0I+0b^B(XpOG-=&WY!KoXWdaBsz~;|D$?w zY>dgu&u-38p=3%*rA?QXh_~=Rq3U(r0Ow&jYeUzm({L>z8fP zFQwbV>0d4Fe&R0cVowh|^wJI9o8Rv8ab^dbLGiW+>;itH^M2s)>ShW>(VofL&(y&c zk@Z#AtDomgBV=sd^fv?Wt$uFtX+#+7(6KVj{|wJGi)0BbKh3=~V>O<-pJRFVGt0n_ zWg@mchP5F0pZh|@1N#AZR*|3}HXu(!C+gH5DYN^kPdGk+BohK>ctuTgbBYrUAN^JRz0Nm+h1Qqy}iWn(rfO<-})q>ODzYVMfC$?4>=4N_$S zwEu%`7X+*I9KJcxncl zgI@?>8|&Qt*Jx|iPq=_zepXS%A>7IF)0w2%Bg9yoEVx=eIKv>_IAru)7BcmZoYM#R zonh?nmWZ(Y=RJL`xr`DI@y3lPU1y-igku_^8C&=DJViwaU9?=!m?W%5uqKs#Gp%jE zLh&xj<=&l~HMtL;jWnTpPMk9e}}cg{6A#urY8u6zpK zzdwfj-h~;mmdHR@Ki8VMJgeR-9;5n6D9StuE@cNtW?sA~LJRaX{0c@D{u2i1LY?@d zo|BQ9vdCH!4*)XjI*73fWuZ3SN~L(u5eaa-6PGe4EQ0uDdM79};ZqCIE9O$8uOPh{wSF&0y!xI*tROo^P^xX+Be7-pk!3$ z_Tt7RAztbz4l!xtNnWO5tQqraa50yN2~fz1b%^hfBcs>1%%JGqm@I7b#ZRk?{5IMc ze2$Dee+bHp z^C~_zQMjpvt-|qt>TD8CIx<|7g?pVdB6Nic8c-q3UGc))nTQdqpKpRyUsaEP63gHm zF!qk7Y;oawMf2FXWOeu$KIOtC#jgVTBlq`o=|#eIT=>_$hn{_Zz?-c#oGePZ+~5!m zVi~dTgv=N3(`!V+DYM!B2w-v6h?q~cwR~RXWjU@wO?#4n;1`-;nFYu5EKj**(|6X- z##I>);Jm^m1KPpv>ca;1QgSsxqHgpdf1ER#7{mMySFsHwq_&xFtd?-p;`Us1TLH6f zluYaVlYf&(m28r9mB$WmEy@Ns5A?&9&N)Vt7rYe*?xNX=O% zIn|gJsKlw~^x(|E=yR5+8nBq6@C!HmC#CiEF?iff`0x4?waQ+5bJOB;l3vdbFA>|; zKueQN8rQxnEX+AVmIqo(6$6a_UyRj%T9wMRB$qOCX`JdEztxFfAgso35%Ny4G8Jd$ z=%JInp3rgpWpyEAV5HH^qg6t(zjbqR=QwZC$1FT$*NAQBYPZcNwPw9&P>T5-$ML5n zjIoLN$(->9r0q}Bq=w$QeP*n0{(^$2oItanV#R*{wH3ix8`tzT-xz_;H$zThC{byz%XKB*XF5uTR~8}#j0*pu8FcD;_jqheeZe(=gP^n ze9D=$XlvC&XX|a1!4USk)2Fwl1P*g25$$Q(6C(Q^67w-NVG0-rxmhP=t7gz3>&!*Y zMf#c1?j$W_uViLCdTi6aSMJh&pOopc2)^HKIdO^~YuxyEL?hV#a7dxkes`Kiq4V5B z;aT|Xzkt0QFq4A4P|>jBaaqw~-F|=3L0ZP|rsFX-r-nG2F74e* z(iK(hoA02z3@xX%_q#x-Q!?pk>qr6VF+1sC@cV1iD~mDCEvd@uhpE)DyNkjFU5{f9 zwWqsb3giV7)ofex7U!DpoW13Nr`{f##@1!quJz4o22#hWv?b#*L*tNZ?|c1#YlFb8 zTWj?1^HXi38@R`C3()FVccbk3z+esQ;>vRNvC?vHCB!%c*H}WYI@00e-J$+ghU;Cs z(`09iDFo%usZE4;#}RIp#0X6U5xA!os!AvnAGAK>Hjfm?BHY3^%+tXyXg$^s*l`LHUp<0_*yNH6C|_;;RYBNt~wi?T`czLtDqS3>Zfi+uTMccDjauN z5UWdNq_gs?#%U{)2#C+61Cv;>8j)>)xosp*G+tat@6n-qY%#X18lJXL zIPKI+CjqIs)IHYnZ=m-kTA)SnUPJrYgqqGXs`4;fiRRApcnhp~Spd$MG%xrxBu|KV z+NKfIl_4Av*bH;u?e@wnU6#=z#pm!;z1+G*JRNBV!_zq#IFmt32mM?c$~K-`>DrIw zmD(QHA9zd*FR!6uWC(POQ_r`UkE!ziB1sFJz4^=8oJt#bZ5EN+!PN zIa`VxruKXF$LKBS4dTGn_O^FHP%P`|0iJ!=K+z>2@5*0SA9%Y8AJSV@60&%F<^RvM+R27^;G}qd!u2#h4}o%DB!)bb@!v(_@_C)t1tKoAKnSfufzzFAEwL59iN_-? zpF3PRc-UJf`_&znXm5y%JomUaG9)#_J$*LyZP5iX!za~BJPi%N|NwCL4k*Q3f_&(J}*LO<1xO^-*T_5TGRHe zE#yGgv;T;^ST^&fB|M!)b7Y3>u-jD2JoDE;-B=!REtOGl+*P%6kdz6LS9JBVJ~$X~}TA%^TNnwixGAn_b8gLC8(XNiaYRlrdAjb5)nAX~y3wdO=FsK`Fbpp@i& zN)ZT~SpbaOcfb05)mjZq$O1eBr}y_m?eG~qSVZ#r0N8$HN0}3BHqM1DtJ@|m-f(He z#Z0z#IB5mRLZppn{4(?j<-fWRK91n3h2Rg&KGp4TsC!mEowVKWYPCP@Oswc1W9!ku zp@@>@F^;4F>+H(Y=qSkz7tN{kP z{;*u{b$i6{cQX?seapRtF#JJo`Sv6@ae={eN1RL3;<$iH#tK@u($ry-|9$~QMepe{ zW3IhFJgP;%dQI0#o)11K=r#9q2N>=Zsg@adg%#3ql`7ABb?gI|5BS93!gbaWSg zKRTVWdDGS_Yg*_vZj{@$OO)XAvF8cK^5WBdkDkL-W`}#XwEP1iNuI%lU5*>p!LI~j zzcAKAbY)W9!r_kxdU~ITQ_RL&lNUo-!Z*6w5LcCq+O(DYbtOJePZG#A777|6q@cg$ zAcRvM#3Z-w|Ji#ylGb(hd(hzB%o23jFFWqFpBG!wIeNNz=J1vxO`ShOha+2N72;;pHJ;SV!PKP><-` z+Md*5uJ#}afJdi9gSV|N)0UV>blJcV9jP<(Z(%*W+WExpT3JYl-! zhxXw9Q6Dg^PFMX(U+00(&0`1C%Y%0rbQA6)6j>mfMUHP&?bq037k7xe6PHKZYE*uw z4%5~Xg}eRir}LKAbH8-m=0hj=kLhsI$4SH-4p-U?M7WZg@;{Tgb@bEuj!Ae20T2JW?Iq{9WO)mt=~`d|d&xl{;J;*w~86sn5yf51_8u#FtM8>n`A6 zw#1Y^3^w*3GxvliadwdT@VX9Ly>(l+GtH)X4gq>U)pju0%769ThHzy% zkA(1@`(X=>MBANF`d~E=>x{MZAZYiEuTOlg*7*ey1zS*qP)m)LB!@YIO7`vXO?U{o zH?sgDYcBoM2-1nN18qfMU?8TqoSv5fde7Tbw{D{Z-fQ{C#l7<}Mj8cF8I0ts08^if zI1eUrW#ydNj}}_CLsTU*6VDGxt2WsCMdy<($Ipum?YDd|7X-$jXj=7zO$#pJAF4eR z)qk@ScMF#lD_*A<+xu!#X*tcyH59c2MDm`;P}+`ndW_%ME)|KZsm6x`e_GWQF?v9) zPXoNHOqYX%ckV4hP`a~{5zBiv_K?$Oba+Z6K>K6KSLgAhU#uU_N+O0wOZTbbTKF!B zZtfZo)t-F;zq&?X!(|!{66=ISJ_k^c$1K(fSDBdGH#n0fd>Y~U)FREPG5q<)FD_ZS zwNS`E9Qf3!@ks&%HkiqH7pd4E@s3urgb~}^e52@{J`x)FYQ@VGN@=QA5B@16y){U% zE^J1eg4SuI=cSGek#c;E$2lRbPt&)q4-{e$GOpm6_FOW36r<>USskyP`~}6iIC!Wc z0yDIDG~a$3hKI-Nb|&*0TQdz1hxk>+L*)W|=|{_GS-@?n!1aT^Ez9K&46c5josJ=b z@6$?@!nkoDUb%E!hoKSb3rZj1QI>2;w6j|Mj+Ef@5?qgws&d!GBl?VTcE&Js@URVP z*h8z4pr{me_Bpv&i)J?^W(Sx0cSr5l4B+2J?lJq{c&Qac!Z7M&A_Vp>$EgMW@k}$^ zkVep@YnQwZ|Nb%EX7!(DiOptifZ;&GVn(`6fQqsS$wV@easYkGOdtuV;`u&^qbXGy zdUuiPJ1T6T7jXTH=X42J!{Sgp~g?XP2 z)gu7ecQZ3-dU_6VsKSls4~+)fOC> zpS#cQn@O#Q!>#|!vQ6FSgs-_Q)$VULAJMCgGtxzp9R3pAkG<`p+Dpt>qfWwDI~~6} z?C^9y8(fCD)l~Xy9OviGxiB`aW3-$$>5XF#P=I7!B|Pa(`&I5Pd7X4nv_JSTPSPvY z_?0cbV3!92>DE0?y+%gH;k^wZ3Y918{v%Awj;C!SaW40>sRKS|VAL!ph>ja!;7(3N z2>4&0^Sq@%t?SuYivlB6>Iwi?@LbHN?(Wn%WK{*$WMMvZ{n@2ui)&nfP6Nkfz0JmL z&GdQ4pN$YmhTERmzkT52ewcsjeWBOBTh-FsimmWLiqki)n9LLUWVc3wi2L5*Kteh# zfN>mtDR^x?j>;Md@P?VZ7@|#N6i*CUn~(%I_e}7^f(D*$iO6rEx_6_{ylZB+_J=&; z0}q;xtulTb{tH2>WH(C9PN8G>3BRn$BoEl zB0KrY!12a~=WK|kb^QbKH-Wf_b(yN8u-kZj3SZ!-xQ-b3ZHfVNoCMfE7k2aVZC|G4=?h9w8yJC>6 zquH{7%yxeB$Rsa@FGHm=m8cuiI$HqyI-mf?jkjPo{&5DX`uiz}zn8v6%?9RQe)i>? zg7Gx1AHC2$du1w*G@YjA=D%5#tPNuq+~I^jIR$H>MY11nn1P)jVuO1rNz_0}Qw%Z5 zk8&y@43MsrVIHxUKYPqM$84a4Kb8HtU$UY7z%m|-NC~AiEY~+_2k(_?W-F-~hEZ39U697Cz2S z#BIf1Ng^?hvzZ;op#HqhbC`+kiuHTNX@TFyp+<+PeLeX4roS?r>F%&QBE12&G22tE z#NB*g(*W_jWFXSJ<5;mfZamt=8ecYU&{}b7&>B8L{Q81aBR;hOPaQ7PVc;VUHNB=* z=?y>jY0n$ui=Ez{>Qs~X`Knz>Z2&gH$68fBl9&Ecdc^j$7FBnVxGCw0(Y;Zj+c|$L&l?%~_o9AfuXrH4))RJA z*tT7c&<4l!VYqm1hwD8J?o?{FZX6V5LgVC~L>f}7^Grk1p%nQ3-4ic0_P zW=QNZY>hJ3OA}u z)b{*S{auaJuJSo-IH92%o?wEeQtieq_EN0I4*jFtwLilg6q;<{O3N>r@Mik+rgA#Vw&Uv&`4 zD2{3NiL{OPbj7m2^CedP_UW7}Zm?jG`;FT2zgSao$@^a|0LDE`JIA&#l!c9VG#mRP zy?U)vBqhk@LVh*Tt`z4AS>KXs`SHQ62hFC#Xd->!D#3NG({er}c73b+;+=WWVX!N$ zN11t)8xA{%@cAoKmO|SChNfKXzx>E`B@Ra7-$Iwbcr*K-PXBYMfawvIA=C=#(PPPyrlfXoNgw5%6nj7p^xcTkA+VtQ}$-j#C zC~adpCyvh=6g}7rH#*RV3)2&)4usuv$0Gqm+MJ(*89cFy+uXIZFmPSYlx<%K7ua5r z&wQg}y69ObmE{68g<2HUeJP9@Z|m7ukl9Xcg^=uPS?-caTCckF>c~pGrDJ-dWr>cs*_Bog)VwbI zBcM2};llbYj8sQ#pLz=0cWSuz@g-t9KHDxjSmK<;T@~0el{F7|p9HjXe-OWE9TS+e zPlN|0_lfsA`oMc9HgZA`!TF09wWbpDJwM%t1@MRvF5{NZdv5UOr7fKA0z#PaiE>!Z zdU}U1jF(?@$Dy2zTHnKhh;&=w=V`_|hlt)}cOL zP=v#kK;SVD+Vp$sV|gi?S>)k-uU4^c+IDZa3Z%*y)0NyrXR5_HJ z?md3<6#+-8iXABO6^BtzqAicLH|xN4z7(O@AnRle4WWw}-;n%K){)O99I<|CGLiJ> zo-?oJ-lTVBc40c+y+E<{qpf#%FyU4Tnt>5eDqH=S+Q3LMGIyk_S>gVG35hc$U?#^V zT@<$G1-j;9NhBMZn+AEHmy7#Aw^rq>uAP~dj=cS8%0fdhG8Asrd#a!_NHqt_6A7pYEPHH?BPv|Bp^eT9YwQ!5=;i%jneSTwC+uo%k!UMWV;>}9*wIZ{=M;Xx(Q0QZkqTT z^7e9UivqNMwxFwO@B9FtfLta=xox6KI{_ZR$Wt#-R} zjGiwUkk8&S6}lC@>GN2~S$4M5UR-Xe8$%CWP^Lhl%nE(y(lCaTg}&wQdNX z)8K3G%NRHV;(J&*yQ(Xr=Rl2oUk~9j|DtiK0rsdsC7`!#zdIi4{p{Uwox{J*%E`{k zDcPSC0T>T6tf0=%YIYp4|rd<&6#DAcvyp`XHP_fmtAVEYMl?f&Ebo4P?f9*j;S8K^#|@ZQtnJNA_G6%b+4liP z8Un`uS!u#9?RN28=35`ztZoxc7Aj9KXat`QB3;@S_ZdYi4n4`4AL+gqnMS!8)A+!2 z8d@d*w%*}7ArBP|OW?aLv~0w~$B9DO#uMIssob)nCYp;X8svU)=TpK1~m1FXGnD8*Cx)l)_~Bj^~an zyF;Eh=^gp(2Sc)(IMepKxe0Bro37;6k0#Y9AzEy22e%gm`wV>Cud-VY_vemnGcGc6 zCu$QDT^}8^ZGD#Er#Jr)+I#|Cs#rt24#zGZ8lfb;1N9-h_fF8D_4t+yDmq$jtZCwU04XJ1ckKZ`+ZJB<(o^h?f{XxndyehP=` zI-kokd0;R0x++vaou|`nE=LR7&mldkdV<#tf_72Hs20@|?I+nMZY!m@EoVH1SvAcF zM_eH2UQMm)kxM%p4xTvVc?$`Tc*Q|Col4=lz{Gc147VuwSD1?#*7$%OHFx|!!sUBq z$WCWr>qcjnD~0RBvS9ngHjwPWUyZD}weGQnrt$-HCTaYka|C)-L?m#HR_Pj6dlN$R zCS!w(I~bh_zR-|icR@0I&^Z!BlkNG8l?}G7?%5nqEqo#&zxSs>oM#KV-Iv>DJ@486 zLTEpuhj3m$xMWfxGk07x_{UMYC0}+oTPhmt!vpqyXI6hds?9;33ZoEuJn>r2?$2M`VWYoG zEIRsm7Q#hYtqTk@2+RA&zbdsueI18$ACMoEdRydLHY0Bvk3XSJvw2v}?Hz7IXBh=K|$!1!WPE)JnW(2!9%Gh zCj0@wCIwa*ppHnTiaGRxL%dPSr@4G1-M(hn=*neKbr*}q=cZK_VAM+qV#ES8(T5kN z_zaudDge_LdY>a=-!W8Oj)9)0(1XWbELD=zX_YzPB%rZsm@&|tG6OHnaR>TLxR1Wu)42I> zXdSO(b5b+oJlyA)?$Qxpyj}bJYLC4qJD5aacdHMfgA5onpDCkQhqNA@+&;>8IAr&D zTUQ3O^ICmla_`t@bGctofiO0N)96%wIL0kkA$3*I>ZLGASF*6*yC`70(U3&D9v5Vm~n&sd`Y*>BLj6^nZ!)VG+@u ztml4i_`0dJC5rulu9Ayr)D}E@q-@l(jPN;AjHjfyb zsn274Y30*}`JS$K7aHzEWTD`J3gVIoaYSL=bbZ_tP!2NAPxEkQV3}o`Q&><)oOw>D zrB9tHkueTJ|Kp{qlJ21ztewd)xmQcTn2H(Gi{QvONf<(SYK zx1@5&7y29fRm|^=%FPgi&ZJkLt!5Qzt|hMMYDE588i$SiV~j$aWMqN z0S6L;M*2RT)B{Jjpv@ID+^k$}u8m3g0q7}^)JLEX%h|+aa0YiNL*k5RSb!QlQaQeK zK&B|lXlyy=H}z&y|K1J9W*)r$ZYIF4Kk}iTEF_=s1zpWIgKJ-~5?|GT-0n_2ix_5I z;9ay?kkbytz{{7Q*UGZc4BH(`{1&C_O+U(j%xk%C-_<@UnX!Ce*M3jm$@GTSy@ISd ztJEQQojLmD@lnvP5LQg->~|Bu-kfiwR%_?Zg%185`<@o&0+(334Ql^1en^brsGk&$ zKiOC+f|y0K^X&6~ExcQ_W>lG1Dov|fyx?yf)wf~IntNHmS+}QN7F$SlAr$$k&&ozB z^|h1Dw)6`rr$#rL{j8pYFZg`|Xr$^!I!@^)DjRu@k^831G;W_VM?NoH{t3l?YZ}M9 zPGj4jMEijJcn+Vsx)Dcx6D6A-J`L7qJ~uSc=^J0xg>;c2NG>2HSCsqbLIh>zwG-#c z0d=~qy6q{CB{=bMv_)hc6Oqn_q$weHzk%G9=jDDFtKs6m;8;OT^qtfvd;95&|dWi1Xyr>q!Ychx0;PkFzHujPJ& z@l6*~@K7UB4=CN|4H>zpFRSe#C~q$eEpCS+$a@H~dMhKdQxE^=x_&RtyK|jAFZSNE*1gvH+|z?$e|QX}sT#H%5aGk*uNSC6G?euqvo56aij?8_d0JLOQ&rSamE{2{wPd+KG%;7z%S~6J z%8S)^($wATv!1Os+$}cK=cL}f<2V*S8fr|~G;zOIRuOIDZssIlFDS3?ZF1JIv}`lF z8Q(wNzja&1>jE8FyoFY@v}-r36^TcEO%Y7G^C`3qSfUw@RYccdj|=k_oS6xbA|FTK3*4*HfH{$lVqsz z2^6Uad(L}jxin$^)p&b!ZBtwmI7r^1%zO2PaY0 zqWHm1WKz>VeHn9GX-nHrpgs$iG1zZpvG~m_OtZ&+>v*r2Im%>mmn7Q9HS*aQdfF9-2cTpu6!M16Y~*skoJ9Y!)MO;no0_FQy6?K> zc71dIHva5yjS`NAU#6+1UqCxW^+}w_|7}=oq$xXERQdU!b^BM9@8&iH{jYzGnXtG?7FlJ4wZo9- zWZTzjVahBj?CmpWc=;vCrEPAz^~?%ZFTOAGyq7P&`#t?GzuNPUr=G^5yp4Vbnn?X7 zws&wG>-qK%fw|?*ihTxIjmGyTIlKK^hTMlpd0#~+Fx#@cB#(BgF4q?s)< z$Z9j`quk4ACZp#!jtX%PLq1{vjOy6-RsU&hNHT1CjBkutDMfO9cQ~ed?6SxC>`Nr@ ziV7CUItkG3+ zgo}(L`$lvZ&~JmZ?bresiORAV(TLdg6HZl0 zzWgGwHgWvxi1ekS`C_|GodYB0J8{q3^sMVN%ZK*Oty3J8|E8D%-j^}y@4-Bi=UxZ1 z8i{w;C?A>RR?z~)7C1H4bTX2Z-X0^LM2O6A-|}(_z8Hv$-7<7{HSCncf7mQFFOqzBv{oq>Ot3T-yJFWNjTcA}% z;j!`dSCzs?N8Z&F7#X#OY46(1x<5;yZb%|SpS(V}1L(s+Suii!Q;yC_QWDeQSdQx>|V45f|O3&-gS6(cG~++daul_$x)eZYONvBbiYcV4`iM{P5g0U znHFtOSdmgAF$a>rV5W+1l)AVs9Caw-dOI}r`+gVo_c=$s%BH|AvLY3UTp3%AWdFTU zxPPm5*YQ6FpcwBiohqq}AL-Fp{Dh|j^Qz$9jPgRbXm}m{gI*6eDB#B{{Jhpw+5T|Lt*-~t5t1E>>#u*D1^3U>0gYvAJsi)j z^WrVH!kaadJV1v(z{5XQ&_`jqE!aCGdJUFK66wz@hR*NF#O7g39#JdKA?3ur)P2g( zY;K>l2ncuXOgly|3tD{|JG>dcj01y?u!(fphqvtpVMY;HC2gRmOk^kb-quT z`A%H@N7r^M-lJ^#?gvI5sR|!gx)_fZ)YnbO5Rz6Zq`36 zT79fb`*lmHd1RW)$Hr!+eVDl{8LdkJ-p5_#a77C+SZhI)CAEp>55O8wx2AXHjU`1_YF|sXU%d^ zNyRK<-lyHt3BHkbn(?BN znkfc>e=X;lRc$93nF1qxf|zcC%*lc~hB8#)+6m&mN6b=t9HoVatN2}9Iv7`9Pp~>b zph)1=$O?im+O~Fc#qM_m%t3}zo(AqqkWYSWH#Vy6D($pRQ!P*%a|8;?optfIbz5?E z>C~AN2kLzK1vYJua7o)JFm7N(2Ulo|ET);1*(=@%9Ngh~RBQ8_@v(Q~w#ewKTMLrL zl`PhmY~s2zhQwX6#?^wajs6o~_(z!l2ZeXn?kHynRP0FieXa37aZL-Nw{-JaG7Zub z%;CCp7F3mLgS#HDEBt;(I%lkG6-G~`aX!YtD%w7ylKbYqi&)dP;(hO#uCkz0Q_+*C z8saP(yw*~H^rph!2U#)c;+CQ+`rXE2_f6R5E0<4UrQ^&0>MtE5#J@sS4E!my#7FgW z>y?jw`Tk9Ut)f%<|JuhV)T#TEgPw^} z)m8^{IY~-PWxUwNkCDy2oq10_*JL~@|HLwuSiaRj;!LUxO(I$yy>YR8wtT<%VWqtk zpp)8B!^6^oxMZ*e08n`?UMcMzeKY&&9Wv#zvLaWC@DDScD@3^9J2}5jbnI1`C(-(b z^jrkUH(Ul7><=_*cEXh)zEAO0Mv<(A^ecos_q8KVD7hr!M&&LD8w&C0^WB5EtO2D5 zfmd7Gz?_0`|Gze z0s3WA1o%ukF#|jJR#$dh*>*PJH&-z&gIY>aFZg*hf&Yf*d$~TnC~{;TBxC-JK_h6S z{1rk5L&!Rh_QTC=s6wtA20P$!K(9@dP^Y;QQl|-qG$WigDtID)BCfagj2e!oTI`aw zX?{HDcS{%50h^cS>&=HGA`0DrEiZg^cDaWhwYJ(x?>Jgnls+;5?QF#4dk7lXMS!l;J565Y=8t4 zuC>79;xJa)*hy`wIa%u&$LrJ@t%><{mFbxRyZ8sT7Dv1P(*n5fxxgRU&zh(fiV=gp zGcwDKkaRpC0&v(&9)4XoqkBgVdN?WyDB>8FosP<89*Jvb^``)shlW)YxQJcdP2Oop z0aBDH4MxrkKOheJEA;MDJ{(T?O#sar@?6>5$X+juT*57 zm47}}x$SxwW*Qb%x%W&?^;sr^P3h0Wnp3H|y6~D12E8CXa=WX5MDCW}0E%;Sb3T=$ z^;6!Ss-KT55|in6n@X43*LO(}_jCp`PzIOqBOlwI;MvHvxh)HuYSofUSc17pop--R z(Z^nxxn*S4&2@03eWd&B-Age?qYkGqt`g$0UrK^g&x7`>|Ax2H*Z;HC&YM!@i+k_z zB&i?pv%y;G*6~c#LKmuYs4w@x?Wx9HSo>DZ-A`=VxH-|MR* zZA*{WFBE27g0#3x)EJ%bLFr!`D}ktNW!`1p<{Pe@t@;b{1U^I;#md&V${^~LNrHn> zQon_nYQFQh&5~72$J%FyKFCqSvQzV*bhNLg?BTsw!X|aB)Y(sHLu`y67I$`_n`D=o zui-u8yCk^Qh)^258{fT(<6?J<6@bO-Hl%#Ec=zm%PUvz;RWBRr+za81S2ZT=it6~u zUhC#|_?=Vk;d?h^wilof1AGtZV6OhP$&T>`#qUDVefX+}!eFIa=Z3i}Mx_Y^2}J$5q(W1jkR{)(F?#v5(rA zBW*r|38C_h2W7e6%-r+$nN~T_-pHVveTc*P7Q*MF{u`lsUi>1R zW_HwnJmO^36}}s=_~kH&pc(z`z~Rs;1qYc&D!HiA(-{6ABe0G0#!cxA_I((a1h?g&1?-$b z`4r@D0ZV~AE{q^=g(iqz8&Z%QJ#SuNN!7|5*zLR5?o#087q|SE>AxSuZ)P-);}(c4 zZ!KM*5*2Jd!(@^cyx}@w4{X)<0n|l`AElMxT=g9)jUgu-zd6PlQS+An1xtQW`DQZ z=>>t!&OYM1z|LpnsWYG6RL2Nwtn@4YmneJqF6y5(epvX#$ui^4JHB5(vw5%GZgquP z{uJsg^UznCdR7JLDFKtqyq-3%RmxQ=G(Iyv6%YsN(AhNqretZjc4ko;I63~F9&S#w zvTtf-wr{@9;}_&vY*^N?A=nxY%3^)?>{|$lF^LN7s906FJD%7)^ynug zng(WU0RbuBqh6(Gg#$ZzZ{gLB2`76?0SayraYjvwX0+d|s zz+ES+?i=DOcJ$|u?cLj;e#5xhfjz=8d}##_vJrnO8nol+#(IqTk>LJu6qkk$f<*L=$=~)B57o#vkXSnYqcyKgdLEDqf5=U z+jfo%nRWhyIy-utUM|w^!_fus7TYw^ERn0arWsS>j`oT|UHNSmswUks9(_g-r)b+3 z_YoM;l}c(vxan<5hYFdkmDJ1p$e|3H&LHSx_?Rp*{UxVs z_Gx9xXhmUGf702`)yg+M7FCMdJmXdHa|UzrDVKo~Pc~u`|9J;b3b>{EaHd@Bva{;A|zzTJx%&ADJ*K3%t!iWs*mlSakLL-UUB znmNW<`NZQdR-`PWZKnF7MwI(peiZzUmsz`qE+F`esroGOh#NOZy&X#UTO|Gt@{6&Q zSy;bc!DUpw0QN<$uR`Sr#z4KSJ~gS&6?6+yUaA`)P0AXW>$og5p#tQS`>w z`GpH-6Yg7K!DmfANSo%~?3A@u!W|(^YX0w_0!d ztz}A9%)p+#&lEaT_V9W8*&Ech3cCz0Z*TxJ$X<=ux&2itD44qt{BPOz_se#jV5a18 zzKtTUl>?<~Y;lOfBTDv*vO#%x${M;@vIG6`=YBPpv9Ckcjc*fo-d)~J z0GsR{jO@x2-GK^Q)01$EM0js(F0FbY#+A2-+GM@Q9~8IWu;ca4TW2dUvX={#Ez_?i z)h6VxqKlPXF*T7fw>1i%+a--%<9kpmI7SN_oOW^tYt$?xk0?VgkV(f)x6;jLzn5Kp zQ+MSyr`*k_#7u_DM>SN6R2{hrYd6_a@{ncyz4tkmNr?kco55$kr1>J9+`E#a#bbEF z#b>U#jzaU7vXwN~!?LlqPb@^!$7w;{!jhsrGvkyn`I)r%{uLj^ME};d z_N51u^U1Vxk<7M#6m)ZuQu=lt__c zgYSnu?YTs5hZBYFfc-$x7~uIG#>jV{Y(K?xl`Z#ZT*1RwTfqk5VfI(Mpd=Ovk=Wg& zcN7Lg)uLy4eGyeRqGI(@OgRz&?%l9k_+8@IldzUsN3opzpe|}}d%3>fd+*3o-;I6c z>0=5PxHD1*mRlZKT~8yrZA%xeSg~2!X!D&)o8gqtkxp;!lid?1OBW9{olJX@!XpTy;Rnfh;eN+k7Zm*F z{Iv$Ix)udXS;w9Wzn+U^`sV0-5%241K)U#{HJ`}5KfMQk{+&IVBZr!WJoSy`6hg}N}~J(vYEIx2usby@HMTXeD*3qbaW`dZT!-Y zom@QdlGC|Tzk_QNP|ieP@wg`631d3gaX{$$^$yNVp~H^Qe=>{?HM|hPZ7Hxh2*?t; z*XX|rVaAMOahtlUPTX_t3C?|Bqwo<{$BwfzO3yv80UP$ZVpM^}ZIfL<5^xbJ&?tMY zqDskSr0)z-^+WU;gi_Kyf-NR&}`0npvFV4d;V)KHnk+ep{Z06xJfMDLO8jVgVb$RURufz-CwEF#>`e|EC?L zu=^7#=HZsrZwR{wd>v@8aw?#+hAi!IskyVAR3syjIPt8r!SFxy61u4&YBM5rf zEHyXBaYV9e@^iD|8nV&=)$*=05#hS870NDSbO4=jUsUij5BFZU7Lq-AVSw6Mf>A7+N_-vZ{)0-6)GxU(5SN*r9hsU5oIBs zjjv?mcrpXj3rHo5NBtwk|DyClv4M@u4p+^GRDkcPn(Q^O)&2#Y!)OZf9PMYwd;T7iZf?16v zpN-h?NRB9$I_J~Y(t(0EK<27-l5tO^Ktr%G4GQ>mJ9$OBdLCEpEdh>)_Z5H^!kit>)s_!F6X2U z)-TxxHgbt+!ykWNKH4DD7)*N+`w8`&I;~a%MpR(dPb71-o$g$x|EuY(Wszb6RoXAB zh`nHE9xL)Ua`PQ8tBwbBN1p{TTIPOOvZ&FrP53pj$0F>)KwHjOj($3lj6bp$S3I)X zZ0Z$9_)q4LbTwuh-R5;>*%)ovCmNv;w)`a}j?uXOF`8e&{pT7{?d}gA{&_vr`XT4e zKGrlR@i92bGxgvth{=>6fJpuv7aF-ov-{)tu@Z}Vzb?<_n0?fOt;&~B|LNVB-_fOy ztrw?sOEMP~_dfKsD8HWbYAZ1m$&B;2HfNNm4LtUrUu52kNg{IB_XKc!cLDuH*P?mM zRYhq2J$lgp%=>`F>aT!W_ruEBhuw#eorG(pi!HJ}h!*fakCQbY=cN=cIqW-@2B2>~9Tm-q4nZyu6q zDxi^Ni+gL{Cxr)eO)2Vk--GvWy6JT>w2_n==%y;8I;0&60G5 z(g-kWjrFW~%@B)1Aw1{n2*XJBg20=HFb)t8mod^}k-xi|2#z~Vxe!ZmN7lpQ!Q8_guMz+U)U zso>}=ID=ue9!)7+U6z;Tb(85>(u(!7Vi;_R8U7UpBVvqUT`-GK-k6oIs(*jCE4`3Q ziI?UyDWUv+;X?mW7VCciPA|T3(OZyGyNijd$o;{1%gSojx3$X7EEe4%v@Y~wBDQac zK3IlRJ$%MiusCEL+Q%}ys@3?XJcER#``COqiUEEHa$mUO&PSgAda~rS*#4yaJ-2-! zfI;cs<&9zcQx`H}9P>hdXJGx7AD*7)DY@lpl3)J7j(5)E`DIijOMOT8?`d#}&2+oh zNp?A>*SiAY0z#f|zr8H|55cg~lTpu@YNIW%TRB)^t_9dX{T%o0b?0cC^DE5lUE$df zthQkKrXz-7hNR)=AF^%kjeT;CXXWa_-%kDP9 z@PPnR{KSXCt8mL_JT)y{j#J*VfSNE-ArMC?O=f4xllFBSb-A=XCQ zavrh^+T%MTN!Uj`IS5OGT4fOL-FFkpmd3qDPnwJcbJ%gDIDwG1HB(8rSCC*oG!??* zwE3C+8_O0xnC&Q-z1)cu8WeEwn;BVO znm~7jU;GPaM{hO?c&!)_uz?QQVd(ioo){n0pcQ^@ALcpfD}}0S{doyqk#?S+V!!hF z3Kp`gF$)&*^#~>K$;tw+!JKWbB=j8Ck_C{_e1~$PX)F3`DV>d4xfKf*7 zZB;*J#1&e@Xfb(Zl>>rp>{0FWf|)ggccBLU?07yuUY@E(FNw7c&f6uLflU&7Dd86j z^MmdJBZd$f8=!VxX`A!C`I6%7t7#lt?O*THz+w{|-@oOqIxp?DJzc^zTJyRv7qWK@ zTCzh<++5G z4hrvcGGP1VmQVon$;`&(o3PRCL{_9Pg0TvFEy?C$MR}bUD~Iyu?a*C|{q3Bqpm4dR z*&Ir^0wY>h{y>HPKC}_5lZzFYU&xWqF!b$e$%dYODzs}!oNZ`f9^JEzwoVTa@=6at zVebRy;8DAdzUuGV@YUjeY{MJBSq&&s4Oq_fCCh^z7_j`eQ=5|=*HK7Ey4l$|tHfAm zHn_xjTbfM|sM+%P>GHLWi3!cK!1>)pBipD%k4*T%kM|&w`(0!Kj?71ox=&vE?1}vji(MGm29=4XS{Rl%sw0U$jZ-&J z)_;ha6W#*2b$rhI{Ma%slq02kZ$ZbrW3i|35lx6$>8r3$O!KQW=1{@Hmukx1?t!D# z)9UQk;3C6KsYCR_hu8LWAYlvNNKZ{hP7P)L#0nCtdQCBx?gM_a3}s|+rn>BCU)2J) z>ockM0?HJRB*}hTQjsjzB&h1=u`;}QnkBzXV<`U#fd_Q1Th`+l{ZVn3m3#P+nkLMQ z4xU(ChOjy)ObMA6E?aB7UPZ??=K40X;ho}g8y3a z&q>R{&2On*8aMV`Mj2#VrqbBFATdDSl{y-~QXCP?oo1Q%!fbWd`0mvE&s<(+ZdcOF zr?)nTzri5qRMujm2L{O ziSz5BNhZ1^a)68uH>B-R-=V8W`h{G~nA+5v4OObRclYhCYCGUq=p*rYbvW!ta79h3c zacurOpDyV<_jxfS7p>L1WOwx_LWxf2k}FpH=qLBn{n9s;D*q)*XqGU2>jR5YHaeHR znPTbH`!#z^vY%xmVPd=9x<%bjvHoRG$%;5VcI?@JQ3MZzfs&d&MeX}#y%co+?(*`Y z-Yz=B_OqIj5>j^u_u1sX*ybG>KOsxJ1vTw1*?-L&k=V(1 zfhzp+ZfI>VQr)4=dQ&d9jmqm7!@=T-F)+NT(9)`6rKrs^$mYKJ{uk)FGYeQdXCp3K zPW^80{>k%OXQWeE2PLWdkqmDMW9YIy^n8{00D6eA!tRHbLE>IR*;=p9DTN5_L_1f0 zCIkb!&KqK&%sud3UnG$?v$8RMCrMJsuTB}?s5a1^us8Wu5Ak&}!{XKv#zIi3(n=4g zStIYeVhQmo=P3YR4)vvqiH^2tYc{WZM) zKX|6_8xN}Rp;Z(FktuQd?8_^NugYUg@6|IgD^`)8VuLnPb!BQ-hCr{CEnT)mABmQB zAI#n|VxppI=_5bvP9OG8g)C*t)1Qr!9!y`jpf+fOZz^|0Yi06+=S1s!-Up<<%nh`} zX7maJGI|$YiQN5DPT%zlCl7OlOsFAckS6t3%DnbAJ$2 zYEzceLTbxs8ov`msb6@203Ys2SP^@XDWd2fvuJQTGfmVf!8r6cJ_&2Zu$GX${#_d) zoJ^$ksfgQz__2+pMS;9le`GCCv`qYspE?m2I;OVB>HR5Jn1=9R+U-R{WzXf99?z}U--<&ESYThZXA^u zxQ`?rQ0U!jsWm>k(+rC*A?gb=dkJ96W(m2rIKEtc%{KcyUvlx=9l`!G8wbhThH09& zBrY%1DH)4u%W4_ zpU%7Re#j|BN!d1kntrA62AVTD-k zzaw7x_9sV%y}si*KGtYVw!s8$@}??12_3djvqE5X`u`fYCjqHJxTMj%xum=;Qa9{$ z3q8h^pQb`wweWdy8TRNc);0lyAMU5O|7~|{a4T7}p!e9(XSiG)>D{_L1R*RRafuW3 zKW=j#N>S%|Olp2gpp%XTFK%tSOXB_D9i;6>+HMM+pmPzbbqoLdB&5pkBDlk-7@sJW z5f5a0M)=MAkAnLh2CeA);1l@&<_f~Yu1;}k>a zhqB;=4itQ&?aFbY1=W!n(A1e*t%q3lLg{(=%rmh`~Kc&$Au3`sty-#|SaTDQ;TN{gS;o@+H*1R%~qxuHI$UhHcvf zLi#v&V=k1wYGp?}wG%2^x0X;1RwM|kx zirUf2^zhEL`T=5Qc4)`A1^8D~?D=Qbd(*ks>a986ffv?t! zHN2V%he2AJQbkMS&2gv}L(1ln_XebErpN84MX^}4J2ddgJsXf{DA{7|8n0&Ck$Y)-?}n8J$enMr=~Dg9gD_=?lT8iNS;foxVu%lbw9qrDHW9)mk6 z#*7AHK76J6Fkn2-lodbOH`DkaD<>IV;ARF2dqi7J6I0mvfQZ>y?!7LIbCc(INMN@kaiQhq!4HwPaRF0UlS`OmJ}bxI^$Dn*K9W@*M1lg?lg`L%R` zJU8>EqCX`YT>|BDUS`_JOuTkmY|fP#T#`|f!#TAx+Q1gpG<|I&b7g!Ez#Gx%^m=>B zfA4x1fV@b%Or4SJrjXsV)ero;#{-@&mM7UclcJvTI^T%6W=~n(r3d7UQG6{m8KE$I zvR@%e>F)WK#i{dG(!4ZNCTvT{KILU6dc&Rdqe7OO6S7Igph^t#YU=R-EBK5IDP$jx z%!F7j(sm|#xgZPMCOkGSc2zflKXDKTK%6fg=C!AAwVKxim6JNr!eq|hen}zU0e_%% zVmO|gQ+VZg@Go(H747#A`!f1qqe}y{4(|MWubxfh@PjzHGR_s=?*PvsbOaip+SsRWcmEQ$@kNfLzy9Ht^Xi zu7i4Q#am&?eD(-MH|!`!{8coLg{;rlxtXpO&Dq8=o}A-~Y+;^02HNDH$9Xd~&-mp} zLwQqJu3|bsS$-%X^e~v%*!$w9Wm=xFm@fo5-KS@7+qDt;BsEGaK4^*AzXayjDS$2j zf;U6M6`#v}t|U|GjTA1{G+CTiIUS@C=S?u6vzy00M5q|jd?Db_j?I&lAQ(37``PE+ zH@?}T0kHa@RaBesT^hDLGn-A4Myd$F9_zSE1KwC-Z>kE{^Rg@hg1iY8CpL67-blB} zMUU^GI&^?BeQE|AP&RP0ADWC{5({&bXmg%IesRy~LH94BSB)8C!&6BU#)C%_UvJ5!kRyOA#7A~8=r)$OU5!=RWay(X6+6^tknl)kx}Y`!%TeS83*yocM)M%4apXRn^JKe~1$JDUvQf_lm%y4=NLTBRoSP z)-48^hCDDT6zt1{C zDoA3(pA&@yy)C_hC5C~hC9{9CzI)LSztYPuWw@PW8UtkExGO#WT2r&ZSF~_J9(z1JqwP+Ao@ccyk))cFfyH!w^VRJe(G1wy^?EE;S6@^0Xju|!o zzDB{eDDm(R%_faSeHTfTuP(kGJ*DMm-JpQ8T0|z15sr0Zv46k?XvCj%g3GvSj1!+( zXIT?nwXQ5lm|MHnP+NN4F%z{UM?brUq!*6nunn~H#>1zmp-Q4HUg~L7xH$jmXXWDh zr%i{!n3V8uWl2qeJvOEB55}mXALtWz5si%JS3b5CSYOM_3K^i#;UjPDuFiT86(Udh zY6YGV&H@Ok@5VXkzi`%L9{MxI_dSZsnI*m<@O=KrpNrM;{%#G^xkIy}nr-Z%^i%f- zWBQ7K`Fnawk4;;HL5_6mEG?L$DEgKohw5p?m2xzA;9=7h$T7fF{A=Myrx>E5A)ru$ zK)O*}L&(<9m&%IN2TU?eT8g5tp6K@?3n!Uibe|9O$MvD9^Avh66-1sGox}qPMkr{*w({wMwE}D zrAyK)KwKt-)`^WVGtI2``qJfZLwK&0j|Rsi4S65hbon4o=!0SI{Z5kWTAvoi@x|^b zc>3cD@g%bdY=@}%dBt}!MWqBw7xg2RM*!@n_o1>k;|4(Yu4WUPlre(cY-q3jW23ia z94R+;9?WvuxDzwRR4KRBH9u!BJK;o21IUT`*H?cp`Cc;Z3h-%(VXT|*a}9Odk++7# z_+o{NMFqlWuO>#}mIab4bK0y$VE~>cUcD+FP?LP*IUSh4+J@N)o1NO59eamETRX(-m}B3;ORyq^VKilDaMs46^Fxm)qs0_r#s>L40)HAiMIa&D}PjJOv@4wH*Cr6a3 ziP>(dO9EZCze!EW*&iAxoUucs4@a%MPctKzV@Oc%mIr_JD~^d9$WxJR{E+)#pZxtw z7S48e=jOjTmmunm)aToC!c5{9Wjvo&s(gC4>8iHNA1wQ@1W7MetUuZCtehMi0kvcR zhZo$6dy-i;R2N=m8M40VrZDVorE}%7CB3S*@*5GN=zhmPwBD(XLXYeBCA) z3v5Npe%n7-jbZ7AwXia%MvzF00=ekE~<fJB%!iCCRUY4)g1_*{+Y3izQ~~MC>T*;wWW< zhE$&)?lOcXo_RBj(aS<ee`(0cc)s z0#~#z6^cUIjX+_y0Rd1{IBb-^yjVOKCKAXQPzj^1+=k7RTlym_nJ! z8%oIahP#mO8V$i~5CD|CFf0DJY!MnA?MJ?mO-ydX(tehQlGUkeH4zEUM40KV$tJD@ zy>h=0G{VJ4a44adRS&YMv}?li}`%93*_rI|ce&LRN$E8)8}Rk@Jq* z-H7YITW4XE>P?r?R#vO^f>Y6_Ot$9*T5E2@#k|c0L}`w+W%l15F?%f!cDm}=Br9&q ziR_qd>!{hgMH*_=H`ev`ryN^-hvqN845&I>mU)YLVq87?%hkLt8StjSQEpGyE&P(@ z_be&2FX?HV#PL$i9#m}G;=>aAda<`k>jb-8C{BpUByPZ zeI?HzHILNO2YmYO?spagTnZQfQCxnkkBZoUU?qTK3<);W+ai>2L)v-C$%Z^Xx#8aE zN)(r+`jMKUB(T@n>fdjctuPygW%dB?7Akip>oNhxmF3cyYv-Q%SP~mQ(D-o$wGV?$ zq~j%Qb~8HUnc4SKJ;c)|hwqE`kWvDnNhR9M*hB$5|OahXvWY?%p`IZDK{Alzuz z#1)km|1+XWG0w5s(R;hFg7`l0R}S->ITt$m^`#cMG~RX?%q3TWFA;v+$R=aoqAiRr zVsQGW8Yesi5UGXp)g4@>pRuIAOe+Rca=%JPevua}6G6EEyl%oy!CXTgeaJsoHmW_? zpL_HQ-&q?AI1ei-;H|&>#D=(itA^k-o-t*ZM5L7i4Qu^w|Ku}E<1Gmj*#xV}y=}#& zeR&FHYA*4^YCkc+}73fg)Rhjr_ zG%rLIsfvTKDX~Tu@lXh85beI^YC~N9tnJZxpvDepmY$8a!o{^}M_{tm`koHk%a>0; z13!lXzu5Ssb-YgRDO|UsUTfA&Oix%#3ZF4LvsNK3w zG0rch+c2kVtKQ8fKcC&Y6NE`3CHhr*!)1K&$(|${D(}68i(i2M2QRO};G{&7+IO52#Tv?B!&zt*+fAiM=7Ti>r`^%*}n} zqlubtGAT=deNIRmSL57FSJK-e4lSE+0yVQv9v%Oj3i21}GN3I&(=^r@pR~CnbRX#- zRZGxUG;$S1HQIL-k@v(he&k*Jjo(MUO%b=GIWN%`0{coo(b{J6x_w#i3zcjEH@anITN&#ME35gGc(^p@eS&3&rMuz1PP{pzSDc0 z7vjBg38}hvWH#D$DboGi0@_0zh zgLF$`*J}fdHUz4WEkN`6jhoXJm5{F0MH{u57z7%F^4qn7Rz}Lp*)G?+`=Td8eX*K@ zwAud_*riA8Pks&??d6<@XCiJoB-kQP{{b1I4Y=peG;7jjSpzjSaN?fCLzth2cTLvhfc5_oV7Qr}qeZ)II=FCKq z3F=vEbUvfDV*h?5sVRRNjOqEzv^3Qu?|8u+8B6_3`yKLew&t_f-`JXuD6!fLc<1r2 z>^h#DK5m5vgLeiw@$eIIV( zGj_mLW@rbV4MUD_qs{7HDyVUgXGgoS!(`;{fky?Qh*yUF97YtdX7dJ_IzqHy zuuMVg_Q0MsD^tjziwvbg`yhjMC`d1>*cvNQWGk}!6Xj)ND`jPktzWpI-Eb_#WH%DU zWJ>aGW7f+Q$%HanQzm3`+03pxGsp4$#A?~BP3V)iNJ^p)|AG_aymn#y9|Ii z+?l+vTU_f8D*i-%HaFgq_5_~U_-4U!&1SKPQ_TOa--t&xkN9~u=FX4R%~^%_vH3z1 zIH2JUGnlst=thh$@MmImf8rXheYt1&PH;_4&aas%N0bBQmkFSZgtm?Ax9SD=-%>oc_rB|9KYPLiQ-2t@->FBb+wP+YXYA+e z8;{@rm1q5)#Gzo~6NkXVFXr9#_t!6itI%fm6)1e;(@!Q9?w)Ws(E~a%+;6X+_3ZIR z!{GJUFsMck+j*?Q z9YTSHGe3I|tQdau>nA^Xoc)QG;oW0l{bzlT#=rjWE;?@Gude?3kJcJW-*Vb#PucOl zf4<>oPY>Sdz==n{@l6LGFzm5wulvapcE#Ycz_8bR@YKJUK5>WUX#V@3u6`0yT}!vU zr=I!OZ=Nz{c%wY^;va7N=JktS3D$PRR_%rCH6&Q!QN)g2GP~63=;&aC4%)F z`PEFgp0l!sQFGa)NxP~mhVD+L(HaD+C97NUc^Fo}4ka^yU(cCH1Us0qb31U|GAlU> z*Kp1bjw7-h>1Y*hNd=Aw%oB2?IY){J7Qjw7yB|Y4xNB=BQos%duBPL62)NcXi%&1( z*aO+_h`63$a=$kS?EucL!fBc_WwkA|(w-^hEMS?UKto%jR1~bdy)go75N9e3MZ7ZX z=QFr^3|E*PYuqw+j{&SfYj;Y73{uD-*d(+MGN_&GhvMu*fzArpv@My)0yYWsH#<6J zHy!G)*>)!&z}I98*^W*}W7KKc@5#wke$Ck7IFrr#lAzWT9p~>Ss_2vJ6Y&!2@5jhz zH_a>)SS+xrwKiIw2wS@})hRYzM3}k;ctYeMU>TktOg#b;nIE0o?UPj>WajNY3Tza6 z{Z$x2o^c{#b!1{yexK43I{RGYq8@TMd6RQnX}gwl_R(k`=X(@;Bg{LVeaw3-9}?sQ zV@Yct%&X+)$IvF2t6j_OK!I*769)5zvQbZN-dBa5zPdqR6F*M@cSJDfT00m|_N=XY zWmRKa_{#DnB_wamWeXqsWnwMAZuw%DayPq(uRp21FcY}!4I5wtKzbd*P~OQkTmU?TR5i5gv&oN619RgL4Wb>?XlIM%OT#r7Q( z?Lq83#ouH02!q^uE6mritV5YL*JIb>$wjMPd)7~m|Hr!<{~E&l=U#5><4tX}olfnw zI4-+(;cZ|1n_~{#=dFK#&Cllb@NT-_aNZZr(0W>Tcb$`VcTWuut(|?}mx{X~Y#23~ z2fp!p|1FgXlTY8Xarkeh`m)7iGrzt_*_9BW;AU*MuNSObI(wn=4(}qNw!#Nc;QdAQ z=YgC2_h^PsJ>epK>bnoHUwCZgCY{;~iBIQ@=Q6_-(z4N`rPyF#E+T(!zhuiu4u!Hucm;U(B(+zR*U;Oo(ci(x~ z4unf8zs{TRSAX@s*V4V$fBnkOe*Nn|TIh#vS1=RaxWAft^H;CD_FKPRI`Xxrz4MK3 zm7ripL`KHFQ(Pl71JFU9vfc>mIj>=`s?%(37py_~3MNQ;etl0w?plzoBRF#tThHN+ z&aLV+Tcd}%!PxDtEXn{Z37beH&W;LZ1*p^zZ&g@3AjgiigX4%8M}!q6mz~IMbFF_` z-BiSpW5-_yQ{Jh4)d}t3Dl(uUZzGEWN6P6&r0_)rd0b(>wtV50VdFnUz8<8phQ(Ii z6i?8hKl=MwBBpe(^5z<{RUNYCP6ayzz5pRye|x^j+r-F@FYB2RYud49nF7mbXA@w} z$^_g2fF4GbXHW&Q4u%D`BJ^qnHAHe5CLqV@PcRlrjU`TD1mj9B$rPjsmGFT;c9pDR zyyz$FILLMv?W)F!Ue&X{gE|==Bfy;z7lGalw8#|-r$#DUxg$-Wkr;a9;m36^09=S{{VOWinaGVvZV2^A++_u=7~0MwzIxp-^4Puvv0ZL ztm9sL_$B@+6-|trdg`gY@SAn(Y#G*1N?PB4!%Y7zmC4i2()z;bzP>U0zE3W~m5>9h zA>f#1)1qH{)dqXF)$oh=yPH0SO`d+iDUu6(5*tYAb6O z&idJnSDyEGXC1%K<@W`Jqe#PG6&Sth*7tsQwC2G-E}`8fA2_5%;E;V@vnMT`H)mO)pU?<2^kOa^zTF(V5IlTS`t2(F}gzGut91&W8h+QXi9PtkTM|fS! zR|KLv0-$x*&6sYa!?k9>RLHjWEmVT=FlWl@#_T}Bm4D|yMkb>hwr2fpNpNkbp~g1F zlv^=GEz^@=ZO2b&Gz_ef;>#Z<^P;s0=*g|2GORH@nPsfD1l$2&@EHbO4k5Q=WW#dv zuAhBbO9@Ubv&>?aS#14S!W8Bmk z@#2+bFX()gf7OeN#oGOZ!u~J6xQL)iZj-lJp{-lBa`2ELg9Z&sCC(%=iKE_!!*0sE z%;e10N$Q=YHdKw{<+7@AyiZy+Xi!(zkaeq8M3n~}$=M1<7n^-Gm#f;Q^*3&&A%kfP zV8dotht{yFt0O;q!{2=D&A++z1oynqVZ+99zj^k@haGkPBD+A)cI{jIL5V7wf>nj7 z`rr4Rho3#weoJM)Lq2i#I`;#yYiIuYpIp50j@&NJVdU5YCMbH=?o8}hC{Xx_qd&IL zt-!1fEzMvj1KKv;^X%7V&3pHLlZ&(Ld9r&x$m28rePJq;n&&<0(i5iI`!$x{bIH+X z|G>G!|T`tWJ{3|Vsj zuX`X*xwBc0t2mZg000mGNklDSqH^z`G3 zy)?xU=%8zyE6_9crSe>zunA|!$tHHZ9E`4(HPRks0x}g%HN2NuK&Nv^FTBgN zz=Q)1U;M;FrR+52#j_upc)(!`9{mjo`V^!zpWK)Q2-i1$Z%j^4 zX91&|Ke0yLY-baW9JxW78fe53m?z+fkRvnd)*p-`zxwyfB>teIW6FS@{97d+NRqPK?jeH&}QR114BSn<1^L!cqD<_(i#O@a4Xk+BAP zYS_=%2tw8z$zYj6_Ii3AiRXOAAn5GE$?R1U3>sS@n<~L3aW;$iCAW_9XZl7ITk)%))tm=R*I|_T9~l4j0C}CYIy|A zh8}A?!Fh#S&?*gHWOv{Mp5|E~4W8*|Mk4FU3dUu<9dcI~9mMmVizcQ&S()cj)Y(`S zywOFvi+Y#CgDio$6e7jOEKp&i%<4=|H|M2}eK#N3lPMMI2{r+Kqez4~BAA;r#JdKH ztW9`EWj2Yq^YaLsXW5?Fw&Z3P%N@a727LJzSzFt)V(rU4yG($UnV?PVI$_G1l`pO7 zS?>2&8dh}VR}rgwmaSQ}VwXL|+I@*#CQM$veCg_*m)r_jvj}pVB(;9^E9+OU8a{kP z^EC}!=%7o*Wgq=qPJcL!VD{0`90WNZRvK>AQLFpyMXu8hAF<=QRVy~GS&h|(FdtG% z=GBaLp26W|o6TFY+PH}}Y;5;=9Cgv?cKyadn~HSpR+c$gQ}?tF{@cAz%=rFSKXUS$ zr?!qvJ@%tl-~IIy;HO>J&Yy9Ert9y2%ztlX!ioQO_cty+{iq48QA|DN)bqY_%Zx|v zxvEq**wD1*=KH_%u&Y6IA2`i^0c7p$JHJlkB8~S^@NwHUzxF>$Lfvn;_=mS&_2Hu? zHr~JIQ74@H&;NbT?N=#%{r!)l!vA{L^%uSCsENfGk2&SsD{lG4!@vFt^AgarSUoJf zX4dR*6tibt{VaqP>J}W??`=0&7NIL|MQ13ZY3A$W><392BdQHc#ep3WNI7nUT*=L6-};IP~nO`=J;w^)pKO`MbWx}{H)_w zb;PU@tiRo<;#IgCyymQdk*rfIY>j0YYc6(pAX)}Vp`HpYgKf05fSk&ya%`HtqL+hV z1#A*$Q$e*i$1D^jU;vB5*U&p0o_h+x?WWj5%@lflEm^L-w}vG~jkGONDvxdR6u zVND|&A zl;)Ay#5ty-zzzg+F01uBo0uMhl}5x@inSLPJTY{Kog2GE610z9Cr<7fI(*6e$FV9G zuIM0cmpu80p*!p-*6u4b?LTDr^K&0j>`IJ+r0&;djdtbpPd8p{Y@TsS&wxjND)&+y zBxfQ8^AXRPL4vD$mX4r%E%#G9e8-(=^Tw4gJRMaVcs`^O*~DNMQvwB=H33~e`~yR z;b*^a`?23RW540{GbeuZD~*5n)_F6jR7<~FD%KFc{o?)ePB}fuUMnB|@zt7wDaOI9 z*zUjZ(?2-m8}GKt3?KXYcYo@ZcYn%j@}yRaF8{)hkGg;;XMkTbo*Yv86!c|>zJ*Ugo za}?w*W(PhnsDOYSsB9IUXp0JwvUVWXdcFuWgwyMQqi}W0*55(aCPmnC?saSjypO{v zN4)h+X>9F^f!Os@&lg`kRIKeNX3bq8_cjSa(HutBpyk@y`#5YH1f^q*$hUs(Kn5vd z(6^i_*Lo|teIsiFHa(|`g4Q!jj9F}TqV?Vg>!difQO*baQp!h1A-B=0tgPESUi-XW zPtR9=bZk@TT4O7;p|xt#+MWw=&mcB6R_(*PQ&oaU^`9}t2W9xdkeByeY9StwEE_g>+t0`{$*+$M^mTEEWo z%V$rXu|v8-n?Ou+n_y@voLkfPZXkEo8zSx$V~sMbEq!wKuKVo2&+CqUcJ_mtHmoB- z4Rj41HsOH7*Q|W$g?WFl^-8>hgMuo*ZPoLCdSu+BY5N>>^s`ThwL3@4{)eqzzV!Jy z54kHbn!@Wbeid!mS-x=I$ldnddE}UvdsZ~InfWsWGq7ukw^dO2HZ=!ja(=%+^;+)p zwoy9RdE}^dt5!5hQLQ8Ko-1h6wF|pXY2LvRWJ6r>Q=BDJty#l0#Z7w9#?7>DgId3l z)@^LHC^uWxY&XG?P4V$T8yW?U*6kbC1GJkh6x_Q(PWG#%QmlLSXP^12kKbST&5VEe z;1_4`C={{lctNhf|s6l+~_w$0cE}i*7K=zTy6!_$*6){Xj5KrGk<5tW124R?fcZA3pRW z3RZ-;)>lveao$TizV?V|JG}JxLyKVDpn!^oJpQ0_|K+3aKW13-<(ZHFam~axpFM5& zJx3pT;SFxQQUukDNlh_C7tIox2mIPRcCc$22y^8FqIng9KS0LY8=6;j&iFyzwV@nBez_|YmX|> zO-B_I^7RyF2j3QTBDcEr9O;N7;v5+biPeMbVbC+Y_Dhi+NHdO%`FSRXO$^?lKrW*i zBPE%_YFfAo=W7U7p`+{y&uQpg6IfHg6mc0@BhDJ+x56gMwZyAqSc7m^wzZ!-tmb6F zat`S1cPRmz8t>rHoJ}I1VYtQM%CHXZ<4|7qv#qnClYxv#4%la@L=g)o?hiF&YZK)X zK_II(k9$9i`mfT5>mQ7ZkKN}kowgp4uT_QC4%zXn>8M^$VO>}|PcVCI?(Mur%&f#_ zSey+h_Fl+Nyv4|6`g@tMrcWB?GO#v}Py+pfh~|-wgjgFAW_TADUQa@q%^lWOfu2MJ zdO~zll6UR�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 z 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

?x^RsX!LE-05jh@;^mkX9-G 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ഔAa|5{3mxGOi=^MZ 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 z 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 SZNi?W&{Qz$i3O#T!}F`ACsbDWtMFU2O>YMTn1; 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 + snapshots + .map(({ service }) => ({ + id: service.id, + name: service.name, + depends_on: [...service.depends_on].sort(), + })) + .sort((left, right) => left.id.localeCompare(right.id)); + +const isDemoMutation = (path: string) => + (response: { + url: () => string; + request: () => { method: () => string }; + }) => + response.url().endsWith(path) && response.request().method() === "POST"; + +test("Explore demo exposes evidence-backed root cause and reset-safe exit", async ({ page, request }) => { + test.setTimeout(60_000); + + // The Playwright server uses a persistent temporary database, so restore any + // session left by an interrupted run before taking this test's baseline. + const cleanup = await request.post("/api/v1/demo/exit"); + expect(cleanup.ok()).toBeTruthy(); + + const baselineResponse = await request.get("/api/v1/services"); + expect(baselineResponse.ok()).toBeTruthy(); + const baselineTopology = topologyOf(await baselineResponse.json()); + + await page.goto("/"); + const explore = page.getByRole("button", { name: "Explore demo", exact: true }); + await expect(explore).toBeEnabled({ timeout: 30_000 }); + + const loadResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo")); + await explore.click(); + if (baselineTopology.length > 0) { + const confirmation = page + .getByRole("alert") + .filter({ hasText: "temporarily replace this topology" }); + await expect(confirmation).toBeVisible(); + await confirmation.getByRole("button", { name: "Explore demo", exact: true }).click(); + } + const loadResponse = await loadResponsePromise; + expect(loadResponse.ok()).toBeTruthy(); + expect(await loadResponse.json()).toMatchObject({ + active: true, + synthetic: true, + scenario_name: "Northstar Platform", + service_count: 9, + total: 9, + root_cause_service_name: "PostgreSQL", + affected_count: 5, + }); + + const demoServicesResponse = await request.get("/api/v1/services"); + expect(demoServicesResponse.ok()).toBeTruthy(); + const demoServices: ServiceSnapshot[] = await demoServicesResponse.json(); + expect(demoServices).toHaveLength(9); + const postgres = demoServices.find(({ service }) => service.name === "PostgreSQL"); + expect(postgres).toBeDefined(); + expect(postgres!.latest_probe?.status).toBe("down"); + expect(postgres!.effective_status).toBe("down"); + + const [incidentsResponse, historyResponse, availabilityResponse] = + await Promise.all([ + request.get("/api/v1/incidents"), + request.get(`/api/v1/services/${postgres!.service.id}/history?limit=50`), + request.get(`/api/v1/services/${postgres!.service.id}/availability?hours=24`), + ]); + expect(incidentsResponse.ok()).toBeTruthy(); + const incident = (await incidentsResponse.json()) as { + active: boolean; + candidates: Array<{ + service_name: string; + affected_service_ids: string[]; + }>; + }; + expect(incident.active).toBe(true); + expect(incident.candidates[0]?.service_name).toBe("PostgreSQL"); + expect(incident.candidates[0]?.affected_service_ids).toHaveLength(5); + + expect(historyResponse.ok()).toBeTruthy(); + expect(await historyResponse.json()).toHaveLength(24); + expect(availabilityResponse.ok()).toBeTruthy(); + expect(await availabilityResponse.json()).toMatchObject({ + total_checks: 24, + down_checks: 2, + uptime_percent: 91.67, + }); + + const demoBanner = page.getByRole("status", { name: "Explore demo scenario" }); + await expect(demoBanner).toBeVisible(); + await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 }); + await expect(page.getByText("Demo scenario", { exact: false })).toBeVisible(); + await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Likely root cause", { exact: true })).toBeVisible(); + await expect(page.getByText("Health evidence", { exact: true })).toBeVisible(); + + const workspaces = page.getByRole("navigation", { name: "ServiceMap workspaces" }); + await workspaces.getByRole("button", { name: "Overview" }).click(); + await expect(page.getByRole("heading", { name: "9/9 with current health evidence" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Review unresolved" })).toBeDisabled(); + await page.locator(".overview-lead").getByRole("button", { name: /1 down/i }).click(); + await expect(page.getByRole("combobox", { name: "Status", exact: true })).toHaveValue("down"); + await workspaces.getByRole("button", { name: "Incidents" }).click(); + await expect( + page.getByRole("heading", { name: "Active and historical incidents" }), + ).toBeVisible(); + await page.getByLabel("Search incidents").fill("does not exist"); + await expect(page.getByText("No incidents match these filters.")).toBeVisible(); + await page.getByLabel("Search incidents").fill(""); + await workspaces.getByRole("button", { name: "Topology", exact: true }).click(); + await expect(page.getByRole("region", { name: "Topology commands" })).toBeVisible(); + await page.getByRole("region", { name: "Topology commands" }).locator("summary").filter({ hasText: "Filters" }).click(); + await expect(page.getByRole("combobox", { name: "Filter topology by status" })).toBeVisible(); + const inspector = page.getByRole("complementary"); + await expect(inspector.getByRole("heading", { name: "PostgreSQL" })).toBeVisible(); + await expect(inspector.getByText("Likely root cause", { exact: true })).toBeVisible(); + await expect(inspector.getByText("Health evidence", { exact: true })).toBeVisible(); + + const reset = page.locator("header").getByRole("button", { name: "Reset demo" }); + const resetResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/reset")); + await reset.click(); + const resetResponse = await resetResponsePromise; + expect(resetResponse.ok()).toBeTruthy(); + expect(await resetResponse.json()).toMatchObject({ + active: true, + root_cause_service_name: "PostgreSQL", + }); + + // A rendered PostgreSQL node can be left over from before Reset completed. + // Wait for the demo region to leave its busy state before attempting Exit. + await expect(demoBanner).toHaveAttribute("aria-busy", "false", { timeout: 30_000 }); + await expect(page.getByText("PostgreSQL", { exact: true }).first()).toBeVisible(); + const exit = page.getByRole("button", { name: "Exit demo", exact: true }); + await expect(exit).toBeEnabled(); + + const exitResponsePromise = page.waitForResponse(isDemoMutation("/api/v1/demo/exit")); + await exit.click(); + const exitResponse = await exitResponsePromise; + expect(exitResponse.ok()).toBeTruthy(); + expect(await exitResponse.json()).toMatchObject({ active: false, restored: true }); + + await expect(demoBanner).toBeHidden({ timeout: 30_000 }); + await expect( + page.getByText("Demo closed. Your previous topology was restored.", { exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("button", { name: "Explore demo", exact: true }), + ).toBeEnabled({ timeout: 30_000 }); + + const restoredDemoResponse = await request.get("/api/v1/demo"); + expect(restoredDemoResponse.ok()).toBeTruthy(); + expect(await restoredDemoResponse.json()).toMatchObject({ active: false }); + + const restoredServicesResponse = await request.get("/api/v1/services"); + expect(restoredServicesResponse.ok()).toBeTruthy(); + expect(topologyOf(await restoredServicesResponse.json())).toEqual(baselineTopology); +}); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..9b62355 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import tseslint from 'typescript-eslint' + +export default [ + { + ignores: ['dist/**', 'coverage/**', 'playwright-report/**', 'test-results/**'], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + globals: globals.browser, + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + ignoreRestSiblings: true, + varsIgnorePattern: '^_', + }, + ], + }, + }, + { + files: ['vite.config.ts', 'playwright.config.ts'], + languageOptions: { + globals: globals.node, + }, + }, +] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..fb43d2f --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + ServiceMap — Infrastructure Intelligence + + +

+ + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..a524271 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,23 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location /api/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /health { + proxy_pass http://backend:8000/health; + } + + location / { + try_files $uri /index.html; + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9d713a7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3929 @@ +{ + "name": "servicemap-frontend", + "version": "1.6.2", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "servicemap-frontend", + "version": "1.6.2", + "dependencies": { + "@xyflow/react": "^12.8.5", + "elkjs": "^0.12.0", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.57.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^10.11.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.12.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.70.1", + "vite": "^7.1.5", + "vitest": "^3.2.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.1.tgz", + "integrity": "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/type-utils": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.10.tgz", + "integrity": "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.1.tgz", + "integrity": "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.1.tgz", + "integrity": "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.1", + "@typescript-eslint/types": "^8.70.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.1.tgz", + "integrity": "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.1.tgz", + "integrity": "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.1.tgz", + "integrity": "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.1.tgz", + "integrity": "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.1.tgz", + "integrity": "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.1", + "@typescript-eslint/tsconfig-utils": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/visitor-keys": "8.70.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.1.tgz", + "integrity": "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.1", + "@typescript-eslint/types": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.1.tgz", + "integrity": "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.70.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xyflow/react": { + "version": "12.11.6", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.6.tgz", + "integrity": "sha512-9XsEJNHjatKYndszKTF/bsU7FOP9dJ6V/EQwzy3oMdtqgBuUq7BjKSwkEo+C7s4qHstHQfwwoHA3E8QfpPxZZQ==", + "license": "MIT", + "dependencies": { + "@xyflow/system": "0.0.82", + "classcat": "^5.0.3", + "zustand": "^4.4.0" + }, + "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", + "react": ">=17", + "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@xyflow/system": { + "version": "0.0.82", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.82.tgz", + "integrity": "sha512-4DKnL3CGtCGLRSmgDqaajRVgeksMXq/Yw4wPfdMfm7JvdIiWHGzVYOfFUztiATwdBXZqUU6HhehwUdbV9G23PQ==", + "license": "MIT", + "dependencies": { + "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-selection": "^3.0.10", + "@types/d3-transition": "^3.0.8", + "@types/d3-zoom": "^3.0.8", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/elkjs": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.12.0.tgz", + "integrity": "sha512-YZcKynxVxYoKIOEpywEPwCFdg+BTbxQRNf3pbwdDCvc8O3kQD8bmIwSxKU1eOTVc4Xo+VG9Te+575mlfvOrhEQ==", + "license": "EPL-2.0 OR GPL-3.0-or-later" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.1.tgz", + "integrity": "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.1", + "@typescript-eslint/parser": "8.70.1", + "@typescript-eslint/typescript-estree": "8.70.1", + "@typescript-eslint/utils": "8.70.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", + "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d547042 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "servicemap-frontend", + "private": true, + "version": "1.6.2", + "type": "module", + "scripts": { + "dev": "vite", + "lint": "eslint . --max-warnings=0", + "build": "tsc --noEmit && vite build", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:e2e": "playwright test" + }, + "dependencies": { + "@xyflow/react": "^12.8.5", + "elkjs": "^0.12.0", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@playwright/test": "^1.57.0", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.2", + "eslint": "^10.11.0", + "eslint-plugin-react-hooks": "^7.1.1", + "globals": "^17.12.0", + "typescript": "^5.9.2", + "typescript-eslint": "^8.70.1", + "vite": "^7.1.5", + "vitest": "^3.2.4" + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..eaa2e3d --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,27 @@ +import { defineConfig, devices } from "@playwright/test"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +export default defineConfig({ + testDir: "./e2e", + timeout: 30_000, + fullyParallel: false, + reporter: "list", + use: { + baseURL: "http://127.0.0.1:4173", + trace: "retain-on-failure", + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: { + command: "python -m uvicorn app.main:app --app-dir ../backend --host 127.0.0.1 --port 4173", + url: "http://127.0.0.1:4173/health", + reuseExistingServer: !process.env.CI, + env: { + ...process.env, + SERVICEMAP_DB: join(tmpdir(), "servicemap-playwright-v160.db"), + SERVICEMAP_STATIC_DIR: "dist", + SERVICEMAP_AUTH_MODE: "disabled", + SERVICEMAP_PROBE_INTERVAL: "0", + }, + }, +}); diff --git a/frontend/public/brand/servicemap-dark.svg b/frontend/public/brand/servicemap-dark.svg new file mode 100644 index 0000000..a45add6 --- /dev/null +++ b/frontend/public/brand/servicemap-dark.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/brand/servicemap-mark.svg b/frontend/public/brand/servicemap-mark.svg new file mode 100644 index 0000000..9fc7b5e --- /dev/null +++ b/frontend/public/brand/servicemap-mark.svg @@ -0,0 +1 @@ +ServiceMap diff --git a/frontend/public/brand/servicemap-mono.svg b/frontend/public/brand/servicemap-mono.svg new file mode 100644 index 0000000..b2f4a18 --- /dev/null +++ b/frontend/public/brand/servicemap-mono.svg @@ -0,0 +1 @@ + diff --git a/frontend/public/brand/servicemap-wordmark.svg b/frontend/public/brand/servicemap-wordmark.svg new file mode 100644 index 0000000..333b1ec --- /dev/null +++ b/frontend/public/brand/servicemap-wordmark.svg @@ -0,0 +1 @@ +ServiceMapServiceMap diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/public/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b68246fc627a865f4d9027a7a9d463cfe6036d65 GIT binary patch literal 4051 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 J*1#Xd{1;BQh1mcA literal 0 HcmV?d00001 diff --git a/frontend/public/icons/favicon-16.png b/frontend/public/icons/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..5ba357b4322a38ab989e50e6cb33f833841bdcc2 GIT binary patch literal 447 zcmV;w0YLtVP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0aQsuK~y+TV_+9o z`cDaFKo=kt<28U=R_(t*Y~X)>6>SVTf(D3aTmKK9H1&Voqc#8Imo5PF(bZrWAf%=L zKXPH`|Ii6b|EDfF_uo3L{=aQ;>VNaZGB7`4(t5DGl$ix~12h6Xz&56D-1MJMNe{&) zZW)dL(Q}u9<;}CAup0ovCHwCE*ALG5pE&=-|C-4Q|DU>c?|=QIh5zGb9r|yXR0ozv zR*zvo(T?X}TGi6y|DBf~{~LJ4{XcR2(SP^oY!rEd2Jp#g{(tcL)BnuczW*2RKKrj? zY)6^_AZ+Oo{D1q+_x~OJBeBY3H6U;E!~b$tepod~7&wCE@frZq96fdSfA_NK;Ftgz z0Ez*J+-`6{D%l6)Hvoo>B8&dFuiNwg<+o pipG#5$^a17GcmvrBgp_l7yxqana9%)HOl}1002ovPDHLkV1h$)&mI5( literal 0 HcmV?d00001 diff --git a/frontend/public/icons/favicon-32.png b/frontend/public/icons/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..c43a47c61979a8886e89ef0960ac99355fbc3e97 GIT binary patch literal 803 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 literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3b7d98becf5891fac566cb930b33a5f9ec542474 GIT binary patch literal 12350 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 & { + 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([]); + const [loading, setLoading] = useState(true); + const [selectedId, setSelectedId] = useState(null); + const [busyId, setBusyId] = useState(null); + const [busyAll, setBusyAll] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(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("topology"); + const [form, setForm] = useState(emptyForm); + const [editingId, setEditingId] = useState(null); + const [formOpen, setFormOpen] = useState(false); + const [history, setHistory] = useState([]); + const [historyServiceId, setHistoryServiceId] = useState(null); + const [availability, setAvailability] = useState< + Record + >({ 24: null, 168: null, 720: null }); + const [incident, setIncident] = useState(null); + const [overview, setOverview] = useState< + import("./api").OverviewSummary | null + >(null); + const [semanticTopology, setSemanticTopology] = + useState(null); + const [events, setEvents] = useState([]); + const [demo, setDemo] = useState(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( + [], + ); + const [dockerBusy, setDockerBusy] = useState(false); + const [appopsUrl, setAppopsUrl] = useState(""); + const [discoveryRun, setDiscoveryRun] = useState(null); + const [discoveryStatus, setDiscoveryStatus] = + useState(null); + const [discoveryItems, setDiscoveryItems] = useState( + [], + ); + const [discoverySuggestions, setDiscoverySuggestions] = useState< + TopologySuggestion[] + >([]); + const [discoveryBusy, setDiscoveryBusy] = useState(false); + const [probeRepair, setProbeRepair] = useState( + null, + ); + const [editMode, setEditMode] = useState(false); + const [showMiniMap, setShowMiniMap] = useState(false); + const [edgeEvidence, setEdgeEvidence] = useState([]); + const [providerStatuses, setProviderStatuses] = useState([]); + const [topologySnapshots, setTopologySnapshots] = useState([]); + const [topologyDiff, setTopologyDiff] = useState(null); + const [selectedSnapshotId, setSelectedSnapshotId] = useState(null); + const [snapshotCheckedAt, setSnapshotCheckedAt] = useState(0); + const [selectedEdgeId, setSelectedEdgeId] = useState(null); + const [search, setSearch] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [monitoringFilter, setMonitoringFilter] = + useState("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("landscape"); + const [applicationFilter, setApplicationFilter] = useState("all"); + const [componentScope, setComponentScope] = useState<{ + label: string; + memberIds: string[]; + } | null>(null); + const [topologyLocation, setTopologyLocation] = useState( + topologyRoot, + ); + // Compatibility adapter for legacy non-semantic controls. Navigation state + // itself remains the single TopologyLocation value above. + const semanticScope = topologyLocation; + const setSemanticScope = ( + update: + | Pick + | ((scope: TopologyLocation) => Pick), + ) => { + 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(null); + const [selectedIncident, setSelectedIncident] = + useState(null); + const [incidentDetail, setIncidentDetail] = useState( + null, + ); + const flowInstance = useRef | null>( + null, + ); + const sidePanelRef = useRef(null); + const demoFocusApplied = useRef(false); + const [layoutPositions, setLayoutPositions] = useState< + Record + >({}); + const [layoutRoutes, setLayoutRoutes] = useState< + Record + >({}); + + 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(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(); + 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( + () => + 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( + () => + 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, + ), + [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>((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 => ({ + 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 ( +
+
+
+ Relationship inspector +

+ {source?.name ?? "Unknown"} →{" "} + {target?.name ?? selectedEdge.target_ref ?? "Unknown"} +

+
+ + {selectedEdge.active ? "active" : "inactive"} + +
+
+
+
Direction
+
+ {source?.name ?? selectedEdge.source_service_id} depends on{" "} + {target?.name ?? selectedEdge.target_ref ?? "unknown"} +
+
+
+
Confidence
+
{selectedEdge.confidence}
+
+
+
Evidence
+
{selectedEdge.evidence_type}
+
+
+
Locked
+
{selectedEdge.locked ? "Yes" : "No"}
+
+
+
First seen
+
{formatDate(selectedEdge.first_seen_at)}
+
+
+
Last seen
+
{formatDate(selectedEdge.last_seen_at)}
+
+
+

{selectedEdge.explanation}

+
+ + + + +
+
+ ); + }; + + const renderInspector = () => ( +
+
+
+ Inspector +

{selected?.service.name ?? "Select a service"}

+
+ {selected && ( + + {" "} + {statusLabel(effectiveStatus(selected))} + + )} +
+ {!selected ? ( +

+ Select a node to inspect health, history, dependencies and incident + context. +

+ ) : ( + <> +
+ Why {statusLabel(effectiveStatus(selected))} now? + {selected.health_resolution.explanation || "No current health explanation is available."} + + {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)}` + : ""} + +
+ {panel === "topology" && !nodes.some((node) => node.id === selected.service.id) && ( +

The selected service is outside this graph level. Open its application or use Services to inspect the component in context.

+ )} + {selectedIsRootCause && ( +
+ Likely root cause + + {demo?.affected_count ?? + selectedIncident?.affected_service_ids.length ?? + 0}{" "} + downstream services are affected. + + + {( + demo?.affected_service_names ?? + selectedIncident?.affected_service_names ?? + [] + ).join(" · ")} + +
+ )} + {!selectedIsRootCause && impactIds.has(selected.service.id) && ( +
+ Dependency impact + + This service is affected through the current root cause. + +
+ )} + {!hasCurrentNetworkProbe(selected) && ( +
+ {monitoringLabel(selected.service.monitoring_state)} + + {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."} + + {selected.service.probe_reachability === "internal_only" && ( + + Internal Docker endpoint evidence is retained for discovery and is never used as an active probe. + + )} +
+ )} +
+
+
Effective health
+
+ {statusIcon(effectiveStatus(selected))}{" "} + {statusLabel(effectiveStatus(selected))} +
+
+
+
Direct health
+
+ {statusIcon(selected.direct_status)}{" "} + {statusLabel(selected.direct_status)} +
+
+
+
Monitoring
+
+ {selected.service.monitoring_state.replace("_", " ")} ·{" "} + {selected.service.probe_source.replace("_", " ")} +
+
+
+
Reachability
+
+ {selected.service.probe_reachability.replace("_", " ")} ·{" "} + {selected.service.probe_confidence} +
+
+
+
Runtime
+
+ {selected.service.runtime_state ?? "—"} + {selected.service.runtime_health + ? ` · ${selected.service.monitoring_state === "appops_observation" ? "AppOps" : "Docker"} ${selected.service.runtime_health}` + : ""} +
+
+
+
Application
+
+ {selected.service.application ?? selected.service.group ?? "—"} +
+
+
+
Target
+
+ {selected.service.url ?? + `${selected.service.host ?? "—"}${selected.service.port ? `:${selected.service.port}` : ""}`} +
+
+
+
Probe
+
+ {selected.service.kind.toUpperCase()} ·{" "} + {hasCurrentNetworkProbe(selected) && + selected.latest_probe?.latency_ms != null + ? `${selected.latest_probe.latency_ms} ms` + : selected.latest_probe + ? "Historical probe data" + : "—"} +
+
+
+
{hasCurrentNetworkProbe(selected) ? "Last probe" : "Probe history"}
+
+ {selected.latest_probe + ? `${hasCurrentNetworkProbe(selected) ? "Current" : "Historical"} · ${formatDate(selected.latest_probe.checked_at)}` + : "—"} +
+
+
+
Next scheduled
+
{hasCurrentNetworkProbe(selected) ? formatDate(selected.next_probe_at) : "No active probe"}
+
+
+
Availability
+
+ {hasCurrentNetworkProbe(selected) && + hasCurrentHistory && + availability[24]?.uptime_percent != null + ? `${availability[24]?.uptime_percent}% (24h)` + : selected.latest_probe + ? "Historical probe data" + : "—"} +
+
+
+
7d / 30d
+
+ {hasCurrentNetworkProbe(selected) && hasCurrentHistory + ? `${availability[168]?.uptime_percent ?? "—"}% / ${availability[720]?.uptime_percent ?? "—"}%` + : "Not current"} +
+
+
+
Failures / recovery
+
+ {selected.service.failure_streak} /{" "} + {selected.service.recovery_streak} +
+
+
+
Dependencies
+
{selected.service.depends_on.length}
+
+
+
Direct dependents
+
{directDependents.get(selected.service.id)?.length ?? 0}
+
+
+
Group / env
+
+ {selected.service.group ?? "—"} /{" "} + {selected.service.environment ?? "—"} +
+
+
+
Category / lifecycle
+
+ {selected.service.category.replace("_", " ")} /{" "} + {selected.service.lifecycle} +
+
+
+
Discovery source
+
+ {selected.service.source_type + ? `${selected.service.source_type} · ${selected.service.source_ref ?? "—"}` + : "Manual"} +
+
+
+
Last seen
+
{formatDate(selected.service.last_seen_at)}
+
+
+ {hasCurrentNetworkProbe(selected) && selected.latest_probe &&

{selected.latest_probe.detail}

} +
+ Health evidence + + {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("_", " ")}`} + + {selected.health_resolution.explanation} + {selected.health_evidence.slice(0, 8).map((item) => ( + + {item.scope} · {item.provider} · {statusLabel(item.status)} · {item.freshness} · {(item.confidence * 100).toFixed(0)}% · {formatDate(item.observed_at)} + + ))} +
+ {!!selected.sources.length && ( +
+ Sources + {selected.sources.map((source) => ( + + {source.source_type}: {source.source_name ?? source.source_id}{" "} + · {source.lifecycle} + + ))} +
+ )} + {!!selected.evidence.length && ( +
+ Relationship evidence + {selected.evidence + .filter((item) => item.active) + .map((item) => ( + + {item.confidence} · {item.explanation} + + ))} +
+ )} + {hasCurrentHistory && !!history.length && ( +
+ {[...history].reverse().map((item) => ( + + ))} +
+ )} +
+ + {!hasCurrentNetworkProbe(selected) && ( + + )} + + + +
+ + )} +
+ ); + + const renderServiceForm = () => ( +
setFormOpen((event.target as HTMLDetailsElement).open)} + > + + + {editingId ? "Edit service" : "Add service"} + + +
void submit(event)}> + +
+ + +
+ {form.kind === "http" ? ( + + ) : ( +
+ + {form.kind !== "dns" && ( + + )} +
+ )} +
+ + +
+
+ Topology identity +
+ + +
+
+ +

+ Set a value to lock the operator override; leave it blank/unknown + for deterministic discovery metadata. +

+
+
+
+ + +
+ + +
+ + +
+
+ + +
+
+ Dependencies + {services + .filter((item) => item.service.id !== editingId) + .map((item) => ( + + ))} + {!services.length && ( + No services yet. + )} +
+
+ + {editingId && ( + + )} +
+
+
+ ); + + const renderOverview = () => ( +
+
+ Service intelligence +

+ {demo?.active + ? "Northstar incident at a glance" + : "Operational overview"} +

+

+ {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."} +

+
+ + + + +
+ {(overview?.total_services ?? services.length) > services.length && ( +

{(overview?.total_services ?? services.length) - services.length} hidden services are excluded from this operational view.

+ )} + {!!providerStatuses.length && ( +
+ {providerStatuses.map((provider) => ( + + {provider.provider.replace("_", " ")}: {provider.status} + + ))} +
+ )} +
+
+ Incident posture +

+ {incident?.active + ? `${incident.candidates.length} likely root cause${incident.candidates.length === 1 ? "" : "s"}` + : "No active incident"} +

+ {incident?.candidates.length ? ( +
+ {incident.candidates.map((candidate) => ( + + ))} +
+ ) : ( +

+ Healthy dependency graph. Events will appear here when state + changes. +

+ )} +
+
+
+
+ Monitoring coverage +

+ {services.length + ? `${resolvedComponentCount}/${services.length} with current health evidence` + : "No services yet"} +

+
+ +
+
+ {( + [ + "active_probe", + "docker_health", + "appops_observation", + "observed", + "unmonitored", + ] as MonitoringState[] + ).map((state) => ( +
+ {coverageCounts[state] ?? 0} + {monitoringLabel(state)} +
+ ))} +
+

+ {unresolvedComponentCount} services still have unresolved health. Runtime observation alone confirms presence, not health. Active probes require a verified endpoint. +

+
+
+ Infrastructure discovery +

+ {discoverySuggestions.length + ? `${discoverySuggestions.length} open findings across runs` + : "Discovery is up to date"} +

+

+ {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."} +

+
+ + {!demo?.active && ( + + )} +
+
+
+
+ Recent events + +
+
+ {(overview?.recent_events ?? events.slice(0, 8)).map((item) => ( +
+ +
+ {item.message} + {formatDate(item.created_at)} +
+
+ ))} + {!(overview?.recent_events.length || events.length) && ( +
No events yet.
+ )} +
+
+
+ ); + + const renderIncidents = () => ( +
+
+
+
+ Persistent incident journal +

+ {incident?.active + ? "Active and historical incidents" + : "Incident history"} +

+
+ + {incident?.incidents.length ?? 0} records + +
+
+ + +
+

Showing {visibleIncidents.length} of {incident?.incidents.length ?? 0} records.

+
+ {visibleIncidents.map((item) => ( + + ))} + {!visibleIncidents.length && ( +
No incidents match these filters.
+ )} +
+
+
+ {incidentDetail ? ( + <> + Incident timeline +

{incidentDetail.incident.title}

+
+ + {incidentDetail.incident.status} + + {incidentDetail.incident.status === "open" && ( + + )} + +
+

+ {incidentDetail.incident.root_cause_reason} +

+
+ {incidentDetail.timeline.map((item) => ( +
+ +
+ {item.message} + {formatDate(item.created_at)} +
+
+ ))} +
+ + ) : ( +

+ Select an incident to inspect its root cause, acknowledgement state, + timeline and blast radius. +

+ )} +
+
+ ); + + const renderServicesTable = () => ( +
+
+
+ + + + + + + +
+
+
+ Service + Application + Runtime + Monitoring + Health + Latency + 24h +
+ {filteredServices.map((item) => ( + + ))} + {!filteredServices.length && ( +
+ No services match the current filters. +
+ )} +
+
+ {(selected || formOpen) && ( + + )} +
+ ); + + const renderDiscovery = () => { + const diff = discoveryRun?.diff; + const currentRun = discoveryStatus?.current; + return ( +
+
+ Production infrastructure +

Discover the real environment

+

+ 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. +

+
+ + +
+
+ + + +
+ {currentRun && ( +

+ Discovery is currently {currentRun.status}. +

+ )} + {discoveryRun && ( +
+
+ {discoveryRun.status} + last run +
+
+ {diff?.new ?? 0} + new +
+
+ {diff?.changed ?? 0} + changed +
+
+ {diff?.missing ?? 0} + missing +
+
+ {diff?.new_suggestions ?? 0} + new suggestions +
+
+ )} +
+ Canonical topology is deliberate. + + Importing high-confidence services and relationships is explicit. + Low-confidence network hints remain review-only and never affect + health propagation. + +
+
+ Monitoring coverage repair + + Published endpoints are preflighted before activation. Container-only + Docker DNS is discovery evidence only; AppOps, Docker health and + runtime observations retain their own provenance. + +
+ + +
+ {probeRepair && ( + + {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 + + )} + {!!probeRepair?.changes.length && ( +
+ {probeRepair.changes.slice(0, 12).map((change) => ( +
+ {change.service_name} + + {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}` + : ""} + + {change.reason} +
+ ))} +
+ )} +
+
+ + + {discoverySuggestions.length} open suggestions ·{" "} + {discoveryItems.length} observations + +
+ {reconcileConfirm && ( +
+ Import discovery findings? +

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.

+
+
+ )} +
+
+
+
+ Evidence review +

Open discovery suggestions

+
+ + {discoverySuggestions.length} open + +
+

+ Accept individual findings only after checking their evidence. + Rejected findings are retained and will not reappear on every scan. +

+
+ + + +
+

Showing {Math.min(suggestionLimit, visibleSuggestions.length)} of {visibleSuggestions.length} matching findings ({discoverySuggestions.length} open).

+
+ {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 ( +
+
+
+ + {suggestion.suggestion_type} + + + {source?.name ?? + suggestion.proposed_name ?? + suggestion.source_ref ?? + "Discovered item"} + {suggestion.suggestion_type === "relation" && ( + <> + {" "} + {" "} + {target?.name ?? suggestion.target_ref ?? "target"} + + )} + +
+ + {suggestion.confidence} + +
+ {suggestion.suggestion_type === "service" && ( + + {suggestion.proposed_category.replace("_", " ")} ·{" "} + {suggestion.proposed_url ?? + `${suggestion.proposed_host ?? "no endpoint"}${suggestion.proposed_port ? `:${suggestion.proposed_port}` : ""}`} + + )} + {suggestion.suggestion_type === "relation" && ( + + {suggestion.relation_type ?? "relationship"} · canonical + health edge only after acceptance + + )} +
+ Evidence + {suggestion.evidence.map((item) => ( + {item} + ))} +
+
+ + + {suggestion.suggestion_type === "service" && ( + + )} +
+
+ ); + })} + {!visibleSuggestions.length && ( +
+ No findings match these filters. +
+ )} +
+ {visibleSuggestions.length > suggestionLimit && } +
+
+
+
+ Observed infrastructure +

Latest source observations

+
+ + {dockerCandidates.length} Docker candidates + +
+
+ {discoveryItems.slice(0, 24).map((item) => ( +
+ {item.source_name} + {item.change_type} + + {item.lifecycle} · {formatDate(item.observed_at)} + +
+ ))} + {!discoveryItems.length && ( +
+ No completed discovery run yet. +
+ )} +
+
+ + Show candidate inventory ({dockerCandidates.length}) + +
+ {dockerCandidates.slice(0, 80).map((candidate) => ( +
+
+ {candidate.name} + + {candidate.category.replace("_", " ")} · {candidate.state} + {candidate.health ? ` · ${candidate.health}` : ""} + +
+ + {candidate.image} · {candidate.import_state} + + + {candidate.stable_source_ref ?? "no stable identity"} + {candidate.compose_project + ? ` · compose ${candidate.compose_project}` + : ""} + + {candidate.proxy_route_evidence.map((item) => ( + {item} + ))} + {candidate.probe_candidates.map((probe) => ( + + {probe.source.replace("_", " ")} · {probe.reachability} + {probe.url ?? + (probe.host + ? ` · ${probe.host}${probe.port ? `:${probe.port}` : ""}` + : "")} + {probe.preflight_detail ? ` · ${probe.preflight_detail}` : ""} + + ))} +
+ ))} +
+
+
+
+ ); + }; + + 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 | 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 ( +
+ Change intelligence +

Topology evidence over time

+

+ Snapshots are compact canonical topology captures. They compare service identity, lifecycle, semantic metadata and dependencies without changing your topology. +

+ {selectedSnapshot && ( + + )} + {selectedSnapshot && snapshotCheckedAt - new Date(selectedSnapshot.created_at).getTime() > 24 * 60 * 60 * 1000 && ( +

This topology snapshot is more than 24 hours old. Snapshots update after discovery reconciliation; current health evidence is shown in Overview and Services.

+ )} + {selectedSnapshot ? ( +
+
Selected topology snapshot
{formatDate(selectedSnapshot.created_at)}
+
Compared with
{formatDate(previousSnapshot?.created_at)}
+
Services / relationships
{selectedSnapshot.service_count} / {selectedSnapshot.relationship_count}
+
Added
{topologyDiff?.summary.added ?? 0}
+
Changed
{topologyDiff?.summary.changed ?? 0}
+
Removed
{topologyDiff?.summary.removed ?? 0}
+
+ ) :

A topology snapshot will be captured after the next reconciliation.

} + {changes.length ? ( + <> +

Showing {Math.min(changeLimit, changes.length)} of {changes.length} changes. Expand an item to inspect the changed fields.

+
+ {changes.slice(0, changeLimit).map((change) => { + const fields = changedFields(change.before, change.after); + return
+ {change.name}{change.change_type} {change.entity_type} · {fields.length ? `${fields.length} fields changed` : "record added or removed"} + {fields.length > 0 &&
{fields.map((item) =>
{item.field.replaceAll("_", " ")}{item.before} → {item.after}
)}
} + {change.after && } +
; + })} +
+ {changes.length > changeLimit && } + + ) : ( +

No topology difference is recorded between the two latest snapshots.

+ )} +
+ ); + }; + + const renderConfig = () => ( +
+ Configuration v2 +

Import / export

+

+ Imports validate the complete graph atomically before committing. + Existing v1 documents remain importable. +

+
+ + +
+