From c4d719e7c6d87883b1d9d05af3435d5e48e8e024 Mon Sep 17 00:00:00 2001 From: Sean Sun Date: Sun, 23 Aug 2026 12:59:14 -0400 Subject: [PATCH 1/9] feat(frontend): refine application UI --- frontend/components.json | 24 + frontend/index.html | 1 + frontend/package-lock.json | 561 ++++++++- frontend/package.json | 11 + frontend/src/api/client.ts | 11 + frontend/src/api/mock-data.ts | 775 ++++++++++++ frontend/src/api/stats.ts | 6 +- frontend/src/api/types.ts | 10 + frontend/src/components/atoms/Button.tsx | 25 +- frontend/src/components/atoms/DocsLink.tsx | 2 +- frontend/src/components/atoms/Input.tsx | 6 +- frontend/src/components/atoms/Label.tsx | 2 +- frontend/src/components/charts/animation.ts | 36 + .../components/charts/area-gradient-defs.tsx | 113 ++ .../charts/chart-child-passthrough.ts | 117 ++ .../charts/chart-config-context.tsx | 92 ++ .../src/components/charts/chart-context.tsx | 428 +++++++ frontend/src/components/charts/chart-defs.ts | 72 ++ .../src/components/charts/chart-formatters.ts | 20 + .../components/charts/chart-legend-hover.tsx | 44 + .../components/charts/chart-loading-label.tsx | 56 + frontend/src/components/charts/chart-phase.ts | 52 + .../components/charts/chart-reveal-clip.tsx | 92 ++ .../components/charts/dash-tail-stroke.tsx | 75 ++ .../components/charts/decimate-time-series.ts | 136 +++ frontend/src/components/charts/fade-edges.ts | 52 + .../charts/filter-data-by-x-domain.ts | 59 + .../charts/generate-chart-skeleton-data.ts | 42 + frontend/src/components/charts/grid.tsx | 317 +++++ .../charts/highlight-segment-bounds.ts | 71 ++ .../components/charts/highlight-segment.tsx | 65 + .../src/components/charts/indicator-fade.ts | 64 + .../components/charts/line-chart-loading.tsx | 112 ++ frontend/src/components/charts/line-chart.tsx | 303 +++++ .../components/charts/line-loading-pulse.tsx | 218 ++++ .../components/charts/line-loading-timing.ts | 12 + .../charts/line-series-terminal-marker.tsx | 94 ++ frontend/src/components/charts/line.tsx | 406 +++++++ .../src/components/charts/loading-sweep.tsx | 488 ++++++++ .../src/components/charts/motion-utils.ts | 58 + .../components/charts/path-stroke-utils.ts | 88 ++ .../components/charts/projection-config.ts | 129 ++ .../charts/projection-line-end-marker.tsx | 69 ++ .../src/components/charts/projection-line.tsx | 234 ++++ .../src/components/charts/projection-utils.ts | 386 ++++++ .../charts/reference-area-config.ts | 68 ++ .../reference-area-registration-context.tsx | 16 + .../components/charts/series-bar-layout.ts | 61 + .../charts/series-dash-tail-overlay.tsx | 82 ++ .../charts/series-highlight-layer.tsx | 49 + .../components/charts/series-hover-dim.tsx | 60 + .../src/components/charts/series-markers.tsx | 294 +++++ .../components/charts/series-path-utils.ts | 108 ++ .../components/charts/series-point-marker.tsx | 209 ++++ .../charts/static-chart-preview-context.tsx | 22 + .../charts/time-series-chart-shell.tsx | 698 +++++++++++ .../charts/tooltip/chart-tooltip.tsx | 436 +++++++ .../components/charts/tooltip/date-ticker.tsx | 150 +++ .../src/components/charts/tooltip/index.ts | 14 + .../components/charts/tooltip/tooltip-box.tsx | 235 ++++ .../charts/tooltip/tooltip-content.tsx | 62 + .../components/charts/tooltip/tooltip-dot.tsx | 176 +++ .../charts/tooltip/tooltip-indicator.tsx | 211 ++++ .../charts/use-animated-series-path.ts | 165 +++ .../charts/use-animated-y-domains.ts | 242 ++++ .../charts/use-chart-interaction.ts | 352 ++++++ .../charts/use-chart-phase-orchestrator.ts | 183 +++ .../components/charts/use-enter-complete.ts | 28 + .../src/components/charts/use-grid-shimmer.ts | 111 ++ .../charts/use-highlight-segment.ts | 61 + .../components/charts/use-mount-progress.ts | 29 + .../charts/use-scheduled-tooltip.ts | 97 ++ frontend/src/components/charts/x-axis.tsx | 667 +++++++++++ .../src/components/charts/y-axis-scales.ts | 115 ++ .../src/components/charts/y-axis-ticks.ts | 25 + .../src/components/charts/y-domain-utils.ts | 131 ++ frontend/src/components/chrome/AppShell.tsx | 13 +- .../src/components/chrome/CommandPalette.tsx | 4 +- .../src/components/chrome/EnvSwitcher.tsx | 70 +- frontend/src/components/chrome/SideNav.tsx | 320 ++--- .../src/components/chrome/ThemeToggle.tsx | 38 +- frontend/src/components/chrome/TopBar.tsx | 59 +- frontend/src/components/chrome/UserMenu.tsx | 15 +- .../src/components/data/ChipLanguage.test.tsx | 56 + frontend/src/components/data/EmptyState.tsx | 6 +- frontend/src/components/data/FilterChip.tsx | 57 + .../src/components/data/MetadataBadge.tsx | 22 + frontend/src/components/data/Pagination.tsx | 8 +- frontend/src/components/data/PlatformIcon.tsx | 60 + frontend/src/components/data/SearchInput.tsx | 19 +- .../src/components/data/SelectionChip.tsx | 36 + .../src/components/data/SortableHeader.tsx | 6 +- .../src/components/data/StatCard.test.tsx | 5 +- frontend/src/components/data/StatCard.tsx | 32 +- frontend/src/components/data/StatusBadge.tsx | 8 +- frontend/src/components/data/StatusPip.tsx | 10 +- frontend/src/components/data/StatusTabs.tsx | 8 +- frontend/src/components/data/TagChip.tsx | 48 + .../src/components/feedback/ModalShell.tsx | 15 +- frontend/src/components/forms/DiffView.tsx | 2 +- frontend/src/components/forms/IconPicker.tsx | 4 +- .../src/components/forms/TargetSelector.tsx | 78 +- .../components/primitives/DropdownMenu.tsx | 9 +- frontend/src/components/shimmering-text.tsx | 90 ++ frontend/src/features/audit/AuditPage.tsx | 16 +- .../auth-providers/AuthProvidersPage.tsx | 60 +- .../src/features/carves/CarveDetailPage.tsx | 86 +- frontend/src/features/carves/CarveRunPage.tsx | 51 +- .../src/features/carves/CarvesListPage.tsx | 64 +- .../features/dashboard/ActivityLineChart.tsx | 150 +++ .../features/dashboard/DashboardPage.test.tsx | 2 +- .../src/features/dashboard/DashboardPage.tsx | 733 ++++++------ .../src/features/dev/ComponentGallery.tsx | 29 +- .../enrollment/AssembledConfigCard.tsx | 8 +- .../features/enrollment/CertificateCard.tsx | 10 +- .../src/features/enrollment/EnrollPage.tsx | 67 +- .../src/features/enrollment/FlagsCard.tsx | 8 +- .../environments/EnvConfigPage.test.tsx | 2 +- .../features/environments/EnvConfigPage.tsx | 96 +- .../environments/EnvironmentsPage.tsx | 53 +- .../src/features/log-sinks/LogSinksPage.tsx | 80 +- .../src/features/login/LoginPage.test.tsx | 20 + frontend/src/features/login/LoginPage.tsx | 670 +++++------ .../src/features/login/login-cyber-grid.css | 176 --- .../features/nodes/NodeConsolePage.test.tsx | 2 +- .../src/features/nodes/NodeConsolePage.tsx | 6 +- .../features/nodes/NodeDetailPage.test.tsx | 6 +- .../src/features/nodes/NodeDetailPage.tsx | 154 ++- .../features/nodes/NodeFileExplorerTab.tsx | 14 +- .../features/nodes/NodesTablePage.test.tsx | 11 +- .../src/features/nodes/NodesTablePage.tsx | 55 +- frontend/src/features/nodes/nodeSignals.tsx | 62 +- frontend/src/features/profile/MFAPanel.tsx | 14 +- .../src/features/profile/ProfilePage.test.tsx | 2 +- frontend/src/features/profile/ProfilePage.tsx | 82 +- .../features/queries/QueriesListPage.test.tsx | 2 +- .../src/features/queries/QueriesListPage.tsx | 70 +- .../src/features/queries/QueryDetailPage.tsx | 69 +- .../src/features/queries/QueryRunPage.tsx | 141 ++- .../components/NoCodeQueryBuilder.test.tsx | 164 +++ .../queries/components/NoCodeQueryBuilder.tsx | 1049 +++++++++++++++++ .../queries/components/OptionsPanel.tsx | 6 +- .../queries/components/QuickTemplates.tsx | 12 +- .../queries/components/TargetingPanel.tsx | 137 +-- .../features/queries/query-builder.test.ts | 73 ++ .../src/features/queries/query-builder.ts | 132 +++ .../saved-queries/SavedQueriesPage.tsx | 56 +- .../service-config/ServiceConfigPage.test.tsx | 6 +- .../service-config/ServiceConfigPage.tsx | 64 +- .../src/features/settings/SettingsPage.tsx | 32 +- frontend/src/features/tags/TagsPage.tsx | 38 +- frontend/src/features/users/UsersPage.tsx | 75 +- frontend/src/lib/design-tokens.test.ts | 4 +- frontend/src/lib/design-tokens.ts | 68 +- frontend/src/lib/environment-scope.test.ts | 33 + frontend/src/lib/environment-scope.ts | 20 + frontend/src/lib/utils.ts | 6 + frontend/src/styles/base.css | 207 ++-- frontend/src/styles/tokens.css | 90 +- frontend/src/test-setup.ts | 17 + pkg/osquery/tables_test.go | 33 + pkg/types/types.go | 27 +- 162 files changed, 15224 insertions(+), 2485 deletions(-) create mode 100644 frontend/components.json create mode 100644 frontend/src/api/mock-data.ts create mode 100644 frontend/src/components/charts/animation.ts create mode 100644 frontend/src/components/charts/area-gradient-defs.tsx create mode 100644 frontend/src/components/charts/chart-child-passthrough.ts create mode 100644 frontend/src/components/charts/chart-config-context.tsx create mode 100644 frontend/src/components/charts/chart-context.tsx create mode 100644 frontend/src/components/charts/chart-defs.ts create mode 100644 frontend/src/components/charts/chart-formatters.ts create mode 100644 frontend/src/components/charts/chart-legend-hover.tsx create mode 100644 frontend/src/components/charts/chart-loading-label.tsx create mode 100644 frontend/src/components/charts/chart-phase.ts create mode 100644 frontend/src/components/charts/chart-reveal-clip.tsx create mode 100644 frontend/src/components/charts/dash-tail-stroke.tsx create mode 100644 frontend/src/components/charts/decimate-time-series.ts create mode 100644 frontend/src/components/charts/fade-edges.ts create mode 100644 frontend/src/components/charts/filter-data-by-x-domain.ts create mode 100644 frontend/src/components/charts/generate-chart-skeleton-data.ts create mode 100644 frontend/src/components/charts/grid.tsx create mode 100644 frontend/src/components/charts/highlight-segment-bounds.ts create mode 100644 frontend/src/components/charts/highlight-segment.tsx create mode 100644 frontend/src/components/charts/indicator-fade.ts create mode 100644 frontend/src/components/charts/line-chart-loading.tsx create mode 100644 frontend/src/components/charts/line-chart.tsx create mode 100644 frontend/src/components/charts/line-loading-pulse.tsx create mode 100644 frontend/src/components/charts/line-loading-timing.ts create mode 100644 frontend/src/components/charts/line-series-terminal-marker.tsx create mode 100644 frontend/src/components/charts/line.tsx create mode 100644 frontend/src/components/charts/loading-sweep.tsx create mode 100644 frontend/src/components/charts/motion-utils.ts create mode 100644 frontend/src/components/charts/path-stroke-utils.ts create mode 100644 frontend/src/components/charts/projection-config.ts create mode 100644 frontend/src/components/charts/projection-line-end-marker.tsx create mode 100644 frontend/src/components/charts/projection-line.tsx create mode 100644 frontend/src/components/charts/projection-utils.ts create mode 100644 frontend/src/components/charts/reference-area-config.ts create mode 100644 frontend/src/components/charts/reference-area-registration-context.tsx create mode 100644 frontend/src/components/charts/series-bar-layout.ts create mode 100644 frontend/src/components/charts/series-dash-tail-overlay.tsx create mode 100644 frontend/src/components/charts/series-highlight-layer.tsx create mode 100644 frontend/src/components/charts/series-hover-dim.tsx create mode 100644 frontend/src/components/charts/series-markers.tsx create mode 100644 frontend/src/components/charts/series-path-utils.ts create mode 100644 frontend/src/components/charts/series-point-marker.tsx create mode 100644 frontend/src/components/charts/static-chart-preview-context.tsx create mode 100644 frontend/src/components/charts/time-series-chart-shell.tsx create mode 100644 frontend/src/components/charts/tooltip/chart-tooltip.tsx create mode 100644 frontend/src/components/charts/tooltip/date-ticker.tsx create mode 100644 frontend/src/components/charts/tooltip/index.ts create mode 100644 frontend/src/components/charts/tooltip/tooltip-box.tsx create mode 100644 frontend/src/components/charts/tooltip/tooltip-content.tsx create mode 100644 frontend/src/components/charts/tooltip/tooltip-dot.tsx create mode 100644 frontend/src/components/charts/tooltip/tooltip-indicator.tsx create mode 100644 frontend/src/components/charts/use-animated-series-path.ts create mode 100644 frontend/src/components/charts/use-animated-y-domains.ts create mode 100644 frontend/src/components/charts/use-chart-interaction.ts create mode 100644 frontend/src/components/charts/use-chart-phase-orchestrator.ts create mode 100644 frontend/src/components/charts/use-enter-complete.ts create mode 100644 frontend/src/components/charts/use-grid-shimmer.ts create mode 100644 frontend/src/components/charts/use-highlight-segment.ts create mode 100644 frontend/src/components/charts/use-mount-progress.ts create mode 100644 frontend/src/components/charts/use-scheduled-tooltip.ts create mode 100644 frontend/src/components/charts/x-axis.tsx create mode 100644 frontend/src/components/charts/y-axis-scales.ts create mode 100644 frontend/src/components/charts/y-axis-ticks.ts create mode 100644 frontend/src/components/charts/y-domain-utils.ts create mode 100644 frontend/src/components/data/ChipLanguage.test.tsx create mode 100644 frontend/src/components/data/FilterChip.tsx create mode 100644 frontend/src/components/data/MetadataBadge.tsx create mode 100644 frontend/src/components/data/PlatformIcon.tsx create mode 100644 frontend/src/components/data/SelectionChip.tsx create mode 100644 frontend/src/components/data/TagChip.tsx create mode 100644 frontend/src/components/shimmering-text.tsx create mode 100644 frontend/src/features/dashboard/ActivityLineChart.tsx delete mode 100644 frontend/src/features/login/login-cyber-grid.css create mode 100644 frontend/src/features/queries/components/NoCodeQueryBuilder.test.tsx create mode 100644 frontend/src/features/queries/components/NoCodeQueryBuilder.tsx create mode 100644 frontend/src/features/queries/query-builder.test.ts create mode 100644 frontend/src/features/queries/query-builder.ts create mode 100644 frontend/src/lib/environment-scope.test.ts create mode 100644 frontend/src/lib/environment-scope.ts create mode 100644 frontend/src/lib/utils.ts create mode 100644 pkg/osquery/tables_test.go diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..16615f68 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/base.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "$/components", + "utils": "$/lib/cn", + "ui": "$/components/atoms", + "lib": "$/lib", + "hooks": "$/hooks" + }, + "registries": { + "@bklit": "https://ui.bklit.com/r/{name}.json" + } +} diff --git a/frontend/index.html b/frontend/index.html index 9169c797..1403e706 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,5 +13,6 @@
+ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8d3503f3..17c9ef84 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@hookform/resolvers": "^5.7.1", "@monaco-editor/react": "^4.7.0", + "@number-flow/react": "^0.6.2", "@radix-ui/react-checkbox": "^1", "@radix-ui/react-dialog": "^1", "@radix-ui/react-dropdown-menu": "^2", @@ -25,12 +26,22 @@ "@tanstack/react-query": "^5", "@tanstack/react-router": "^1", "@tanstack/react-table": "^9", + "@visx/curve": "^4.0.1-alpha.0", + "@visx/event": "^4.0.1-alpha.0", + "@visx/grid": "^4.0.1-alpha.0", + "@visx/responsive": "^4.0.1-alpha.0", + "@visx/scale": "^4.0.1-alpha.0", + "@visx/shape": "^4.0.1-alpha.0", "clsx": "^2", + "d3-array": "^3.2.4", + "d3-shape": "^3.2.0", "lucide-react": "^1", "monaco-editor": "^0.56.0", + "motion": "^13.1.1", "react": "^19", "react-dom": "^19", "react-hook-form": "^7", + "react-icons": "^5.7.0", "tailwind-merge": "^3", "zod": "^4" }, @@ -54,7 +65,7 @@ "vitest": "^4" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.0.0" } }, "node_modules/@adobe/css-tools": { @@ -524,6 +535,20 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@number-flow/react": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@number-flow/react/-/react-0.6.2.tgz", + "integrity": "sha512-WjZuV4aA+vhRCgCF+adGLgFVVAJ8vdvq6EchRT2FiBIgElTXtDOA3YgkcMr9UHpvpS9v4H70AB5wZv0D9jc3QA==", + "license": "MIT", + "dependencies": { + "esm-env": "^1.1.4", + "number-flow": "0.6.2" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, "node_modules/@oxc-project/types": { "version": "0.143.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", @@ -1555,9 +1580,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1575,9 +1597,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1595,9 +1614,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1615,9 +1631,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1635,9 +1648,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1655,9 +1665,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1871,9 +1878,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1891,9 +1895,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1911,9 +1912,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1931,9 +1929,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2427,6 +2422,84 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "license": "MIT" + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2441,6 +2514,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "26.1.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", @@ -2455,7 +2540,6 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -2818,6 +2902,147 @@ "node": ">=16.20.0" } }, + "node_modules/@visx/curve": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-4.0.1-alpha.0.tgz", + "integrity": "sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==", + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/event": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-4.0.1-alpha.0.tgz", + "integrity": "sha512-EQqCMSv/s8NbFjo+hz3FKsvvYfP+2QslsFJ/24/O5l/W+7UC6J6aAvO0ujVwrTwdYbuQ+vhxKi1xdPdKR/qj1g==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "4.0.1-alpha.0" + } + }, + "node_modules/@visx/grid": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-4.0.1-alpha.0.tgz", + "integrity": "sha512-rycutGmTHO+znNdPumheWMglm7YfpffvRwUkVy5zy4WoORIuKTMkDxwnOzHG2xMxU3EE/YCd37xFV5AxA30yeg==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/point": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/shape": "4.0.1-alpha.0", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-4.0.1-alpha.0.tgz", + "integrity": "sha512-V19l7iQ7jccBv8kao/EByuI6o4xtxzzLV9nqVI1hRvmdzTVsuLpqlwzYCZUXJaTVvUWf8s4D2SQFjGkj/Nw+0w==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-4.0.1-alpha.0.tgz", + "integrity": "sha512-ijTfr/Nx09f03vIj9nyTr3z4Xth4Y75427UaogJh6dnIRLMEFHQOwNu791sbfiNj0a+ZXuaE32h0vKrFe4/8Qg==", + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-4.0.1-alpha.0.tgz", + "integrity": "sha512-o+1zGywQZY0+yOx3Iw87wc4bbPJRr/HnIukTwfOz4UVyj9pB1OQNVHB7OORO1+LBHJceWpB31co/ZV9KHncKrA==", + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-4.0.1-alpha.0.tgz", + "integrity": "sha512-nzjeE87vFSAXGWFiiNfBpNLAf0Q8Qmf6syvKLjqNi4kGZkdhbUll3E/59YsgWXmjM8+llPLWzGsP+JPvo5eq1A==", + "license": "MIT", + "dependencies": { + "@visx/vendor": "4.0.0-alpha.0" + } + }, + "node_modules/@visx/shape": { + "version": "4.0.1-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-4.0.1-alpha.0.tgz", + "integrity": "sha512-62QeiVNmPlterQGwhkEDcbq7M0MqY0lBsK5QKXtM9ZoPZWkuGV3aykA3+Xu20B2FAvyJq4LqJzBc7Sxr+EAdbA==", + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.17.13", + "@types/react": "*", + "@visx/curve": "4.0.1-alpha.0", + "@visx/group": "4.0.1-alpha.0", + "@visx/scale": "4.0.1-alpha.0", + "@visx/vendor": "4.0.0-alpha.0", + "classnames": "^2.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0-0 || ^18.0.0-0 || ^19.0.0-0" + } + }, + "node_modules/@visx/vendor": { + "version": "4.0.0-alpha.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-4.0.0-alpha.0.tgz", + "integrity": "sha512-6I+MuqXBcv9jnlcVowHoHKSdk9gXTWkHLKyqBwRWg7LY6A3Ei8SHfubpqGV5rBUSppxMq2RszPJUS6w+H0YgmQ==", + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-path": "3.1.1", + "@types/d3-scale": "4.0.2", + "@types/d3-shape": "3.1.7", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-path": "3.1.0", + "d3-scale": "4.0.2", + "d3-shape": "3.2.0", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", @@ -3032,6 +3257,12 @@ "node": ">=18" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3079,9 +3310,135 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "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-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "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-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -3103,6 +3460,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3179,6 +3545,12 @@ "dev": true, "license": "MIT" }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -3217,6 +3589,29 @@ } } }, + "node_modules/framer-motion": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.1.1.tgz", + "integrity": "sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^13.1.1", + "motion-utils": "^13.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -3281,6 +3676,15 @@ "node": ">=8" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3513,9 +3917,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3537,9 +3938,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3561,9 +3959,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3585,9 +3980,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3643,6 +4035,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -3721,6 +4119,43 @@ "marked": "14.0.0" } }, + "node_modules/motion": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/motion/-/motion-13.1.1.tgz", + "integrity": "sha512-WNZoK6xiF+kkTqkZ5K7FDDh6A8BG4i5Hc7KXtW8gtTxkpJFds+hIOrDaQGKjQj/AE/i4hJqAaUHEqp/Qo02y6Q==", + "license": "MIT", + "dependencies": { + "framer-motion": "^13.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.1.1.tgz", + "integrity": "sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^13.0.0" + } + }, + "node_modules/motion-utils": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz", + "integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.17", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", @@ -3740,6 +4175,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/number-flow": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/number-flow/-/number-flow-0.6.2.tgz", + "integrity": "sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw==", + "license": "MIT", + "dependencies": { + "esm-env": "^1.1.4" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -3917,6 +4361,15 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-icons": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.7.0.tgz", + "integrity": "sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -4017,6 +4470,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", @@ -4614,9 +5073,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4638,9 +5094,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4662,9 +5115,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4686,9 +5136,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/frontend/package.json b/frontend/package.json index d950126b..054b343c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "dependencies": { "@hookform/resolvers": "^5.7.1", "@monaco-editor/react": "^4.7.0", + "@number-flow/react": "^0.6.2", "@radix-ui/react-checkbox": "^1", "@radix-ui/react-dialog": "^1", "@radix-ui/react-dropdown-menu": "^2", @@ -36,12 +37,22 @@ "@tanstack/react-query": "^5", "@tanstack/react-router": "^1", "@tanstack/react-table": "^9", + "@visx/curve": "^4.0.1-alpha.0", + "@visx/event": "^4.0.1-alpha.0", + "@visx/grid": "^4.0.1-alpha.0", + "@visx/responsive": "^4.0.1-alpha.0", + "@visx/scale": "^4.0.1-alpha.0", + "@visx/shape": "^4.0.1-alpha.0", "clsx": "^2", + "d3-array": "^3.2.4", + "d3-shape": "^3.2.0", "lucide-react": "^1", "monaco-editor": "^0.56.0", + "motion": "^13.1.1", "react": "^19", "react-dom": "^19", "react-hook-form": "^7", + "react-icons": "^5.7.0", "tailwind-merge": "^3", "zod": "^4" }, diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index d6c13455..0540995f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3,6 +3,8 @@ * Extended in with typed apiFetch, AuthError, and ApiError. */ +import { isMockDataEnabled, resolveMockApiRequest } from './mock-data'; + let csrfTokenInMemory: string | null = null; export function setCsrfToken(t: string | null) { @@ -14,6 +16,10 @@ export function getCsrfToken(): string | null { } export function isAuthenticated(): boolean { + // Fixture-backed development previews do not need a live login session. + // Production, tests, and VITE_USE_MOCK_DATA=false retain the real cookie gate. + if (isMockDataEnabled()) return true; + // Mirror the apiFetch fallback: if memory is empty but the browser // still holds the osctrl_csrf cookie, prime from it before answering // false. Without this, every soft navigation through a route with a @@ -116,6 +122,11 @@ export async function apiFetch( } } + const mockResponse = resolveMockApiRequest(path, { ...init, method, headers }); + if (mockResponse.matched) { + return mockResponse.data as T; + } + const res = await fetch(path, { credentials: 'include', ...init, diff --git a/frontend/src/api/mock-data.ts b/frontend/src/api/mock-data.ts new file mode 100644 index 00000000..8b2fc452 --- /dev/null +++ b/frontend/src/api/mock-data.ts @@ -0,0 +1,775 @@ +/** + * Development-only API fixtures for visual review. + * + * The adapter is enabled by default in `vite` development mode and is never + * included in test or production behavior. Set VITE_USE_MOCK_DATA=false to + * bypass it. Removing it later is intentionally small: delete this file and + * the import/call in client.ts. + */ + +import type { + AdminTag, + CarveDetail, + DistributedQuery, + NodePosture, + NodesPagedResponse, + OsqueryNode, + OsqueryTable, + PostureScore, + QueryResultsResponse, + SavedQuery, +} from './types'; +import type { AuditLogsPagedResponse } from './audit'; +import type { NodeActivityBucket, NodeTileSeries, StatsResponse } from './stats'; + +export type MockApiResult = + | { matched: true; data: unknown } + | { matched: false }; + +const enabled = + import.meta.env.MODE === 'development' && + import.meta.env.VITE_USE_MOCK_DATA !== 'false'; + +export function isMockDataEnabled(): boolean { + return enabled; +} + +const HOUR = 60 * 60 * 1000; +const DAY = 24 * HOUR; +const now = Date.now(); + +function ago(milliseconds: number) { + return new Date(now - milliseconds).toISOString(); +} + +const ALL_PLATFORMS = ['darwin', 'linux', 'windows']; + +function mockOsqueryTable( + name: string, + description: string, + columns: Array<[name: string, type: string, description: string]>, + platforms = ALL_PLATFORMS, +): OsqueryTable { + return { + name, + description, + url: `https://osquery.io/schema/5.23.1/#${name}`, + platforms, + filter: platforms.map((platform) => `filter-${platform}`).join(' '), + columns: columns.map(([columnName, type, columnDescription]) => ({ + name: columnName, + type, + description: columnDescription, + })), + }; +} + +const mockOsqueryTables: OsqueryTable[] = [ + mockOsqueryTable('osquery_info', 'Build, platform, and runtime information for the running osquery process.', [ + ['version', 'text', 'Osquery version'], + ['pid', 'bigint', 'Process identifier'], + ['uuid', 'text', 'Unique process instance identifier'], + ['instance_id', 'text', 'Ephemeral instance identifier'], + ['config_hash', 'text', 'Hash of the active configuration'], + ['build_platform', 'text', 'Platform used to build osquery'], + ['start_time', 'bigint', 'Process start time'], + ['watcher', 'integer', 'Whether the watcher process is enabled'], + ]), + mockOsqueryTable('processes', 'All running processes on the host.', [ + ['pid', 'bigint', 'Process identifier'], + ['name', 'text', 'Process name'], + ['path', 'text', 'Path to the executable'], + ['cmdline', 'text', 'Complete command line'], + ['cwd', 'text', 'Current working directory'], + ['uid', 'bigint', 'User identifier'], + ['gid', 'bigint', 'Group identifier'], + ['parent', 'bigint', 'Parent process identifier'], + ['state', 'text', 'Process state'], + ['start_time', 'bigint', 'Process start time'], + ['total_size', 'bigint', 'Total virtual memory size'], + ['resident_size', 'bigint', 'Resident memory size'], + ['threads', 'integer', 'Number of threads'], + ]), + mockOsqueryTable('users', 'Local user accounts present on the host.', [ + ['uid', 'bigint', 'User identifier'], + ['gid', 'bigint', 'Primary group identifier'], + ['username', 'text', 'Login name'], + ['description', 'text', 'Account description'], + ['directory', 'text', 'Home directory'], + ['shell', 'text', 'Login shell'], + ['uuid', 'text', 'Account UUID'], + ['type', 'text', 'Account type'], + ]), + mockOsqueryTable('listening_ports', 'Processes with listening network sockets.', [ + ['pid', 'bigint', 'Process identifier'], + ['port', 'integer', 'Listening port'], + ['protocol', 'integer', 'Transport protocol'], + ['family', 'integer', 'Network family'], + ['address', 'text', 'Listening address'], + ['fd', 'bigint', 'Socket file descriptor'], + ['socket', 'bigint', 'Socket inode or handle'], + ['path', 'text', 'UNIX socket path'], + ]), + mockOsqueryTable('interface_addresses', 'IP addresses assigned to network interfaces.', [ + ['interface', 'text', 'Interface name'], + ['address', 'text', 'Assigned address'], + ['mask', 'text', 'Network mask'], + ['broadcast', 'text', 'Broadcast address'], + ['point_to_point', 'text', 'Point-to-point destination'], + ['type', 'text', 'Address type'], + ['friendly_name', 'text', 'Human-readable interface name'], + ]), + mockOsqueryTable('system_info', 'Core hardware and operating-system identity.', [ + ['hostname', 'text', 'System hostname'], + ['uuid', 'text', 'Hardware UUID'], + ['cpu_type', 'text', 'CPU architecture'], + ['cpu_brand', 'text', 'CPU brand string'], + ['physical_memory', 'bigint', 'Installed physical memory'], + ['hardware_vendor', 'text', 'Hardware vendor'], + ['hardware_model', 'text', 'Hardware model'], + ['hardware_serial', 'text', 'Hardware serial number'], + ['computer_name', 'text', 'Configured computer name'], + ['local_hostname', 'text', 'Local hostname'], + ]), + mockOsqueryTable('uptime', 'Time elapsed since the host last booted.', [ + ['days', 'integer', 'Whole days since boot'], + ['hours', 'integer', 'Hour component'], + ['minutes', 'integer', 'Minute component'], + ['seconds', 'integer', 'Second component'], + ['total_seconds', 'bigint', 'Total seconds since boot'], + ]), + mockOsqueryTable('logged_in_users', 'Users currently logged into the host.', [ + ['type', 'text', 'Login record type'], + ['user', 'text', 'Username'], + ['tty', 'text', 'Terminal device'], + ['host', 'text', 'Remote host'], + ['time', 'bigint', 'Login time'], + ['pid', 'bigint', 'Login process identifier'], + ]), + mockOsqueryTable('deb_packages', 'Installed Debian packages and package metadata.', [ + ['name', 'text', 'Package name'], + ['version', 'text', 'Installed version'], + ['source', 'text', 'Source package'], + ['size', 'bigint', 'Installed size'], + ['arch', 'text', 'Package architecture'], + ['status', 'text', 'Package status'], + ['maintainer', 'text', 'Package maintainer'], + ['section', 'text', 'Repository section'], + ], ['linux']), + mockOsqueryTable('launchd', 'macOS launch agents and daemons.', [ + ['name', 'text', 'Service name'], + ['path', 'text', 'Property list path'], + ['label', 'text', 'Launchd label'], + ['program', 'text', 'Program path'], + ['run_at_load', 'integer', 'Whether the service runs at load'], + ['keep_alive', 'integer', 'Whether launchd keeps the service alive'], + ['username', 'text', 'Configured user'], + ['groupname', 'text', 'Configured group'], + ], ['darwin']), + mockOsqueryTable('programs', 'Installed Windows applications.', [ + ['name', 'text', 'Application name'], + ['version', 'text', 'Installed version'], + ['install_location', 'text', 'Install directory'], + ['install_source', 'text', 'Installer source'], + ['publisher', 'text', 'Software publisher'], + ['install_date', 'text', 'Installation date'], + ['identifying_number', 'text', 'Product identifier'], + ], ['windows']), +]; + +const mockTags: AdminTag[] = [ + { + id: 1, + created_at: ago(90 * DAY), + updated_at: ago(5 * DAY), + name: 'production', + description: 'Production workloads', + color: '#3e63dd', + icon: 'server', + created_by: 'admin', + custom_tag: '', + auto_tag: false, + environment_id: 1, + tag_type: 0, + cohort: true, + }, + { + id: 2, + created_at: ago(60 * DAY), + updated_at: ago(12 * DAY), + name: 'engineering', + description: 'Engineering endpoints', + color: '#12a594', + icon: 'code', + created_by: 'admin', + custom_tag: '', + auto_tag: false, + environment_id: 1, + tag_type: 0, + cohort: false, + }, + { + id: 3, + created_at: ago(45 * DAY), + updated_at: ago(2 * DAY), + name: 'needs-attention', + description: 'Endpoints requiring follow-up', + color: '#e5484d', + icon: 'alert-triangle', + created_by: 'admin', + custom_tag: '', + auto_tag: true, + environment_id: 1, + tag_type: 1, + cohort: false, + }, + { + id: 4, + created_at: ago(30 * DAY), + updated_at: ago(8 * DAY), + name: 'finance', + description: 'Finance team devices', + color: '#8e4ec6', + icon: 'briefcase', + created_by: 'admin', + custom_tag: '', + auto_tag: false, + environment_id: 1, + tag_type: 0, + cohort: false, + }, +]; + +function makeNode( + id: number, + hostname: string, + platform: 'linux' | 'darwin' | 'windows', + version: string, + lastSeenHours: number, + tags: AdminTag[], + countryCode: string, + risk: 'low' | 'medium' | 'high' = 'low', +): OsqueryNode { + const platformVersion = + platform === 'darwin' ? '15.6' : platform === 'windows' ? '11 24H2' : 'Ubuntu 24.04'; + const active = lastSeenHours < 72; + return { + id, + created_at: ago((120 - id * 4) * DAY), + updated_at: ago(lastSeenHours * HOUR), + node_key: `mock-node-key-${id}`, + uuid: `mock-node-${String(id).padStart(3, '0')}`, + platform, + platform_version: platformVersion, + osquery_version: version, + hostname, + localname: hostname.split('.')[0], + ip_address: `10.24.${Math.floor(id / 10)}.${20 + id}`, + username: id % 2 === 0 ? 'svc-osquery' : 'local-user', + osquery_user: 'root', + environment: 'dev', + cpu: platform === 'darwin' ? 'Apple M3 Pro' : 'Intel Xeon Gold 6338N', + memory: String((16 + (id % 4) * 16) * 1024 * 1024 * 1024), + hardware_serial: `OSCTRL-MOCK-${String(id).padStart(5, '0')}`, + daemon_hash: `daemon-${id.toString(16).padStart(8, '0')}`, + config_hash: `config-${(id * 17).toString(16).padStart(8, '0')}`, + bytes_received: 245_000 + id * 187_321, + last_seen: ago(lastSeenHours * HOUR), + user_id: 1, + environment_id: 1, + extra_data: '', + country_code: countryCode, + tags, + uptime: { + days: 3 + id * 2, + hours: (id * 3) % 24, + minutes: id * 4, + seconds: id, + total_seconds: (3 + id * 2) * 86_400, + last_seen: ago(Math.min(lastSeenHours, 4) * HOUR), + }, + posture: { risk_level: risk }, + health: { + status: !active ? 'offline' : risk === 'high' ? 'at_risk' : risk === 'medium' ? 'attention' : 'healthy', + reason: !active + ? 'No check-in within the active window' + : risk === 'high' + ? 'Critical posture controls failed' + : risk === 'medium' + ? 'Review recommended' + : 'All signals normal', + signals: !active ? ['stale_checkin'] : risk === 'low' ? ['recent_checkin'] : ['posture_findings'], + }, + system_info: { + system: { + hardware_vendor: platform === 'darwin' ? 'Apple Inc.' : platform === 'windows' ? 'Dell Inc.' : 'Amazon EC2', + hardware_model: platform === 'darwin' ? 'MacBookPro18,3' : 'Virtual Machine', + hardware_serial: `OSCTRL-MOCK-${String(id).padStart(5, '0')}`, + cpu_brand: platform === 'darwin' ? 'Apple M3 Pro' : 'Intel Xeon Gold 6338N', + cpu_physical_cores: '8', + cpu_logical_cores: '16', + physical_memory: String((16 + (id % 4) * 16) * 1024 * 1024 * 1024), + computer_name: hostname, + local_hostname: hostname.split('.')[0], + }, + os: { + name: platform === 'darwin' ? 'macOS' : platform === 'windows' ? 'Windows 11' : 'Ubuntu', + version: platformVersion, + platform, + }, + osquery: { + version, + build_platform: platform, + config_valid: '1', + start_time: String(Math.floor((now - (id + 3) * DAY) / 1000)), + }, + }, + }; +} + +const mockNodes: OsqueryNode[] = [ + makeNode(1, 'api-prod-01.nyc', 'linux', '5.23.1', 0.1, [mockTags[0]], 'US'), + makeNode(2, 'web-prod-02.iad', 'linux', '5.23.1', 0.4, [mockTags[0]], 'US'), + makeNode(3, 'mbp-sloane', 'darwin', '5.22.0', 1.2, [mockTags[1]], 'US'), + makeNode(4, 'fin-wks-044', 'windows', '5.21.0', 3, [mockTags[3]], 'GB', 'medium'), + makeNode(5, 'buildkite-linux-07', 'linux', '5.23.1', 7, [mockTags[1]], 'DE'), + makeNode(6, 'mbp-keiko', 'darwin', '5.23.1', 13, [mockTags[1]], 'JP'), + makeNode(7, 'edge-prod-03.sfo', 'linux', '5.20.0', 28, [mockTags[0], mockTags[2]], 'US', 'high'), + makeNode(8, 'design-win-12', 'windows', '5.22.0', 51, [], 'CA'), + makeNode(9, 'worker-prod-18', 'linux', '5.23.1', 80, [mockTags[0], mockTags[2]], 'US', 'medium'), + makeNode(10, 'mbp-archive', 'darwin', '5.19.0', 118, [mockTags[2]], 'AU', 'high'), + makeNode(11, 'acct-win-07', 'windows', '5.21.0', 190, [mockTags[3], mockTags[2]], 'US', 'high'), + makeNode(12, 'legacy-db-02', 'linux', '5.18.1', 264, [mockTags[0], mockTags[2]], 'NL', 'high'), +]; + +function makeQuery( + id: number, + name: string, + query: string, + state: 'active' | 'completed' | 'expired' | 'deleted', + expected: number, + executions: number, + errors = 0, + type = 'query', +): DistributedQuery { + return { + id, + created_at: ago((id + 1) * HOUR), + updated_at: ago(id * 17 * 60 * 1000), + name, + creator: id % 2 ? 'admin' : 'security-bot', + query, + expected, + executions, + errors, + active: state === 'active', + hidden: false, + protected: false, + completed: state === 'completed', + deleted: state === 'deleted', + expired: state === 'expired', + type, + path: type === 'carve' ? query : '', + environment_id: 1, + extra_data: '', + expiration: ago(-12 * HOUR), + target: expected === mockNodes.length ? 'all' : 'selected', + targets: expected === mockNodes.length ? [{ type: 'environment', value: 'dev' }] : [{ type: 'platform', value: 'linux' }], + }; +} + +let mockQueries: DistributedQuery[] = [ + makeQuery(1, 'incident-network-connections', 'SELECT * FROM process_open_sockets;', 'active', 12, 9, 1), + makeQuery(2, 'browser-extension-audit', 'SELECT * FROM chrome_extensions;', 'active', 7, 4), + makeQuery(3, 'unsigned-kernel-modules', 'SELECT * FROM kernel_modules WHERE signed = 0;', 'completed', 8, 8), + makeQuery(4, 'launch-items-review', 'SELECT * FROM launchd;', 'completed', 2, 2), + makeQuery(5, 'stale-local-admins', 'SELECT * FROM users WHERE uid = 0;', 'expired', 12, 10, 2), +]; + +let mockSavedQueries: SavedQuery[] = [ + { id: 1, created_at: ago(48 * DAY), updated_at: ago(4 * DAY), name: 'Listening ports', creator: 'admin', query: 'SELECT pid, port, protocol, address FROM listening_ports;', environment_id: 1 }, + { id: 2, created_at: ago(34 * DAY), updated_at: ago(8 * DAY), name: 'Recent user sessions', creator: 'admin', query: 'SELECT * FROM logged_in_users;', environment_id: 1 }, + { id: 3, created_at: ago(21 * DAY), updated_at: ago(2 * DAY), name: 'Unsigned binaries', creator: 'security-bot', query: 'SELECT path FROM signature WHERE signed = 0;', environment_id: 1 }, + { id: 4, created_at: ago(9 * DAY), updated_at: ago(9 * DAY), name: 'Disk encryption status', creator: 'admin', query: 'SELECT * FROM disk_encryption;', environment_id: 1 }, +]; + +let mockCarves: DistributedQuery[] = [ + { ...makeQuery(21, 'collect-suspicious-binary', '/tmp/update-agent', 'active', 2, 1, 0, 'carve'), carve_status: 'ACTIVE' }, + { ...makeQuery(22, 'browser-artifacts', '/Users/*/Library/Application Support/Google/Chrome/Default/History', 'completed', 3, 3, 0, 'carve'), carve_status: 'COMPLETED' }, + { ...makeQuery(23, 'nginx-access-logs', '/var/log/nginx/access.log', 'expired', 4, 2, 1, 'carve'), carve_status: 'EXPIRED' }, +]; + +const mockAudit: AuditLogsPagedResponse['items'] = [ + { id: 1, created_at: ago(4 * 60 * 1000), service: 'osctrl-api', username: 'admin', line: 'ran query incident-network-connections against 12 nodes', log_type: 4, severity: 1, source_ip: '127.0.0.1', environment_id: 1, env_uuid: 'dev' }, + { id: 2, created_at: ago(19 * 60 * 1000), service: 'osctrl-api', username: 'security-bot', line: 'tagged edge-prod-03.sfo with needs-attention', log_type: 6, severity: 1, source_ip: '10.24.0.8', environment_id: 1, env_uuid: 'dev' }, + { id: 3, created_at: ago(42 * 60 * 1000), service: 'osctrl-tls', username: 'node', line: 'node mbp-sloane enrolled successfully', log_type: 3, severity: 0, source_ip: '10.24.0.23', environment_id: 1, env_uuid: 'dev' }, + { id: 4, created_at: ago(2 * HOUR), service: 'osctrl-api', username: 'admin', line: 'created carve collect-suspicious-binary', log_type: 5, severity: 1, source_ip: '127.0.0.1', environment_id: 1, env_uuid: 'dev' }, + { id: 5, created_at: ago(5 * HOUR), service: 'osctrl-api', username: 'admin', line: 'updated environment dev configuration', log_type: 7, severity: 1, source_ip: '127.0.0.1', environment_id: 1, env_uuid: 'dev' }, + { id: 6, created_at: ago(9 * HOUR), service: 'osctrl-api', username: 'admin', line: 'logged in', log_type: 1, severity: 0, source_ip: '127.0.0.1', environment_id: 0 }, +]; + +function activeNode(node: OsqueryNode) { + return Date.parse(node.last_seen) >= now - 72 * HOUR; +} + +function platformBucket(node: OsqueryNode) { + if (node.platform === 'darwin') return 'darwin'; + if (node.platform === 'windows') return 'windows'; + if (node.platform === 'linux') return 'linux'; + return 'other'; +} + +function platformCounts(nodes: OsqueryNode[]) { + const counts = { linux: 0, darwin: 0, windows: 0, other: 0 }; + for (const node of nodes) counts[platformBucket(node)] += 1; + return counts; +} + +function tileSeries(seed = 1, days = 2): NodeTileSeries { + const length = Math.max(24, days * 24); + const make = (offset: number, scale: number) => + Array.from({ length }, (_, index) => { + const wave = Math.sin((index + seed + offset) / 4) + 1; + return index % (9 + offset) === 0 ? 0 : Math.round(wave * scale + ((index + seed) % 3)); + }); + const config = make(1, 2); + const status = make(2, 8); + const result = make(3, 5); + const query_read = make(4, 2); + const query_write = make(5, 1); + const enroll = make(6, 0.35); + const total = status.map((_, index) => + status[index] + result[index] + config[index] + query_read[index] + query_write[index] + enroll[index], + ); + return { + start: new Date(now - (length - 1) * HOUR).toISOString(), + bucket_seconds: 3600, + enroll, + config, + status, + result, + query_read, + query_write, + total, + }; +} + +const NODE_ACTIVITY_INTERVAL_HOURS: Record = { + '3h': 3, + '6h': 6, + '12h': 12, + '1d': 24, + '2d': 48, + '3d': 72, + '7d': 168, +}; + +function nodeActivity(seed = 1, hours = 24, bucketSeconds = 3600): NodeActivityBucket[] { + const bucketMs = bucketSeconds * 1000; + const length = Math.max(1, Math.round((hours * HOUR) / bucketMs)); + const lastBucketStart = Math.floor(now / bucketMs) * bucketMs; + + return Array.from({ length }, (_, index) => ({ + bucket_start: new Date(lastBucketStart - (length - 1 - index) * bucketMs).toISOString(), + status: (index * seed) % 17, + result: (index + seed) % 9, + query: index % 6 === 0 ? 2 : 0, + carve: index % 17 === 0 ? 1 : 0, + config: index % 4 === 0 ? 1 : 0, + })); +} + +function page(items: T[], search: URLSearchParams) { + const pageNumber = Math.max(1, Number(search.get('page') ?? 1)); + const pageSize = Math.max(1, Number(search.get('page_size') ?? 50)); + const start = (pageNumber - 1) * pageSize; + return { + items: items.slice(start, start + pageSize), + page: pageNumber, + page_size: pageSize, + total_items: items.length, + total_pages: Math.max(1, Math.ceil(items.length / pageSize)), + }; +} + +function body(init: RequestInit): Partial { + if (typeof init.body !== 'string') return {}; + try { + return JSON.parse(init.body) as Partial; + } catch { + return {}; + } +} + +function hit(data: unknown): MockApiResult { + return { matched: true, data }; +} + +function queryStateMatches(query: DistributedQuery, target: string) { + if (target === 'all' || target === 'all-full') return true; + if (target === 'active') return query.active; + if (target === 'completed') return query.completed; + if (target === 'expired') return query.expired; + if (target === 'deleted') return query.deleted; + if (target === 'hidden') return query.hidden; + return true; +} + +export function resolveMockApiRequest(path: string, init: RequestInit = {}): MockApiResult { + if (!enabled) return { matched: false }; + + const url = new URL(path, 'http://osctrl.mock'); + const pathname = url.pathname; + const method = (init.method ?? 'GET').toUpperCase(); + + if (method === 'GET' && pathname === '/api/v1/users/me') { + return hit({ + id: 1, + username: 'admin', + fullname: 'Local preview', + admin: true, + service: false, + created_at: ago(180 * DAY), + updated_at: ago(2 * DAY), + last_login: ago(18 * 60 * 1000), + token_expire: new Date(now + 30 * DAY).toISOString(), + environments: [], + }); + } + + if (method === 'GET' && pathname === '/api/v1/osquery/tables') { + return hit(mockOsqueryTables); + } + + // Dashboard and activity surfaces. + if (method === 'GET' && pathname === '/api/v1/stats') { + const active = mockNodes.filter(activeNode).length; + const activeQueries = mockQueries.filter((query) => query.active).length; + const activeCarves = mockCarves.filter((carve) => carve.active).length; + const stats: StatsResponse = { + total_nodes: mockNodes.length, + active_nodes: active, + inactive_nodes: mockNodes.length - active, + inactive_hours: 72, + total_active_queries: activeQueries, + total_active_carves: activeCarves, + platform_counts: platformCounts(mockNodes), + environments: [{ + uuid: 'dev', + name: 'dev', + active, + inactive: mockNodes.length - active, + total: mockNodes.length, + active_queries: activeQueries, + active_carves: activeCarves, + platform_counts: platformCounts(mockNodes), + }], + }; + return hit(stats); + } + if (method === 'GET' && pathname === '/api/v1/stats/osquery-versions') { + const counts = new Map(); + for (const node of mockNodes) counts.set(node.osquery_version, (counts.get(node.osquery_version) ?? 0) + 1); + return hit([...counts].map(([version, count]) => ({ version, count })).sort((a, b) => b.count - a.count)); + } + if (method === 'GET' && pathname.includes('/api/v1/stats/activity/env-tiles/')) { + return hit(tileSeries(4, Number(url.searchParams.get('days') ?? 2))); + } + if (method === 'GET' && pathname.includes('/api/v1/stats/activity/node-tiles-batch/')) { + const uuids = (url.searchParams.get('uuids') ?? '').split(',').filter(Boolean); + const days = Number(url.searchParams.get('days') ?? 2); + return hit(Object.fromEntries(uuids.map((uuid, index) => [uuid, tileSeries(index + 1, days)]))); + } + if (method === 'GET' && pathname.includes('/api/v1/stats/activity/node-tiles/')) { + const uuid = pathname.split('/').at(-1) ?? ''; + const seed = mockNodes.findIndex((node) => node.uuid === uuid) + 1; + return hit(tileSeries(Math.max(seed, 1), Number(url.searchParams.get('days') ?? 2))); + } + if (method === 'GET' && pathname.includes('/api/v1/stats/activity/node-batch/')) { + const uuids = (url.searchParams.get('uuids') ?? '').split(',').filter(Boolean); + return hit(Object.fromEntries(uuids.map((uuid, index) => [uuid, nodeActivity(index + 1)]))); + } + if (method === 'GET' && pathname.includes('/api/v1/stats/activity/node/')) { + const uuid = pathname.split('/').at(-1) ?? ''; + const seed = mockNodes.findIndex((node) => node.uuid === uuid) + 1; + const hours = NODE_ACTIVITY_INTERVAL_HOURS[url.searchParams.get('interval') ?? '1d'] ?? 24; + const requestedBucketSeconds = Number(url.searchParams.get('bucket_seconds') ?? 3600); + const bucketSeconds = Number.isFinite(requestedBucketSeconds) && requestedBucketSeconds > 0 + ? requestedBucketSeconds + : 3600; + return hit(nodeActivity(Math.max(seed, 1), hours, bucketSeconds)); + } + + // Nodes list, detail, logs, and posture. + const nodeDetail = pathname.match(/^\/api\/v1\/nodes\/[^/]+\/node\/([^/]+)$/); + if (method === 'GET' && nodeDetail) { + return hit(mockNodes.find((node) => node.uuid === decodeURIComponent(nodeDetail[1])) ?? mockNodes[0]); + } + const nodeList = pathname.match(/^\/api\/v1\/nodes\/([^/]+)$/); + if (method === 'GET' && nodeList) { + const q = (url.searchParams.get('q') ?? '').toLowerCase(); + const status = url.searchParams.get('status'); + const platform = url.searchParams.get('platform'); + const filtered = mockNodes.filter((node) => { + if (q && !`${node.hostname} ${node.localname} ${node.ip_address} ${node.uuid}`.toLowerCase().includes(q)) return false; + if (status === 'active' && !activeNode(node)) return false; + if (status === 'inactive' && activeNode(node)) return false; + if (platform && platformBucket(node) !== platform) return false; + return true; + }); + return hit(page(filtered, url.searchParams) satisfies NodesPagedResponse); + } + const nodeLogs = pathname.match(/^\/api\/v1\/logs\/(status|result)\/[^/]+\/([^/]+)$/); + if (method === 'GET' && nodeLogs) { + const type = nodeLogs[1] as 'status' | 'result'; + const uuid = decodeURIComponent(nodeLogs[2]); + const items = type === 'status' + ? [ + { created_at: ago(8 * 60 * 1000), severity: '0', filename: 'scheduler.cpp', line: 'Scheduled query completed successfully', message: 'schedule executed' }, + { created_at: ago(42 * 60 * 1000), severity: '1', filename: 'watcher.cpp', line: 'Performance limit approached', message: 'watchdog memory threshold at 82%' }, + ] + : [ + { created_at: ago(12 * 60 * 1000), name: 'uptime', action: 'added', columns: { days: '14', hours: '6', minutes: '22' } }, + { created_at: ago(65 * 60 * 1000), name: 'listening_ports', action: 'added', columns: { pid: '912', port: '443', protocol: '6' } }, + ]; + return hit({ items, type, uuid, env: 'dev', limit: 100 }); + } + if (method === 'GET' && /\/posture\/score$/.test(pathname)) { + const uuid = pathname.split('/').at(-3) ?? mockNodes[0].uuid; + const score: PostureScore = { + node_uuid: uuid, + timestamp: ago(2 * HOUR), + total_score: 24, + risk_level: 'medium', + pass_count: 8, + warn_count: 2, + fail_count: 1, + controls: [ + { category: 'hardening', control_id: 'OSCTRL-1', framework: 'CIS', title: 'Disk encryption enabled', description: 'System disk should be encrypted.', status: 'pass', severity: 'high', score: 0, max_score: 20, detail: 'Encryption is enabled.' }, + { category: 'access', control_id: 'OSCTRL-2', framework: 'CIS', title: 'No unexpected local admins', description: 'Administrative access should be constrained.', status: 'warn', severity: 'medium', score: 8, max_score: 12, detail: 'One account requires review.' }, + ], + }; + return hit(score); + } + if (method === 'GET' && /\/posture$/.test(pathname)) { + const uuid = pathname.split('/').at(-2) ?? mockNodes[0].uuid; + const posture: NodePosture[] = [ + { id: 1, created_at: ago(3 * DAY), updated_at: ago(2 * HOUR), node_uuid: uuid, environment: 'dev', category: 'uptime', query_name: 'osctrl:posture:uptime', row_count: 1, summary: '14 days, 6 hours', first_seen: ago(3 * DAY), last_seen: ago(2 * HOUR) }, + { id: 2, created_at: ago(3 * DAY), updated_at: ago(2 * HOUR), node_uuid: uuid, environment: 'dev', category: 'disk_encryption', query_name: 'osctrl:posture:disk_encryption', row_count: 1, summary: 'encrypted', first_seen: ago(3 * DAY), last_seen: ago(2 * HOUR) }, + ]; + return hit(posture); + } + if (method === 'POST' && /\/api\/v1\/nodes\/[^/]+\/(delete|tag)$/.test(pathname)) { + return hit({ message: 'Mock action completed' }); + } + + // Distributed queries and their results. + const queryList = pathname.match(/^\/api\/v1\/queries\/[^/]+\/list\/([^/]+)$/); + if (method === 'GET' && queryList) { + const target = decodeURIComponent(queryList[1]); + const q = (url.searchParams.get('q') ?? '').toLowerCase(); + const filtered = mockQueries.filter((query) => queryStateMatches(query, target) && (!q || `${query.name} ${query.creator} ${query.query}`.toLowerCase().includes(q))); + return hit(page(filtered, url.searchParams)); + } + const queryResults = pathname.match(/^\/api\/v1\/queries\/[^/]+\/results\/([^/]+)$/); + if (method === 'GET' && queryResults) { + const name = decodeURIComponent(queryResults[1]); + const items = mockNodes.slice(0, 7).map((node, index) => ({ + id: index + 1, + created_at: ago((index + 1) * 4 * 60 * 1000), + uuid: node.uuid, + environment: 'dev', + name, + data: JSON.stringify({ hostname: node.hostname, platform: node.platform, pid: 400 + index, path: `/usr/bin/mock-${index}` }), + status: index === 5 ? 1 : 0, + })); + return hit({ ...page(items, url.searchParams), since: ago(HOUR) } satisfies QueryResultsResponse); + } + const queryAction = pathname.match(/^\/api\/v1\/queries\/[^/]+\/(delete|expire|complete)\/([^/]+)$/); + if (method === 'POST' && queryAction) { + const name = decodeURIComponent(queryAction[2]); + mockQueries = mockQueries.map((query) => query.name === name ? { ...query, active: false, [queryAction[1] === 'delete' ? 'deleted' : queryAction[1] === 'expire' ? 'expired' : 'completed']: true } : query); + return hit({ message: 'Mock query updated' }); + } + const queryDetail = pathname.match(/^\/api\/v1\/queries\/[^/]+\/([^/]+)$/); + if (method === 'GET' && queryDetail) { + return hit(mockQueries.find((query) => query.name === decodeURIComponent(queryDetail[1])) ?? mockQueries[0]); + } + if (method === 'POST' && /^\/api\/v1\/queries\/[^/]+$/.test(pathname)) { + return hit({ query_name: 'mock-live-query' }); + } + + // Saved queries, including in-memory CRUD for review interactions. + const savedPath = pathname.match(/^\/api\/v1\/saved-queries\/[^/]+(?:\/([^/]+))?$/); + if (savedPath) { + const name = savedPath[1] ? decodeURIComponent(savedPath[1]) : undefined; + if (method === 'GET' && !name) { + const q = (url.searchParams.get('q') ?? '').toLowerCase(); + return hit(page(mockSavedQueries.filter((item) => !q || `${item.name} ${item.creator} ${item.query}`.toLowerCase().includes(q)), url.searchParams)); + } + if (method === 'POST' && !name) { + const request = body<{ name: string; query: string }>(init); + const item: SavedQuery = { id: Date.now(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), name: request.name ?? 'Untitled query', creator: 'admin', query: request.query ?? 'SELECT 1;', environment_id: 1 }; + mockSavedQueries = [item, ...mockSavedQueries]; + return hit(item); + } + if (method === 'PATCH' && name) { + const request = body<{ query: string }>(init); + mockSavedQueries = mockSavedQueries.map((item) => item.name === name ? { ...item, query: request.query ?? item.query, updated_at: new Date().toISOString() } : item); + return hit(mockSavedQueries.find((item) => item.name === name)); + } + if (method === 'DELETE' && name) { + mockSavedQueries = mockSavedQueries.filter((item) => item.name !== name); + return hit({ message: 'Mock saved query deleted' }); + } + } + + // Carves and carve detail. + const carvePath = pathname.match(/^\/api\/v1\/carves\/[^/]+(?:\/([^/]+))?$/); + if (carvePath) { + const name = carvePath[1] ? decodeURIComponent(carvePath[1]) : undefined; + if (method === 'GET' && !name) { + const target = url.searchParams.get('target') ?? 'all'; + const q = (url.searchParams.get('q') ?? '').toLowerCase(); + return hit(page(mockCarves.filter((carve) => queryStateMatches(carve, target) && (!q || `${carve.name} ${carve.path}`.toLowerCase().includes(q))), url.searchParams)); + } + if (method === 'GET' && name) { + const query = mockCarves.find((carve) => carve.name === name) ?? mockCarves[0]; + const detail: CarveDetail = { + query, + files: mockNodes.slice(0, Math.min(query.executions, 3)).map((node, index) => ({ + carve_id: `mock-carve-${index + 1}`, + session_id: `session-${index + 1}`, + uuid: node.uuid, + path: query.path, + status: query.completed ? 'COMPLETED' : 'PENDING', + carve_size: 1_240_000 + index * 820_000, + block_size: 8192, + total_blocks: 152 + index * 100, + completed_blocks: query.completed ? 152 + index * 100 : 94, + archived: query.completed, + created_at: query.created_at, + completed_at: query.completed ? query.updated_at : '', + })), + }; + return hit(detail); + } + if (method === 'POST' && !name) return hit({ query_name: 'mock-carve' }); + } + const carveAction = pathname.match(/^\/api\/v1\/carves\/[^/]+\/(delete|expire|complete)\/([^/]+)$/); + if (method === 'POST' && carveAction) { + mockCarves = mockCarves.map((carve) => carve.name === decodeURIComponent(carveAction[2]) ? { ...carve, active: false, [carveAction[1] === 'delete' ? 'deleted' : carveAction[1] === 'expire' ? 'expired' : 'completed']: true } : carve); + return hit({ message: 'Mock carve updated' }); + } + + // Tags and audit activity. + if (method === 'GET' && /^\/api\/v1\/tags(?:\/[^/]+)?$/.test(pathname)) return hit(mockTags); + if (method === 'POST' && /^\/api\/v1\/tags\/[^/]+\/(add|edit|remove)$/.test(pathname)) return hit({ data: 'Mock tag action completed' }); + if (method === 'GET' && pathname === '/api/v1/audit-logs') { + const qType = url.searchParams.get('type'); + const filtered = qType ? mockAudit.filter((item) => item.log_type === Number(qType)) : mockAudit; + return hit(page(filtered, url.searchParams)); + } + + return { matched: false }; +} diff --git a/frontend/src/api/stats.ts b/frontend/src/api/stats.ts index b8915190..4d66b1ce 100644 --- a/frontend/src/api/stats.ts +++ b/frontend/src/api/stats.ts @@ -121,9 +121,9 @@ export function getNodeActivity( ): Promise { const sp = new URLSearchParams(); sp.set('interval', interval); - // Optional hourly override: the per-node heatmap aligns to an hourly grid so - // it can merge in the Redis-backed config series (hourly). Omitted for the - // Nodes-table sparklines, which keep the interval's native bucket size. + // Optional resolution override: the per-node heatmap requests a fixed number + // of cells and merges the result with the Redis-backed hourly config series. + // Omitted for the Nodes-table sparklines, which keep the native bucket size. if (bucketSeconds) sp.set('bucket_seconds', String(bucketSeconds)); return apiFetch( `/api/v1/stats/activity/node/${encodeURIComponent(env)}/${encodeURIComponent(uuid)}?${sp.toString()}`, diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index 420a19b4..6eea1182 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -563,13 +563,23 @@ export interface OsqueryTableColumn { name: string; description: string; type: string; + notes?: string; + hidden?: boolean; + required?: boolean; + index?: boolean; } export interface OsqueryTable { name: string; + description: string; url: string; platforms: string[]; filter: string; + evented?: boolean; + cacheable?: boolean; + notes?: string; + examples?: string[]; + columns: OsqueryTableColumn[]; } export interface NodePosture { diff --git a/frontend/src/components/atoms/Button.tsx b/frontend/src/components/atoms/Button.tsx index 3fa9e5c6..771cc161 100644 --- a/frontend/src/components/atoms/Button.tsx +++ b/frontend/src/components/atoms/Button.tsx @@ -11,19 +11,16 @@ interface ButtonProps extends ButtonHTMLAttributes { const variantClasses: Record = { primary: [ - 'bg-gradient-to-b from-[color:var(--signal-bright)] to-[color:var(--signal)]', - 'text-[#051010]', + 'bg-[color:var(--accent)] hover:bg-[color:var(--accent-hover)]', + 'text-[color:var(--accent-contrast)]', 'font-semibold', - 'border border-[color:var(--signal)]/60', - 'shadow-[inset_0_1px_0_rgba(255,255,255,0.25),0_1px_14px_-2px_var(--signal-glow)]', - 'hover:brightness-110', - '[data-theme=light]:text-white', + 'border border-[color:var(--accent)]', ].join(' '), ghost: [ - 'bg-[color:var(--bg-2)]', - 'text-[color:var(--text-1)]', + 'bg-[color:var(--bg-1)]', + 'text-[color:var(--text-2)]', 'border border-[color:var(--border)]', - 'hover:bg-[color:var(--bg-3)] hover:border-[color:var(--border-strong)]', + 'hover:bg-[color:var(--bg-3)] hover:text-[color:var(--text-1)] hover:border-[color:var(--border-strong)]', ].join(' '), danger: [ 'bg-[color:var(--danger)]/10', @@ -34,9 +31,9 @@ const variantClasses: Record = { }; const sizeClasses: Record = { - sm: 'px-2.5 py-1 text-xs rounded-md', - md: 'px-3.5 py-2 text-sm rounded-lg', - lg: 'px-5 py-2.5 text-base rounded-lg', + sm: 'h-6 px-2 text-xs rounded-md', + md: 'h-8 px-3 text-sm rounded-md', + lg: 'h-10 px-4 text-sm rounded-lg', }; export const Button = forwardRef( @@ -47,8 +44,8 @@ export const Button = forwardRef( disabled={disabled} className={cn( 'inline-flex items-center justify-center gap-2 font-medium', - 'transition-all duration-[120ms] ease-out', - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--signal)]', + 'transition-[background-color,border-color,color] duration-[100ms] ease-out', + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-soft)] focus-visible:border-[color:var(--accent)]', 'disabled:opacity-40 disabled:cursor-not-allowed disabled:pointer-events-none', variantClasses[variant], sizeClasses[size], diff --git a/frontend/src/components/atoms/DocsLink.tsx b/frontend/src/components/atoms/DocsLink.tsx index 4ed899f5..03834cb8 100644 --- a/frontend/src/components/atoms/DocsLink.tsx +++ b/frontend/src/components/atoms/DocsLink.tsx @@ -51,7 +51,7 @@ export function DocsLink({ title={`Open ${label} (${href})`} className={cn( 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded cursor-pointer', - 'text-[10px] font-mono-tabular text-[color:var(--text-3)]', + 'text-xs tabular-nums text-[color:var(--text-3)]', 'hover:text-[color:var(--text-1)] hover:bg-[color:var(--bg-2)]', 'transition-colors', 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-[color:var(--signal)]', diff --git a/frontend/src/components/atoms/Input.tsx b/frontend/src/components/atoms/Input.tsx index 75910d20..82850abd 100644 --- a/frontend/src/components/atoms/Input.tsx +++ b/frontend/src/components/atoms/Input.tsx @@ -11,13 +11,13 @@ export const Input = forwardRef( (