diff --git a/.gitignore b/.gitignore index c7fb6a5..eeab753 100644 --- a/.gitignore +++ b/.gitignore @@ -135,4 +135,8 @@ data/ /bin/ /loadsim -/otelcontext \ No newline at end of file +/otelcontext +# 1b agent plan (scratch) +ui/PLAN-1b.md +# preview-only Vite config (not a shipped artifact) +ui/vite.preview.config.ts diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..1df6679 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,15 @@ +# Gitleaks configuration. +# +# Extends gitleaks' built-in ruleset (so real secrets are still caught) but +# excludes the embedded UI build artifacts under internal/ui/dist/. Those are +# minified, gitignored bundle files — committed only in pre-"source-only-main" +# history (#99) — whose hashed/minified contents trip the generic-api-key +# heuristic. They are build output, never source, and never carry real secrets. +[extend] +useDefault = true + +[allowlist] +description = "Embedded UI build artifacts are build output, not secret-bearing source." +paths = [ + '''internal/ui/dist/.*''', +] diff --git a/CHANGELOG.md b/CHANGELOG.md index 99fe110..bd8f943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,16 @@ last published pre-release tag (`v0.3.0-beta.1`). ## [Unreleased] -_Nothing yet._ +### Changed — map-centric "Service Map" UI redesign -## [v0.3.0-beta.1] — 2026-06-12 +- The home is now a single live **Service Map**: a sunflower/phyllotaxis radial + layout (most-critical at center) that scales 7→120+ services, with system + vitals in the header, a recent-anomaly strip (15m/30m/1h counts + service + chips), and the Inspector docked on selection. +- Logs and Traces are no longer human screens — they are served by the MCP + `search_logs`/`trace_graph` tools; the nav collapses to Service Map + ⌘K. +## [v0.3.0-beta.1] — 2026-06-12 ### Fixed — production OOM restarts (memory-survival series) - **AnomalyStore memory blowup** (merged from `fix/sqlite-survival-hardening`): stable diff --git a/ui/package-lock.json b/ui/package-lock.json index adb1289..47bded9 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -37,7 +37,7 @@ "jsdom": "^29.1.0", "typescript": "~5.7.2", "typescript-eslint": "^8.59.1", - "vite": "^8.0.10", + "vite": "^8.0.16", "vitest": "^4.1.5" } }, @@ -100,13 +100,13 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "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.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -115,9 +115,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "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": { @@ -125,21 +125,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "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.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@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", @@ -156,14 +156,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -173,14 +173,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "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.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@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" @@ -190,9 +190,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "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": { @@ -200,29 +200,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "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.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "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.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -252,9 +252,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "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": { @@ -262,14 +262,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "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.28.6", - "@babel/types": "^7.29.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -302,33 +302,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "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.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@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.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -833,14 +833,14 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.2" }, "funding": { "type": "github", @@ -852,9 +852,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -1517,9 +1517,9 @@ "license": "MIT" }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1534,9 +1534,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1551,9 +1551,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1568,9 +1568,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", - "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -1585,9 +1585,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", - "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1602,13 +1602,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1619,13 +1622,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1636,13 +1642,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1653,13 +1662,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1670,13 +1682,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", - "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1687,13 +1702,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", - "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1704,9 +1722,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", - "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -1721,9 +1739,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", - "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ "wasm32" ], @@ -1740,9 +1758,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ "arm64" ], @@ -1757,9 +1775,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", - "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1931,9 +1949,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, @@ -2544,9 +2562,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", - "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2580,9 +2598,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -2600,11 +2618,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2614,9 +2632,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001780", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", - "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", "dev": true, "funding": [ { @@ -2791,9 +2809,9 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.313", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", - "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "version": "1.5.375", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.375.tgz", + "integrity": "sha512-ZWP5eB4BVPW/ZYo9252hQZHZ5XavtsTgpbhcmMmRwymavC5AsLWQWBPaKMeNd2LW0KGby5HPXvj7+sr4ta5j/Q==", "dev": true, "license": "ISC" }, @@ -3879,9 +3897,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -3905,11 +3923,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/obug": { "version": "2.1.1", @@ -4033,9 +4054,9 @@ } }, "node_modules/postcss": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", - "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4053,7 +4074,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4243,14 +4264,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", - "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.127.0", - "@rolldown/pluginutils": "1.0.0-rc.17" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -4259,27 +4280,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", - "@rolldown/binding-darwin-x64": "1.0.0-rc.17", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.17", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", - "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -4417,9 +4438,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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": { @@ -4670,17 +4691,17 @@ } }, "node_modules/vite": { - "version": "8.0.10", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", - "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.10", - "rolldown": "1.0.0-rc.17", - "tinyglobby": "^0.2.16" + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -4696,7 +4717,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", + "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/ui/package.json b/ui/package.json index e634848..a6270b8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -26,7 +26,8 @@ "wouter": "^3.10.0" }, "overrides": { - "brace-expansion": "5.0.6" + "brace-expansion": "5.0.6", + "@babel/core": "^7.29.6" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -45,7 +46,7 @@ "jsdom": "^29.1.0", "typescript": "~5.7.2", "typescript-eslint": "^8.59.1", - "vite": "^8.0.10", + "vite": "^8.0.16", "vitest": "^4.1.5" } } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 06bbc7c..a85d52b 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, useCallback, useState } from 'react' -import { Redirect, Route, Switch } from 'wouter' +import { Redirect, Route, Switch, useSearch } from 'wouter' import RouteFallback from './components/common/RouteFallback' import Shell from './components/shell/Shell' import TrailBar from './components/trail/TrailBar' @@ -11,10 +11,7 @@ import styles from './App.module.css' // All routes are code-split; the Inspector is split too and only fetched // once a ?service= drill-down actually happens. The ⌘K palette + shortcut // sheet chunk loads on the first open request — zero cost until then. -const TriageView = lazy(() => import('./components/triage/TriageView')) -const FlowMapView = lazy(() => import('./components/map/FlowMapView')) -const TracesView = lazy(() => import('./components/traces/TracesView')) -const LogsView = lazy(() => import('./components/logs/LogsView')) +const ConstellationHome = lazy(() => import('./components/triage/ConstellationHome')) const ServiceInspector = lazy(() => import('./components/inspector/ServiceInspector')) const PaletteHost = lazy(() => import('./components/palette/PaletteHost')) @@ -23,6 +20,17 @@ interface AppProps { onToggleTheme: () => void } +/** + * /map folds into the home canvas (the map IS home). wouter's + * drops the query string, so rebuild the target from useSearch() — this keeps + * deep links like /map?service=x and /map?impact=x alive across the fold. + */ +function MapRedirect() { + const search = useSearch() + const to = search ? `/?${search}` : '/' + return +} + export default function App({ theme, onToggleTheme }: Readonly) { const { service, trail, popToFrame, popOne } = useInvestigation() @@ -56,12 +64,13 @@ export default function App({ theme, onToggleTheme }: Readonly) {
}> - - - - - {/* Unknown paths (incl. the retired /dashboard and /mcp) - land on the Triage home. */} + + {/* The map is the home canvas now — fold /map into / while + preserving ?service= / ?impact= deep links. */} + + {/* Unknown paths (incl. the retired /dashboard, /mcp, + /traces and /logs) land on the Constellation home — + logs and traces are MCP-tool surfaces for agents now. */} diff --git a/ui/src/__tests__/App.test.tsx b/ui/src/__tests__/App.test.tsx index 33da1a0..c32b094 100644 --- a/ui/src/__tests__/App.test.tsx +++ b/ui/src/__tests__/App.test.tsx @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { Router } from 'wouter' import { memoryLocation } from 'wouter/memory-location' @@ -72,11 +73,12 @@ function renderApp(path: string) { } describe('App routing', () => { - it('mounts the Triage home at /', async () => { + it('mounts the Constellation home at /', async () => { renderApp('/') - // Empty graph → the feed's connect empty state proves the mount. + // Empty graph → the home's anomaly tape + connect empty state prove the + // ConstellationHome lazy chunk mounted. expect( - await screen.findByRole('region', { name: /service triage feed/i }), + await screen.findByRole('region', { name: /recent anomalies/i }), ).toBeInTheDocument() }) @@ -85,35 +87,77 @@ describe('App routing', () => { await waitFor(() => expect(memory.history).toContain('/')) }) - it('redirects the retired /dashboard route to the Triage home', async () => { + it('redirects the retired /dashboard route to the home', async () => { const memory = renderApp('/dashboard') await waitFor(() => expect(memory.history).toContain('/')) }) - it('redirects the retired /mcp route to the Triage home', async () => { + it('redirects the retired /mcp route to the home', async () => { const memory = renderApp('/mcp') await waitFor(() => expect(memory.history).toContain('/')) }) - it('mounts the FlowMapView at /map', async () => { - renderApp('/map') - // Lazy chunk + empty graph → the view's empty state proves the mount. + it('redirects the removed /traces screen to the home', async () => { + const memory = renderApp('/traces') + await waitFor(() => expect(memory.history).toContain('/')) + }) + + it('redirects the removed /logs screen to the home', async () => { + const memory = renderApp('/logs') + await waitFor(() => expect(memory.history).toContain('/')) + }) + + it('folds /map into the home canvas', async () => { + const memory = renderApp('/map') + await waitFor(() => expect(memory.history.at(-1)).toBe('/')) + }) + + it('preserves ?service= when folding /map into the home', async () => { + const memory = renderApp('/map?service=ghost') + // The redirect rebuilds the query so the deep link survives the fold. + await waitFor(() => expect(memory.history.at(-1)).toContain('service=ghost')) + // …and the Inspector docks for that service. expect( - await screen.findByText(/no services discovered yet/i), + await screen.findByRole('button', { name: /close inspector/i }), ).toBeInTheDocument() }) + it('preserves ?impact= when folding /map into the home', async () => { + const memory = renderApp('/map?impact=ghost') + await waitFor(() => expect(memory.history.at(-1)).toContain('impact=ghost')) + }) + it('mounts the Service Inspector when ?service= is present', async () => { - renderApp('/map?service=ghost') + renderApp('/?service=ghost') expect( await screen.findByRole('button', { name: /close inspector/i }), ).toBeInTheDocument() }) it('renders the trail bar when ?trail= is present', async () => { - renderApp('/map?trail=svc:checkout') + renderApp('/?trail=svc:checkout') expect( await screen.findByRole('navigation', { name: /investigation trail/i }), ).toBeInTheDocument() }) + + it('opens the command palette on the global ⌘K shortcut', async () => { + const user = userEvent.setup() + renderApp('/') + await screen.findByRole('region', { name: /recent anomalies/i }) + await user.keyboard('{Meta>}k{/Meta}') + expect( + await screen.findByRole('dialog', { name: /command palette/i }), + ).toBeInTheDocument() + }) + + it('opens the shortcut sheet on "?"', async () => { + const user = userEvent.setup() + renderApp('/') + await screen.findByRole('region', { name: /recent anomalies/i }) + await user.keyboard('?') + expect( + await screen.findByRole('dialog', { name: /keyboard shortcuts/i }), + ).toBeInTheDocument() + }) }) diff --git a/ui/src/components/common/Gauge.module.css b/ui/src/components/common/Gauge.module.css new file mode 100644 index 0000000..0273c68 --- /dev/null +++ b/ui/src/components/common/Gauge.module.css @@ -0,0 +1,59 @@ +.gauge { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + aspect-ratio: 1 / 1; +} + +.dial { + display: block; + width: 100%; + height: 100%; +} + +.track, +.fill { + fill: none; + stroke-width: 6; + stroke-linecap: round; +} + +.track { + stroke: var(--gauge-track); +} + +.fill { + /* Animate the reveal with the shared duration/easing; under + prefers-reduced-motion --dur-2 is zeroed at tokens.css:123, so the arc + snaps to its value with no transition. transform/opacity-free property, + but this is a one-shot value change (no looping), so it is safe to ease. */ + transition: stroke-dashoffset var(--dur-2) var(--ease); +} + +.ok { + stroke: var(--ok); +} + +.warn { + stroke: var(--warn); +} + +.crit { + stroke: var(--crit); +} + +.accent { + stroke: var(--accent); +} + +.readout { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; +} diff --git a/ui/src/components/common/Gauge.tsx b/ui/src/components/common/Gauge.tsx new file mode 100644 index 0000000..08e3ec6 --- /dev/null +++ b/ui/src/components/common/Gauge.tsx @@ -0,0 +1,101 @@ +import { Metric } from './Metric' +import styles from './Gauge.module.css' + +/** Status palette for the arc fill. Saturation is rationed to status. */ +type GaugeTone = 'ok' | 'warn' | 'crit' | 'accent' + +interface GaugeProps { + /** Raw measured value (drives the arc fill and aria-valuenow). */ + value: number + /** Upper bound of the scale. Defaults to 1 (a 0–1 ratio). */ + max?: number + /** Accessible name fragment, e.g. "HEALTH". Also the visible legend. */ + label: string + /** Pre-formatted numeral — the source of truth, always rendered. */ + valueText: string + /** Optional unit suffix rendered tight against the numeral. */ + unit?: string + /** Arc fill color token. Defaults to the single accent. */ + tone?: GaugeTone +} + +// 270° sweep with a 90° gap centered at the bottom of the dial. The arc starts +// at the lower-left (135° from +x, measuring clockwise in SVG's y-down space) +// and sweeps clockwise through the top to the lower-right. +const SWEEP_DEG = 270 +const RADIUS = 42 +const CENTER = 50 +const CIRCUMFERENCE = 2 * Math.PI * RADIUS +const ARC_LENGTH = CIRCUMFERENCE * (SWEEP_DEG / 360) +// Rotate so the 90° gap sits at the bottom: the dasharray begins at +x (3 +// o'clock); rotating the whole ring by 135° puts the visible arc's start at the +// lower-left and its end at the lower-right. +const ROTATION = 135 + +function polar(angleDeg: number): { x: number; y: number } { + const rad = (angleDeg * Math.PI) / 180 + return { x: CENTER + RADIUS * Math.cos(rad), y: CENTER + RADIUS * Math.sin(rad) } +} + +// Endpoints of the 270° track, anchored to the same geometry as the dasharray. +const start = polar(ROTATION) +const end = polar(ROTATION + SWEEP_DEG) +// large-arc-flag is 1 because the swept angle (270°) exceeds 180°. +const ARC_PATH = `M ${start.x} ${start.y} A ${RADIUS} ${RADIUS} 0 1 1 ${end.x} ${end.y}` + +const TONE_CLASS: Record = { + ok: styles.ok, + warn: styles.warn, + crit: styles.crit, + accent: styles.accent, +} + +/** + * Reusable SVG arc gauge (Instrument Deck). The numeral (`valueText`) is the + * source of truth and is rendered centered in the mono voice via {@link Metric}; + * the 270° arc is decoration that fills proportionally to `value / max` and may + * saturate without ever altering the numeral. Pure SVG + CSS, no chart library. + */ +export function Gauge({ + value, + max = 1, + label, + valueText, + unit, + tone = 'accent', +}: Readonly) { + const safeMax = max > 0 ? max : 1 + const fraction = Math.min(Math.max(value / safeMax, 0), 1) + // Reveal `fraction` of the arc by offsetting the remainder out of view. + const fillOffset = ARC_LENGTH * (1 - fraction) + const aria = `${label} ${valueText}${unit ?? ''}` + + return ( +
+ +
+ +
+
+ ) +} diff --git a/ui/src/components/common/Metric.tsx b/ui/src/components/common/Metric.tsx new file mode 100644 index 0000000..cffbc31 --- /dev/null +++ b/ui/src/components/common/Metric.tsx @@ -0,0 +1,31 @@ +interface MetricProps { + /** Uppercase instrument legend, e.g. "P99". Omit for a bare numeral. */ + label?: string + /** The numeral, pre-formatted by the caller (e.g. via format.ts). */ + value: string + /** Optional unit suffix rendered tight against the value, e.g. "ms", "%". */ + unit?: string +} + +/** + * The Instrument Deck "indicator" primitive: a measured quantity rendered in + * the mono/tabular-nums voice with an optional uppercase legend and unit. The + * accessible name reads " " so screen readers announce a + * complete reading while the visual pieces stay aria-hidden. + */ +export function Metric({ label, value, unit }: Readonly) { + const aria = [label, `${value}${unit ?? ''}`].filter(Boolean).join(' ') + return ( + + {label ? {label} : null} + + {unit ? ( + + ) : null} + + ) +} diff --git a/ui/src/components/common/ServiceGroups.module.css b/ui/src/components/common/ServiceGroups.module.css index d2f5f0f..960f962 100644 --- a/ui/src/components/common/ServiceGroups.module.css +++ b/ui/src/components/common/ServiceGroups.module.css @@ -7,13 +7,10 @@ overflow-y: auto; } +/* The .legend utility (global.css) owns the mono-caps voice; the module only + * positions the section label above its rows. */ .sectionTitle { margin: 0 0 var(--space-1); - color: var(--text-3); - font-size: var(--text-2xs); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; } .list { diff --git a/ui/src/components/common/ServiceGroups.tsx b/ui/src/components/common/ServiceGroups.tsx index cf4c68f..c5f67c3 100644 --- a/ui/src/components/common/ServiceGroups.tsx +++ b/ui/src/components/common/ServiceGroups.tsx @@ -8,21 +8,30 @@ import styles from './ServiceGroups.module.css' interface ServiceGroupsProps { nodes: readonly SystemNode[] onOpen: (id: string) => void + /** Id of the inspected service — drives row focus-distortion. Optional so + * the xs flow-map fallback (no inspector) renders a plain list. */ + selectedId?: string | null } function Section({ title, nodes, onOpen, -}: Readonly<{ title: string; nodes: SystemNode[]; onOpen: (id: string) => void }>) { + selectedId, +}: Readonly<{ + title: string + nodes: SystemNode[] + onOpen: (id: string) => void + selectedId?: string | null +}>) { if (nodes.length === 0) return null return (
-

{title}

+

{title}

    {nodes.map((n) => (
  • - +
  • ))}
@@ -31,18 +40,19 @@ function Section({ } /** - * xs default for /map: status-grouped service cards, worst first; quiet - * healthy services collapsed behind a disclosure. + * Status-grouped service list, worst first; quiet healthy services collapsed + * behind a disclosure. Hairline-separated rows (not cards). Doubles as the xs + * default for /map. */ -export default function ServiceGroups({ nodes, onOpen }: Readonly) { +export default function ServiceGroups({ nodes, onOpen, selectedId }: Readonly) { const ranked = rankServices(nodes) const [healthyOpen, setHealthyOpen] = useState(false) return (
-
-
-
+
+
+
{ranked.healthy.length > 0 && (
{healthyOpen && (
    {ranked.healthy.map((n) => (
  • - +
  • ))}
diff --git a/ui/src/components/common/ServiceRow.module.css b/ui/src/components/common/ServiceRow.module.css index f5dc6fa..b67d6b2 100644 --- a/ui/src/components/common/ServiceRow.module.css +++ b/ui/src/components/common/ServiceRow.module.css @@ -5,14 +5,25 @@ width: 100%; min-height: 44px; padding: var(--space-1) var(--space-2); - border: none; + /* Hairline separation, not a card. The 1px transparent ring slot keeps the + * box from jumping when the selected ring appears. */ + border: 1px solid transparent; + border-bottom-color: var(--stroke-1); border-radius: var(--radius-2); background: transparent; color: var(--text-1); font-size: var(--text-sm); text-align: left; cursor: pointer; - transition: background var(--dur-1) var(--ease); + /* Focus-distortion travel: transform + opacity only (GPU). Keyed off + * --dur-2 so the reduced-motion block (zeros --dur-*) collapses the travel + * while the end-state (ring + dim) is preserved. */ + transition: + background var(--dur-1) var(--ease), + transform var(--dur-2) var(--ease), + opacity var(--dur-2) var(--ease), + border-color var(--dur-2) var(--ease); + transform-origin: left center; } @media (pointer: coarse) { @@ -27,6 +38,20 @@ } } +/* Signature focus-distortion: the single inspected row lifts and gets the full + * --accent ring — matching the canvas node and docked inspector so one selected + * service reads at the same intensity across all three surfaces. */ +.row[data-selected] { + background: var(--bg-raised); + border-color: var(--accent); + transform: scale(1.015); +} + +/* Non-selected rows recede while one is inspected. */ +.row[data-dimmed] { + opacity: 0.55; +} + .dot { flex: none; width: 8px; @@ -38,7 +63,7 @@ flex: 1; min-width: 0; overflow: hidden; - font-family: var(--font-mono); + /* Service name is a NAME — Inter, not the indicator voice. */ text-overflow: ellipsis; white-space: nowrap; } @@ -60,7 +85,7 @@ width: 60px; height: 4px; border-radius: 2px; - background: var(--bg-inset); + background: var(--gauge-track); overflow: hidden; } @@ -73,15 +98,14 @@ .metrics { flex: none; display: flex; - gap: var(--space-2); + gap: var(--space-3); justify-content: flex-end; - min-width: 110px; - font-family: var(--font-mono); font-size: var(--text-xs); color: var(--text-2); } .metric { - min-width: 48px; + display: inline-block; + min-width: 44px; text-align: right; } diff --git a/ui/src/components/common/ServiceRow.tsx b/ui/src/components/common/ServiceRow.tsx index 1a694f6..bb321bd 100644 --- a/ui/src/components/common/ServiceRow.tsx +++ b/ui/src/components/common/ServiceRow.tsx @@ -1,4 +1,4 @@ -import { formatMs, formatPercent } from '@/lib/format' +import { formatCount, formatMs, formatPercent } from '@/lib/format' import { nodeStatus, statusToken } from '@/lib/triage' import type { SystemNode } from '@/types/api' import styles from './ServiceRow.module.css' @@ -7,22 +7,46 @@ interface ServiceRowProps { node: SystemNode /** Row tap — drill into the service (pushes the investigation trail). */ onOpen: (id: string) => void + /** + * The id of the single inspected service, if any. When this row is the + * selected one it gets the focus-distortion (scale + accent ring); the + * non-selected rows dim. Optional so the xs flow-map fallback (no + * inspector) keeps the plain list. + */ + selectedId?: string | null } /** - * One service row: status dot, mono name, inline 60px health bar, err%/p99 - * (tabular), alert-count badge. Shared by the Triage feed and the flow map's - * xs card list so "how a service looks" has one source of truth. + * One service row: status dot, name, inline health bar, then the mono + * indicator columns (rps / err% / p99) — every measured quantity in the + * indicator voice. Shared by the Triage feed and the flow map's xs card list + * so "how a service looks" has one source of truth. + * + * Focus-distortion: when a service is inspected, the selected row lifts + * (scale 1.015) with an --accent-edge ring and the rest dim. This is a pure + * CSS transition applied after the URL state commits (no JS animation loop), + * so INP stays low; reduced-motion keeps the end-state (ring + dim), drops + * only the travel. */ -export default function ServiceRow({ node, onOpen }: Readonly) { +export default function ServiceRow({ node, onOpen, selectedId }: Readonly) { const color = statusToken(nodeStatus(node.status)) + const hasSelection = selectedId != null && selectedId !== '' + const selected = hasSelection && selectedId === node.id + const dimmed = hasSelection && !selected return ( - ) diff --git a/ui/src/components/common/ToneBadge.module.css b/ui/src/components/common/ToneBadge.module.css index 36a1754..2d3a5f7 100644 --- a/ui/src/components/common/ToneBadge.module.css +++ b/ui/src/components/common/ToneBadge.module.css @@ -26,6 +26,6 @@ } .unknown { - color: var(--text-2); - background: var(--bg-overlay); + color: var(--unknown); + background: var(--unknown-bg); } diff --git a/ui/src/components/common/__tests__/Gauge.test.tsx b/ui/src/components/common/__tests__/Gauge.test.tsx new file mode 100644 index 0000000..be97e76 --- /dev/null +++ b/ui/src/components/common/__tests__/Gauge.test.tsx @@ -0,0 +1,100 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { Gauge } from '../Gauge' +import styles from '../Gauge.module.css' + +// The arc geometry mirrors the component: a 270° sweep of radius 42. +const ARC_LENGTH = 2 * Math.PI * 42 * (270 / 360) + +function fillPath(): SVGPathElement { + // The fill arc is the path that carries a stroke-dashoffset. + const paths = Array.from(document.querySelectorAll('path')) + const fill = paths.find((p) => p.getAttribute('stroke-dashoffset') !== null) + if (!fill) throw new Error('fill arc not found') + return fill +} + +describe('Gauge', () => { + it('always renders the numeral and legend (the source of truth)', () => { + render() + expect(screen.getByText('94')).toHaveClass('num') + expect(screen.getByText('HEALTH')).toHaveClass('legend') + expect(screen.getByText('%')).toBeInTheDocument() + }) + + it('exposes role=meter with aria-valuenow/min/max reflecting value and max', () => { + render( + , + ) + const meter = screen.getByRole('meter') + expect(meter).toHaveAttribute('aria-valuenow', '312') + expect(meter).toHaveAttribute('aria-valuemin', '0') + expect(meter).toHaveAttribute('aria-valuemax', '1000') + }) + + it('names the meter "