diff --git a/.gitignore b/.gitignore index 415ccd9..a5e7ade 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,10 @@ tmp/ # Testing artifacts testdata/output/ +# Web dashboard +web/node_modules/ +web/dist/ + # Claude Code settings .claude/settings.local.json result diff --git a/Makefile b/Makefile index d908edc..b21b72c 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,13 @@ PURPLE := \033[0;35m CYAN := \033[0;36m NC := \033[0m # No Color -.PHONY: all build clean clean-all test test-race test-integration \ +# Code signing (macOS) — set via environment or here +CODESIGN_IDENTITY ?= - + +.PHONY: all build build-tui clean clean-all test test-race test-integration \ fmt vet lint install uninstall run demo \ - deps deps-update deps-tidy cross-build release help dev ci info + deps deps-update deps-tidy cross-build release help dev ci info \ + web-deps web-build web-dev web-clean build-signed # Default target all: clean build @@ -53,16 +57,54 @@ help: ## Show this help message @echo " make cross-build # Build for all platforms" @echo " make install # Install to GOPATH/bin" +# Web dashboard targets +web-deps: ## Install web dashboard dependencies + @echo "$(BLUE)Installing web dependencies...$(NC)" + @cd web && rm -rf node_modules && npm ci + @echo "$(GREEN)✓ Web dependencies installed$(NC)" + +web-build: web-deps ## Build web dashboard + @echo "$(BLUE)Building web dashboard...$(NC)" + @cd web && npm run build + @echo "$(GREEN)✓ Web dashboard built$(NC)" + +web-dev: web-deps ## Start web dev server with API proxy + @echo "$(BLUE)Starting web dev server...$(NC)" + @cd web && npm run dev + +web-clean: ## Clean web build artifacts + @echo "$(BLUE)Cleaning web artifacts...$(NC)" + @rm -rf web/dist web/node_modules + @echo "$(GREEN)✓ Web artifacts cleaned$(NC)" + +# Ensure web/dist exists for go:embed (stub if not built) +ensure-web-dist: + @mkdir -p web/dist + @test -f web/dist/index.html || echo '' > web/dist/index.html + # Build targets -build: deps ## Build the TUI binary - @echo "$(BLUE)Building TUI version...$(NC)" +build: deps web-build ## Build binary with embedded web dashboard + @echo "$(BLUE)Building Gonzo with web dashboard...$(NC)" @mkdir -p $(BUILD_DIR) CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) \ $(GO) build $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_DIR) @echo "$(GREEN)✓ Built $(BUILD_DIR)/$(BINARY_NAME)$(NC)" +build-signed: build ## Build and codesign for macOS (set CODESIGN_IDENTITY for distribution) + @echo "$(BLUE)Signing $(BUILD_DIR)/$(BINARY_NAME)...$(NC)" + @codesign --force --options runtime --sign "$(CODESIGN_IDENTITY)" --timestamp $(BUILD_DIR)/$(BINARY_NAME) + @echo "$(GREEN)✓ Signed $(BUILD_DIR)/$(BINARY_NAME)$(NC)" + @codesign -dvv $(BUILD_DIR)/$(BINARY_NAME) 2>&1 | head -5 + +build-tui: deps ensure-web-dist ## Build TUI-only binary (placeholder web dashboard) + @echo "$(BLUE)Building TUI-only version...$(NC)" + @mkdir -p $(BUILD_DIR) + CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) \ + $(GO) build $(BUILD_FLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_DIR) + @echo "$(GREEN)✓ Built $(BUILD_DIR)/$(BINARY_NAME) (TUI only)$(NC)" + # Cross-platform builds -cross-build: clean deps ## Build for multiple platforms +cross-build: clean deps ensure-web-dist ## Build for multiple platforms @echo "$(BLUE)Building for multiple platforms...$(NC)" @mkdir -p $(DIST_DIR) @@ -108,11 +150,11 @@ deps-tidy: ## Clean up dependencies @$(GO) mod tidy # Testing -test: deps ## Run tests +test: deps ensure-web-dist ## Run tests @echo "$(BLUE)Running tests...$(NC)" @$(GO) test -v ./... -test-race: deps ## Run tests with race detection +test-race: deps ensure-web-dist ## Run tests with race detection @echo "$(BLUE)Running tests with race detection...$(NC)" @$(GO) test -race -v ./... @@ -142,7 +184,7 @@ fmt: ## Format code @echo "$(BLUE)Formatting code...$(NC)" @$(GO) fmt ./... -vet: ## Run go vet +vet: ensure-web-dist ## Run go vet @echo "$(BLUE)Running go vet...$(NC)" @$(GO) vet ./... @@ -211,6 +253,7 @@ clean: ## Clean build artifacts @echo "$(BLUE)Cleaning build artifacts...$(NC)" @rm -rf $(BUILD_DIR) @rm -rf $(DIST_DIR) + @rm -rf web/dist @rm -f $(BINARY_NAME) @echo "$(GREEN)✓ Cleaned$(NC)" diff --git a/README.md b/README.md index afd118f..a1de077 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,17 @@ Here are some references to get you started: - **Log Counts analysis** - Detailed modal with heatmap visualization, pattern analysis by severity, and service distribution - **AI analysis** - Get intelligent insights about log patterns and anomalies with configurable models +### 🌐 Web Dashboard (Dstl8 Lite) + +- **Embedded React UI** - Full web dashboard served directly from the Gonzo binary (no external dependencies) +- **Real-time updates** - WebSocket-powered live streaming with 1-second refresh +- **Severity distribution** - Interactive time-series severity charts with stream-level filtering +- **Sentiment heatmap** - Color-coded heatmap visualization grouped by pod, namespace, service, host, or deployment +- **Pattern analysis** - Drain3-powered log pattern detection and classification +- **Log viewer** - Searchable, auto-scrolling log viewer with click-to-expand details +- **Source browser** - Explore log sources with dimension breakdowns +- **Light/dark mode** - Automatic theme support + ### 🔍 Advanced Filtering - **Regex support** - Filter logs with regular expressions @@ -306,6 +317,30 @@ export OPENAI_API_KEY=sk-your-key-here cat logs.json | gonzo --ai-model="gpt-4" ``` +### Web Dashboard (Dstl8 Lite) + +Gonzo includes an embedded web dashboard that runs alongside the TUI. It starts automatically on port 5718. + +```bash +# Gonzo starts the web dashboard automatically +gonzo -f application.log --follow +# Open http://localhost:5718 in your browser + +# Use a custom port +gonzo -f application.log --web-port=3000 + +# Disable the web dashboard +gonzo -f application.log --web-disabled +``` + +The dashboard includes: +- **Workspaces** - Overview of all active log streams with sparkline previews +- **Stream Details** - Severity distribution, top attributes, pattern analysis, and live log viewer per stream +- **Sentiment Heatmap** - Real-time heatmap grouped by pod, namespace, service, host, or deployment with auto-detection of available dimensions +- **Sources** - Browse log sources with dimension breakdowns + +All data updates in real-time via WebSocket, matching what you see in the TUI. + ### Keyboard Shortcuts #### Navigation @@ -452,6 +487,10 @@ Flags: -s, --skin string Color scheme/skin to use (default, or name of a skin file) --stop-words strings Additional stop words to filter out from analysis (adds to built-in list) +Web Dashboard Flags: + --web-port int Port for the Dstl8 Lite web dashboard (default: 5718) + --web-disabled Disable the web dashboard + Kubernetes Flags: --k8s-enabled=true Enable Kubernetes log streaming mode --k8s-namespaces stringArray Kubernetes namespace(s) to watch (can specify multiple, default: all) @@ -501,6 +540,10 @@ test-mode: false # AI configuration ai-provider: "openai" # Options: "openai" (default), "claude-code" ai-model: "gpt-4" + +# Web dashboard (Dstl8 Lite) +web-port: 5718 # Port for the web dashboard +web-disabled: false # Set to true to disable ``` See [examples/config.yml](examples/config.yml) for a complete configuration example with detailed comments. @@ -689,6 +732,8 @@ When you don't specify the `--ai-model` flag, Gonzo automatically selects the be | `GONZO_LOG_BUFFER` | Override log buffer size | | `GONZO_MEMORY_SIZE` | Override memory size | | `GONZO_AI_MODEL` | Override default AI model | +| `GONZO_WEB_PORT` | Override web dashboard port (default: 5718) | +| `GONZO_WEB_DISABLED` | Disable web dashboard (true/false) | | `GONZO_TEST_MODE` | Enable test mode | | `NO_COLOR` | Disable colored output | @@ -760,16 +805,19 @@ Gonzo is built with: - **OpenTelemetry** - Native OTLP support - **Large amounts of** ☕️ -The architecture follows a clean separation: +The architecture follows a clean separation with a shared analysis engine: ``` cmd/gonzo/ # Main application entry internal/ -├── tui/ # Terminal UI implementation -├── analyzer/ # Log analysis engine +├── engine/ # Shared analysis engine (feeds TUI and web) +├── tui/ # Terminal UI implementation +├── web/ # Dstl8 Lite web dashboard (embedded React) +├── analyzer/ # Log analysis engine ├── memory/ # Frequency tracking ├── otlplog/ # OTLP format handling └── ai/ # AI integration +web/ # React frontend source (Vite + TypeScript) ``` ## 🧪 Development @@ -782,9 +830,13 @@ internal/ ### Building ```bash -# Quick build +# Quick build (includes web dashboard) make build +# Build web dashboard only +make web-deps # Install npm dependencies +make web-build # Build React app + # Run tests make test @@ -879,6 +931,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - [AWS CloudWatch Logs Usage Guide](guides/CLOUDWATCH_USAGE_GUIDE.md) - Usage instructions for AWS CLI log tail and live tail with Gonzo - [Stern Usage Guide](guides/STERN_USAGE_GUIDE.md) - Usage and examples for using Stern with Gonzo - [Victoria Logs Integration](guides/VICTORIA_LOGS_USAGE.md) - Using Gonzo with Victoria Logs API +- [Web Dashboard](guides/WEB_DASHBOARD_USAGE.md) - Dstl8 Lite web UI setup - [Contributing Guide](CONTRIBUTING.md) - How to contribute - [Changelog](CHANGELOG.md) - Version history diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md index d4c45ac..33fb55d 100644 --- a/USAGE_GUIDE.md +++ b/USAGE_GUIDE.md @@ -143,8 +143,12 @@ cat test.log | ./build/gonzo --reverse-scroll-wheel # Reverse scroll wheel direction (natural scrolling) --config string # Config file (default: ~/.gonzo.yaml) +# Web dashboard (Dstl8 Lite) + --web-port=5718 # Port for the web dashboard + --web-disabled # Disable the web dashboard + # Version and help --v, --version # Show version information +-v, --version # Show version information -h, --help # Show help message # Commands @@ -273,6 +277,44 @@ This affects all scrollable areas: - **Consistency**: Keep same scroll direction across all applications - **Personal preference**: Use whichever feels more intuitive to you +## Web Dashboard (Dstl8 Lite) + +Gonzo includes an embedded web dashboard that starts automatically alongside the TUI. + +### Accessing the Dashboard + +```bash +# Start Gonzo — web dashboard is available at http://localhost:5718 +./build/gonzo -f test.log --follow + +# Custom port +./build/gonzo -f test.log --web-port=3000 + +# Disable the web dashboard +./build/gonzo -f test.log --web-disabled +``` + +Open `http://localhost:5718` in your browser to see: +- **Workspaces** - Overview of active log streams with live sparklines +- **Stream Details** - Severity distribution, top attributes, pattern analysis, and live log viewer +- **Sentiment Heatmap** - Real-time heatmap grouped by pod, namespace, service, host, or deployment +- **Sources** - Browse log sources and their dimensions + +The dashboard updates in real-time via WebSocket — the same data that powers the TUI. + +### Building the Web Dashboard + +The web dashboard is built with Vite + React + TypeScript and embedded into the Go binary. + +```bash +# Install dependencies and build (included in make build) +make web-deps +make web-build + +# Full build including web dashboard +make build +``` + ### Supported Integrations - [Victoria Logs Integration](guides/VICTORIA_LOGS_USAGE.md) - Using Gonzo with Victoria Logs API diff --git a/cmd/gonzo/app.go b/cmd/gonzo/app.go index 7939c69..eeaa0e1 100644 --- a/cmd/gonzo/app.go +++ b/cmd/gonzo/app.go @@ -12,7 +12,12 @@ import ( "strings" "time" + "io/fs" + "github.com/control-theory/gonzo/internal/analyzer" + "github.com/control-theory/gonzo/internal/engine" + "github.com/control-theory/gonzo/internal/releases" + "github.com/control-theory/gonzo/internal/state" "github.com/control-theory/gonzo/internal/filereader" "github.com/control-theory/gonzo/internal/formats" "github.com/control-theory/gonzo/internal/k8s" @@ -22,6 +27,8 @@ import ( "github.com/control-theory/gonzo/internal/tui" versioncheck "github.com/control-theory/gonzo/internal/version" "github.com/control-theory/gonzo/internal/vmlogs" + gonzoweb "github.com/control-theory/gonzo/internal/web" + webembed "github.com/control-theory/gonzo/web" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" @@ -108,6 +115,23 @@ func runApp(cmd *cobra.Command, args []string) error { if versionChecker != nil { dashboard.SetVersionChecker(versionChecker) } + if !cfg.WebDisabled { + dashboard.SetWebPort(cfg.WebPort) + } + + // What's New: fetch GitHub releases in background and wire up auto-show + currentVersion, _ := GetVersionInfo() + relFetcher := releases.NewFetcher() + if currentVersion != "dev" && currentVersion != "" && !cfg.DisableVersionCheck { + relFetcher.FetchInBackground() + } + appState := state.Load() + dashboard.SetReleasesFetcher(relFetcher) + dashboard.SetCurrentVersion(currentVersion) + dashboard.SetLastSeenVersion(appState.LastSeenVersion) + + // Create shared analysis engine (used by web dashboard) + eng := engine.NewEngine(cfg.LogBuffer, textAnalyzer.GetStopWords(), nil, cfg.UseLogTime) tuiModel := &simpleTuiModel{ formatDetector: formatDetector, @@ -117,6 +141,7 @@ func runApp(cmd *cobra.Command, args []string) error { otlpAnalyzer: otlpAnalyzer, freqMemory: freqMemory, dashboard: dashboard, + engine: eng, updateInterval: cfg.UpdateInterval, testMode: cfg.TestMode, versionChecker: versionChecker, @@ -138,6 +163,19 @@ func runApp(cmd *cobra.Command, args []string) error { tuiModel.ctx = ctx tuiModel.cancelFunc = cancel + // Start web dashboard in background (if not disabled) + if !cfg.WebDisabled { + // Get embedded web dashboard assets + var staticFS fs.FS + if distFS, err := fs.Sub(webembed.DistFS, "dist"); err == nil { + staticFS = distFS + } + webSrv := gonzoweb.NewServer(eng, staticFS, currentVersion, relFetcher) + if err := webSrv.Start(ctx, cfg.WebPort); err != nil { + log.Printf("Warning: web dashboard failed to start: %v", err) + } + } + if _, err := p.Run(); err != nil { if strings.Contains(err.Error(), "TTY") || strings.Contains(err.Error(), "/dev/tty") { return fmt.Errorf("TUI requires a real terminal. Try --test-mode for non-interactive testing") @@ -168,6 +206,7 @@ type simpleTuiModel struct { otlpAnalyzer *analyzer.OTLPAnalyzer freqMemory *memory.FrequencyMemory dashboard *tui.DashboardModel + engine *engine.Engine // Shared analysis state for web dashboard updateInterval time.Duration testMode bool ctx context.Context @@ -633,6 +672,11 @@ func (m *simpleTuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Send periodic snapshot with current count (even if 0) snapshot := m.freqMemory.GetSnapshot() + // Feed snapshot to the shared engine + if m.engine != nil { + m.engine.UpdateFrequencySnapshot(snapshot) + } + // No automatic reset - only manual reset via 'r' key now shouldReset := false @@ -644,6 +688,11 @@ func (m *simpleTuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { ResetDrain3: shouldReset, // Reset drain3 when frequency memory resets } + // Feed interval counts to the shared engine + if m.engine != nil && m.severityCounts != nil { + m.engine.IngestSeverityCounts(*m.severityCounts) + } + // Reset severity counts for next interval m.severityCounts = &tui.SeverityCounts{} m.logCount = 0 @@ -783,6 +832,7 @@ func systemLogPath() string { case "darwin": return "/var/log/system.log" case "linux": + // Try syslog first, then messages for _, p := range []string{"/var/log/syslog", "/var/log/messages"} { if _, err := os.Stat(p); err == nil { return p diff --git a/cmd/gonzo/main.go b/cmd/gonzo/main.go index ae05aee..c924626 100644 --- a/cmd/gonzo/main.go +++ b/cmd/gonzo/main.go @@ -55,6 +55,10 @@ type Config struct { DisableVersionCheck bool `mapstructure:"disable-version-check"` ReverseScrollWheel bool `mapstructure:"reverse-scroll-wheel"` UseLogTime bool `mapstructure:"use-log-time"` + + // Web dashboard (Dstl8 Lite) + WebPort int `mapstructure:"web-port"` + WebDisabled bool `mapstructure:"web-disabled"` } var ( @@ -186,6 +190,10 @@ func init() { rootCmd.Flags().Bool("reverse-scroll-wheel", false, "Reverse scroll wheel direction (natural scrolling)") rootCmd.Flags().Bool("use-log-time", false, "Use original log timestamps instead of receive time for heatmap and display (falls back to receive time if log has no timestamp)") + // Dstl8 Lite web dashboard flags + rootCmd.Flags().Int("web-port", 5718, "Port for the Dstl8 Lite web dashboard") + rootCmd.Flags().Bool("web-disabled", false, "Disable the web dashboard") + // Bind flags to viper viper.BindPFlag("memory-size", rootCmd.Flags().Lookup("memory-size")) viper.BindPFlag("update-interval", rootCmd.Flags().Lookup("update-interval")) @@ -215,7 +223,8 @@ func init() { viper.BindPFlag("disable-version-check", rootCmd.Flags().Lookup("disable-version-check")) viper.BindPFlag("reverse-scroll-wheel", rootCmd.Flags().Lookup("reverse-scroll-wheel")) viper.BindPFlag("use-log-time", rootCmd.Flags().Lookup("use-log-time")) - + viper.BindPFlag("web-port", rootCmd.Flags().Lookup("web-port")) + viper.BindPFlag("web-disabled", rootCmd.Flags().Lookup("web-disabled")) // Add version command rootCmd.AddCommand(versionCmd) } diff --git a/cmd/gonzo/processing.go b/cmd/gonzo/processing.go index ee8ecea..0d11de2 100644 --- a/cmd/gonzo/processing.go +++ b/cmd/gonzo/processing.go @@ -151,6 +151,11 @@ func (m *simpleTuiModel) processSingleLogEntry(result *analyzer.AnalysisResult, // Count severity for this interval m.severityCounts.AddCount(logEntry.Severity) + // Feed the shared analysis engine (for web dashboard) + if m.engine != nil { + m.engine.Ingest(*logEntry) + } + updateMsg := tui.UpdateMsg{NewLogEntry: logEntry} m.dashboard.Update(updateMsg) } diff --git a/flake.nix b/flake.nix index 73cc30e..7422e9a 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,13 @@ src = ./.; modules = ./gomod2nix.toml; - # If you split binaries later, enable: subPackages = [ "cmd/gonzo" ]; + # Create stub web/dist so //go:embed all:dist compiles + # Full web dashboard is built separately via `make build` + preBuild = '' + mkdir -p web/dist + echo '' > web/dist/index.html + ''; + ldflags = ["-s" "-w"]; meta = with pkgs.lib; { diff --git a/go.mod b/go.mod index 36039bf..e591ca8 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/NimbleMarkets/ntcharts v0.3.1 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/glamour v1.0.0 github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 github.com/fsnotify/fsnotify v1.9.0 github.com/jaeyo/go-drain3 v0.1.2 @@ -19,16 +20,21 @@ require ( k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 k8s.io/klog/v2 v2.130.1 + nhooyr.io/websocket v1.8.17 ) require ( + github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/ansi v0.10.2 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -41,21 +47,24 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect @@ -71,6 +80,8 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.7.13 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 669debe..122fc03 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,20 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/NimbleMarkets/ntcharts v0.3.1 h1:EH4O80RMy5rqDmZM7aWjTbCSuRDDJ5fXOv/qAzdwOjk= github.com/NimbleMarkets/ntcharts v0.3.1/go.mod h1:zVeRqYkh2n59YPe1bflaSL4O2aD2ZemNmrbdEqZ70hk= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= @@ -16,14 +24,18 @@ github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGs github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= +github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -31,6 +43,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -70,10 +84,14 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jaeyo/go-drain3 v0.1.2 h1:fY21wgbwhzzaoRNSQ+6HVbpYw4KkAYjCFCoERYozIJ8= @@ -93,16 +111,19 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e h1:OLwZ8xVaeVrru0xyeuOX+fne0gQTFEGlzfNjipCbxlU= github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e/go.mod h1:NQ34EGeu8FAYGBMDzwhfNJL8YQYoWZP5xYJPRDAwN3E= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= +github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -113,6 +134,8 @@ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -127,6 +150,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -166,6 +190,10 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= +github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= @@ -265,6 +293,8 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/gomod2nix.toml b/gomod2nix.toml index 88dcf54..0006913 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -1,261 +1,386 @@ schema = 3 [mod] - [mod."github.com/NimbleMarkets/ntcharts"] - version = "v0.3.1" - hash = "sha256-ZybI8VrsxgK890OYFJazgFnNR6bGi4suowVHd1+XJlM=" - [mod."github.com/atotto/clipboard"] - version = "v0.1.4" - hash = "sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk=" - [mod."github.com/aymanbagabas/go-osc52/v2"] - version = "v2.0.1" - hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=" - [mod."github.com/charmbracelet/bubbles"] - version = "v0.21.0" - hash = "sha256-cfjUHgy9eq5SretTHuYuRaeeT6QmJYQBB9dsI8QSnW0=" - [mod."github.com/charmbracelet/bubbletea"] - version = "v1.3.6" - hash = "sha256-79MpQk+92RDXnNc2hSct8EDf0AhV6MZpvFMmmrVC/bs=" - [mod."github.com/charmbracelet/colorprofile"] - version = "v0.2.3-0.20250311203215-f60798e515dc" - hash = "sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q=" - [mod."github.com/charmbracelet/lipgloss"] - version = "v1.1.1-0.20250404203927-76690c660834" - hash = "sha256-CwS1AyoRsPOFA4SrC0CTtwLnbg57FmmSW/mvVQUFYcc=" - [mod."github.com/charmbracelet/x/ansi"] - version = "v0.9.3" - hash = "sha256-Bzum17p7UQZeNxL155Pho/+GXj1DElB9Bp3O194CYf8=" - [mod."github.com/charmbracelet/x/cellbuf"] - version = "v0.0.13" - hash = "sha256-ubgBd82jcS5L4i6rCFLOB1BXGrDEu6wWlciJA0gsH2g=" - [mod."github.com/charmbracelet/x/term"] - version = "v0.2.1" - hash = "sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM=" - [mod."github.com/davecgh/go-spew"] - version = "v1.1.1" - hash = "sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=" - [mod."github.com/emicklei/go-restful/v3"] - version = "v3.12.2" - hash = "sha256-eQ0qtVH7c5jgqB7F9B17GhZujYelBA2g9KwpPuSS0sE=" - [mod."github.com/erikgeiser/coninput"] - version = "v0.0.0-20211004153227-1c3628e74d0f" - hash = "sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU=" - [mod."github.com/fsnotify/fsnotify"] - version = "v1.9.0" - hash = "sha256-WtpE1N6dpHwEvIub7Xp/CrWm0fd6PX7MKA4PV44rp2g=" - [mod."github.com/fxamacker/cbor/v2"] - version = "v2.9.0" - hash = "sha256-/IZK76MRCrz9XCiilieH5tKaLnIWyPJhwxDoVKB8dFc=" - [mod."github.com/go-logr/logr"] - version = "v1.4.3" - hash = "sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA=" - [mod."github.com/go-openapi/jsonpointer"] - version = "v0.21.0" - hash = "sha256-bB8XTzo4hzXemi8Ey3tIXia3mfn38bvwIzKYLJYC650=" - [mod."github.com/go-openapi/jsonreference"] - version = "v0.20.2" - hash = "sha256-klWZKK7LZqSg3HMIrSkjh/NwaZTr+8kTW2ok2+JlioE=" - [mod."github.com/go-openapi/swag"] - version = "v0.23.0" - hash = "sha256-D5CzsSQ3SYJLwXT6BDahnG66LI8du59Dy1mY4KutA7A=" - [mod."github.com/go-viper/mapstructure/v2"] - version = "v2.4.0" - hash = "sha256-lLfcV9z4n94hDhgyXJlde4bFB0hfzlbh+polqcJCwGE=" - [mod."github.com/gogo/protobuf"] - version = "v1.3.2" - hash = "sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c=" - [mod."github.com/google/gnostic-models"] - version = "v0.7.0" - hash = "sha256-sxShRxqOUVlz9IkAz0C/NP/7CmLBW3ffwoZTdh+rIOc=" - [mod."github.com/google/go-cmp"] - version = "v0.7.0" - hash = "sha256-JbxZFBFGCh/Rj5XZ1vG94V2x7c18L8XKB0N9ZD5F2rM=" - [mod."github.com/google/uuid"] - version = "v1.6.0" - hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=" - [mod."github.com/grpc-ecosystem/grpc-gateway/v2"] - version = "v2.26.3" - hash = "sha256-j/nyE8OgiwZ1YP0h3gmmxY1Fx4GDMCohi9g9kgNz4U8=" - [mod."github.com/hashicorp/golang-lru/v2"] - version = "v2.0.7" - hash = "sha256-t1bcXLgrQNOYUVyYEZ0knxcXpsTk4IuJZDjKvyJX75g=" - [mod."github.com/inconshreveable/mousetrap"] - version = "v1.1.0" - hash = "sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=" - [mod."github.com/jaeyo/go-drain3"] - version = "v0.1.2" - hash = "sha256-mBQensNJV8/yKb8cqJTk7QG9B8PwN3NoZcWmvSv3vPo=" - [mod."github.com/josharian/intern"] - version = "v1.0.0" - hash = "sha256-LJR0QE2vOQ2/2shBbO5Yl8cVPq+NFrE3ers0vu9FRP0=" - [mod."github.com/json-iterator/go"] - version = "v1.1.12" - hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM=" - [mod."github.com/lrstanley/bubblezone"] - version = "v0.0.0-20240914071701-b48c55a5e78e" - hash = "sha256-FoYUk7NlM5r1yYuhVPVSj72dWVHX1gb6v8Doqw0jx3I=" - [mod."github.com/lucasb-eyer/go-colorful"] - version = "v1.2.0" - hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE=" - [mod."github.com/mailru/easyjson"] - version = "v0.7.7" - hash = "sha256-NVCz8MURpxgOjHXqxOZExqV4bnpHggpeAOyZDArjcy4=" - [mod."github.com/mattn/go-isatty"] - version = "v0.0.20" - hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" - [mod."github.com/mattn/go-localereader"] - version = "v0.0.1" - hash = "sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E=" - [mod."github.com/mattn/go-runewidth"] - version = "v0.0.16" - hash = "sha256-NC+ntvwIpqDNmXb7aixcg09il80ygq6JAnW0Gb5b/DQ=" - [mod."github.com/modern-go/concurrent"] - version = "v0.0.0-20180306012644-bacd9c7ef1dd" - hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo=" - [mod."github.com/modern-go/reflect2"] - version = "v1.0.3-0.20250322232337-35a7c28c31ee" - hash = "sha256-0pkWWZRB3lGFyzmlxxrm0KWVQo9HNXNafaUu3k+rE1g=" - [mod."github.com/muesli/ansi"] - version = "v0.0.0-20230316100256-276c6243b2f6" - hash = "sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs=" - [mod."github.com/muesli/cancelreader"] - version = "v0.2.2" - hash = "sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ=" - [mod."github.com/muesli/termenv"] - version = "v0.16.0" - hash = "sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI=" - [mod."github.com/munnerz/goautoneg"] - version = "v0.0.0-20191010083416-a7dc8b61c822" - hash = "sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q=" - [mod."github.com/pelletier/go-toml/v2"] - version = "v2.2.3" - hash = "sha256-fE++SVgnCGdnFZoROHWuYjIR7ENl7k9KKxQrRTquv/o=" - [mod."github.com/pkg/errors"] - version = "v0.9.1" - hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=" - [mod."github.com/pmezard/go-difflib"] - version = "v1.0.0" - hash = "sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA=" - [mod."github.com/rivo/uniseg"] - version = "v0.4.7" - hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=" - [mod."github.com/sagikazarmark/locafero"] - version = "v0.7.0" - hash = "sha256-ZmaGOKHDw18jJqdkwQwSpUT11F9toR6KPs3241TONeY=" - [mod."github.com/sourcegraph/conc"] - version = "v0.3.0" - hash = "sha256-mIdMs9MLAOBKf3/0quf1iI3v8uNWydy7ae5MFa+F2Ko=" - [mod."github.com/spf13/afero"] - version = "v1.12.0" - hash = "sha256-TX3DcyAdrXqf+TxmEz4TilWQo2Y4hcBXOeRY6BjDp+s=" - [mod."github.com/spf13/cast"] - version = "v1.7.1" - hash = "sha256-BjX0aY/PC37gIdMc7JhMgvhWFsksGdAcp2FgzpuvkPo=" - [mod."github.com/spf13/cobra"] - version = "v1.9.1" - hash = "sha256-dzEqquABE3UqZmJuj99244QjvfojS8cFlsPr/MXQGj0=" - [mod."github.com/spf13/pflag"] - version = "v1.0.6" - hash = "sha256-NjrK0FZPIfO/p2xtL1J7fOBQNTZAPZOC6Cb4aMMvhxI=" - [mod."github.com/spf13/viper"] - version = "v1.20.1" - hash = "sha256-gbCM0k7RAlvn7jpSuWB2LX5Nip9vgwsPNGbDXTI7JvM=" - [mod."github.com/subosito/gotenv"] - version = "v1.6.0" - hash = "sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A=" - [mod."github.com/x448/float16"] - version = "v0.8.4" - hash = "sha256-VKzMTMS9pIB/cwe17xPftCSK9Mf4Y6EuBEJlB4by5mE=" - [mod."github.com/xo/terminfo"] - version = "v0.0.0-20220910002029-abceb7e1c41e" - hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=" - [mod."go.opentelemetry.io/otel"] - version = "v1.37.0" - hash = "sha256-zWpyp9K8/Te86uhNjamchZctTdAnmHhoVw9m4ACfSoo=" - [mod."go.opentelemetry.io/otel/sdk/metric"] - version = "v1.37.0" - hash = "sha256-dm6Aa5UDFgQCVexayiWu85A1ir1lpNn5rIN9tj9KVDs=" - [mod."go.opentelemetry.io/proto/otlp"] - version = "v1.7.0" - hash = "sha256-wqmUA6XGjODfuTCRQs32tYFkvO717ZGyZYZFcbNHvLw=" - [mod."go.uber.org/multierr"] - version = "v1.11.0" - hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0=" - [mod."go.yaml.in/yaml/v2"] - version = "v2.4.2" - hash = "sha256-oC8RWdf1zbMYCtmR0ATy/kCkhIwPR9UqFZSMOKLVF/A=" - [mod."go.yaml.in/yaml/v3"] - version = "v3.0.4" - hash = "sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4=" - [mod."golang.org/x/net"] - version = "v0.43.0" - hash = "sha256-bf3iQFrsC8BoarVaS0uSspEFAcr1zHp1uziTtBpwV34=" - [mod."golang.org/x/oauth2"] - version = "v0.30.0" - hash = "sha256-btD7BUtQpOswusZY5qIU90uDo38buVrQ0tmmQ8qNHDg=" - [mod."golang.org/x/sync"] - version = "v0.16.0" - hash = "sha256-sqKDRESeMzLe0jWGWltLZL/JIgrn0XaIeBWCzVN3Bks=" - [mod."golang.org/x/sys"] - version = "v0.35.0" - hash = "sha256-ZKM8pesQE6NAFZeKQ84oPn5JMhGr8g4TSwLYAsHMGSI=" - [mod."golang.org/x/term"] - version = "v0.34.0" - hash = "sha256-faLolF6EUSSaC0ZwRiKH5JF/TmtcMQ+m+RWWl6Pk1PU=" - [mod."golang.org/x/text"] - version = "v0.28.0" - hash = "sha256-8UlJniGK+km4Hmrw6XMxELnExgrih7+z8tU26Cntmto=" - [mod."golang.org/x/time"] - version = "v0.9.0" - hash = "sha256-ipaWVIk1+DZg0rfCzBSkz/Y6DEnB7xkX2RRYycHkhC0=" - [mod."google.golang.org/genproto/googleapis/api"] - version = "v0.0.0-20250528174236-200df99c418a" - hash = "sha256-VO7Rko8b/zO2sm6vML7hhxi9laPilt6JEab8xl4qIN8=" - [mod."google.golang.org/genproto/googleapis/rpc"] - version = "v0.0.0-20250811230008-5f3141c8851a" - hash = "sha256-hbGMdlN/vwPIOJhYv6CAEnpQqTXbQ1GlXabiQUOv3sc=" - [mod."google.golang.org/grpc"] - version = "v1.74.2" - hash = "sha256-tvYMdfu/ZQZRPZNmnQI4CZpg46CM8+mD49hw0gFheGs=" - [mod."google.golang.org/protobuf"] - version = "v1.36.7" - hash = "sha256-6xCU+t2AVPcscMKenVs4etGqutYGPDXCQ3DCD3PpTq4=" - [mod."gopkg.in/evanphx/json-patch.v4"] - version = "v4.12.0" - hash = "sha256-rUOokb3XW30ftpHp0fsF2WiJln1S0FSt2El7fTHq3CM=" - [mod."gopkg.in/inf.v0"] - version = "v0.9.1" - hash = "sha256-z84XlyeWLcoYOvWLxPkPFgLkpjyb2Y4pdeGMyySOZQI=" - [mod."gopkg.in/yaml.v3"] - version = "v3.0.1" - hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=" - [mod."k8s.io/api"] - version = "v0.34.2" - hash = "sha256-8TpPB07Dh6REOxYzT78OGLCV1iTZlMVzEQCzmpA4Q3U=" - [mod."k8s.io/apimachinery"] - version = "v0.34.2" - hash = "sha256-y6GVugdaX81lHwVWgDgFoIi/v6K9A3YC2kKlGUritVU=" - [mod."k8s.io/client-go"] - version = "v0.34.2" - hash = "sha256-ELhaXsMrieNn0ms84n5kL9QlNDXQ2u8xpbny+bTvdZg=" - [mod."k8s.io/klog/v2"] - version = "v2.130.1" - hash = "sha256-n5vls1o1a0V0KYv+3SULq4q3R2Is15K8iDHhFlsSH4o=" - [mod."k8s.io/kube-openapi"] - version = "v0.0.0-20250710124328-f3f2b991d03b" - hash = "sha256-0v0MN67pora3UCA8vXSJDgkosx/1RaB2HkjUnwdVLbY=" - [mod."k8s.io/utils"] - version = "v0.0.0-20250604170112-4c0f3b243397" - hash = "sha256-USPKRYYKfbhQWU0CgT/8V1hdBirWjmhTZvJCuuUlNFo=" - [mod."sigs.k8s.io/json"] - version = "v0.0.0-20241014173422-cfa47c3a1cc8" - hash = "sha256-dkegDkyjp/niYirIdhbQrBYt/uttCZQAfsBzKSzOMh0=" - [mod."sigs.k8s.io/randfill"] - version = "v1.0.0" - hash = "sha256-xldQxDwW84hmlihdSOFfjXyauhxEWV9KmIDLZMTcYNo=" - [mod."sigs.k8s.io/structured-merge-diff/v6"] - version = "v6.3.0" - hash = "sha256-2EqUZSaHUhwTrdjoZuv+Z99tZYrX1E6rxf2ejeKd2BM=" - [mod."sigs.k8s.io/yaml"] - version = "v1.6.0" - hash = "sha256-49hg7IVPzwxeovp+HTMiWa/10NMMTSTjAdCmIv6p9dw=" + [mod.'github.com/NimbleMarkets/ntcharts'] + version = 'v0.3.1' + hash = 'sha256-ZybI8VrsxgK890OYFJazgFnNR6bGi4suowVHd1+XJlM=' + + [mod.'github.com/alecthomas/chroma/v2'] + version = 'v2.20.0' + hash = 'sha256-rL1mJS8fCltcii9w/efyyE4iyzogyjP+N80lE3CuzVE=' + + [mod.'github.com/atotto/clipboard'] + version = 'v0.1.4' + hash = 'sha256-ZZ7U5X0gWOu8zcjZcWbcpzGOGdycwq0TjTFh/eZHjXk=' + + [mod.'github.com/aymanbagabas/go-osc52/v2'] + version = 'v2.0.1' + hash = 'sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg=' + + [mod.'github.com/aymerick/douceur'] + version = 'v0.2.0' + hash = 'sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE=' + + [mod.'github.com/charmbracelet/bubbles'] + version = 'v0.21.0' + hash = 'sha256-cfjUHgy9eq5SretTHuYuRaeeT6QmJYQBB9dsI8QSnW0=' + + [mod.'github.com/charmbracelet/bubbletea'] + version = 'v1.3.6' + hash = 'sha256-79MpQk+92RDXnNc2hSct8EDf0AhV6MZpvFMmmrVC/bs=' + + [mod.'github.com/charmbracelet/colorprofile'] + version = 'v0.2.3-0.20250311203215-f60798e515dc' + hash = 'sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q=' + + [mod.'github.com/charmbracelet/glamour'] + version = 'v1.0.0' + hash = 'sha256-1sGgVA3QXJ2P5c/DNMb7R0HFG1X5vz9nmDhLrzKdgMw=' + + [mod.'github.com/charmbracelet/lipgloss'] + version = 'v1.1.1-0.20250404203927-76690c660834' + hash = 'sha256-CwS1AyoRsPOFA4SrC0CTtwLnbg57FmmSW/mvVQUFYcc=' + + [mod.'github.com/charmbracelet/x/ansi'] + version = 'v0.10.2' + hash = 'sha256-+JMWFbgv/5Wl2UERKxY4p5oayCRgJEDZpt2ACu3oG1o=' + + [mod.'github.com/charmbracelet/x/cellbuf'] + version = 'v0.0.13' + hash = 'sha256-ubgBd82jcS5L4i6rCFLOB1BXGrDEu6wWlciJA0gsH2g=' + + [mod.'github.com/charmbracelet/x/exp/slice'] + version = 'v0.0.0-20250327172914-2fdc97757edf' + hash = 'sha256-C1tksnevc/RdytJRQg5LQ0+VVSWlTwbNGic649m6E1Q=' + + [mod.'github.com/charmbracelet/x/term'] + version = 'v0.2.1' + hash = 'sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM=' + + [mod.'github.com/davecgh/go-spew'] + version = 'v1.1.1' + hash = 'sha256-nhzSUrE1fCkN0+RL04N4h8jWmRFPPPWbCuDc7Ss0akI=' + + [mod.'github.com/dlclark/regexp2'] + version = 'v1.11.5' + hash = 'sha256-jN5+2ED+YbIoPIuyJ4Ou5pqJb2w1uNKzp5yTjKY6rEQ=' + + [mod.'github.com/emicklei/go-restful/v3'] + version = 'v3.12.2' + hash = 'sha256-eQ0qtVH7c5jgqB7F9B17GhZujYelBA2g9KwpPuSS0sE=' + + [mod.'github.com/erikgeiser/coninput'] + version = 'v0.0.0-20211004153227-1c3628e74d0f' + hash = 'sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU=' + + [mod.'github.com/fsnotify/fsnotify'] + version = 'v1.9.0' + hash = 'sha256-WtpE1N6dpHwEvIub7Xp/CrWm0fd6PX7MKA4PV44rp2g=' + + [mod.'github.com/fxamacker/cbor/v2'] + version = 'v2.9.0' + hash = 'sha256-/IZK76MRCrz9XCiilieH5tKaLnIWyPJhwxDoVKB8dFc=' + + [mod.'github.com/go-logr/logr'] + version = 'v1.4.3' + hash = 'sha256-Nnp/dEVNMxLp3RSPDHZzGbI8BkSNuZMX0I0cjWKXXLA=' + + [mod.'github.com/go-openapi/jsonpointer'] + version = 'v0.21.0' + hash = 'sha256-bB8XTzo4hzXemi8Ey3tIXia3mfn38bvwIzKYLJYC650=' + + [mod.'github.com/go-openapi/jsonreference'] + version = 'v0.20.2' + hash = 'sha256-klWZKK7LZqSg3HMIrSkjh/NwaZTr+8kTW2ok2+JlioE=' + + [mod.'github.com/go-openapi/swag'] + version = 'v0.23.0' + hash = 'sha256-D5CzsSQ3SYJLwXT6BDahnG66LI8du59Dy1mY4KutA7A=' + + [mod.'github.com/go-viper/mapstructure/v2'] + version = 'v2.4.0' + hash = 'sha256-lLfcV9z4n94hDhgyXJlde4bFB0hfzlbh+polqcJCwGE=' + + [mod.'github.com/gogo/protobuf'] + version = 'v1.3.2' + hash = 'sha256-pogILFrrk+cAtb0ulqn9+gRZJ7sGnnLLdtqITvxvG6c=' + + [mod.'github.com/google/gnostic-models'] + version = 'v0.7.0' + hash = 'sha256-sxShRxqOUVlz9IkAz0C/NP/7CmLBW3ffwoZTdh+rIOc=' + + [mod.'github.com/google/go-cmp'] + version = 'v0.7.0' + hash = 'sha256-JbxZFBFGCh/Rj5XZ1vG94V2x7c18L8XKB0N9ZD5F2rM=' + + [mod.'github.com/google/uuid'] + version = 'v1.6.0' + hash = 'sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=' + + [mod.'github.com/gorilla/css'] + version = 'v1.0.1' + hash = 'sha256-6JwNHqlY2NpZ0pSQTyYPSpiNqjXOdFHqrUT10sv3y8A=' + + [mod.'github.com/grpc-ecosystem/grpc-gateway/v2'] + version = 'v2.26.3' + hash = 'sha256-j/nyE8OgiwZ1YP0h3gmmxY1Fx4GDMCohi9g9kgNz4U8=' + + [mod.'github.com/hashicorp/golang-lru/v2'] + version = 'v2.0.7' + hash = 'sha256-t1bcXLgrQNOYUVyYEZ0knxcXpsTk4IuJZDjKvyJX75g=' + + [mod.'github.com/inconshreveable/mousetrap'] + version = 'v1.1.0' + hash = 'sha256-XWlYH0c8IcxAwQTnIi6WYqq44nOKUylSWxWO/vi+8pE=' + + [mod.'github.com/jaeyo/go-drain3'] + version = 'v0.1.2' + hash = 'sha256-mBQensNJV8/yKb8cqJTk7QG9B8PwN3NoZcWmvSv3vPo=' + + [mod.'github.com/josharian/intern'] + version = 'v1.0.0' + hash = 'sha256-LJR0QE2vOQ2/2shBbO5Yl8cVPq+NFrE3ers0vu9FRP0=' + + [mod.'github.com/json-iterator/go'] + version = 'v1.1.12' + hash = 'sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM=' + + [mod.'github.com/lrstanley/bubblezone'] + version = 'v0.0.0-20240914071701-b48c55a5e78e' + hash = 'sha256-FoYUk7NlM5r1yYuhVPVSj72dWVHX1gb6v8Doqw0jx3I=' + + [mod.'github.com/lucasb-eyer/go-colorful'] + version = 'v1.3.0' + hash = 'sha256-6BKrJsfmxie+YFAWzTYVPQfrwjQEXRo+J8LY+50C1BU=' + + [mod.'github.com/mailru/easyjson'] + version = 'v0.7.7' + hash = 'sha256-NVCz8MURpxgOjHXqxOZExqV4bnpHggpeAOyZDArjcy4=' + + [mod.'github.com/mattn/go-isatty'] + version = 'v0.0.20' + hash = 'sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=' + + [mod.'github.com/mattn/go-localereader'] + version = 'v0.0.1' + hash = 'sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E=' + + [mod.'github.com/mattn/go-runewidth'] + version = 'v0.0.17' + hash = 'sha256-Xeb3F+WZhyhEWNKuWRm/umc2FolXYHtbi3QoPfDPtaA=' + + [mod.'github.com/microcosm-cc/bluemonday'] + version = 'v1.0.27' + hash = 'sha256-EZSya9FLPQ83CL7N2cZy21fdS35hViTkiMK5f3op8Es=' + + [mod.'github.com/modern-go/concurrent'] + version = 'v0.0.0-20180306012644-bacd9c7ef1dd' + hash = 'sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo=' + + [mod.'github.com/modern-go/reflect2'] + version = 'v1.0.3-0.20250322232337-35a7c28c31ee' + hash = 'sha256-0pkWWZRB3lGFyzmlxxrm0KWVQo9HNXNafaUu3k+rE1g=' + + [mod.'github.com/muesli/ansi'] + version = 'v0.0.0-20230316100256-276c6243b2f6' + hash = 'sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs=' + + [mod.'github.com/muesli/cancelreader'] + version = 'v0.2.2' + hash = 'sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ=' + + [mod.'github.com/muesli/reflow'] + version = 'v0.3.0' + hash = 'sha256-Pou2ybE9SFSZG6YfZLVV1Eyfm+X4FuVpDPLxhpn47Cc=' + + [mod.'github.com/muesli/termenv'] + version = 'v0.16.0' + hash = 'sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI=' + + [mod.'github.com/munnerz/goautoneg'] + version = 'v0.0.0-20191010083416-a7dc8b61c822' + hash = 'sha256-79URDDFenmGc9JZu+5AXHToMrtTREHb3BC84b/gym9Q=' + + [mod.'github.com/pelletier/go-toml/v2'] + version = 'v2.2.3' + hash = 'sha256-fE++SVgnCGdnFZoROHWuYjIR7ENl7k9KKxQrRTquv/o=' + + [mod.'github.com/pkg/errors'] + version = 'v0.9.1' + hash = 'sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw=' + + [mod.'github.com/pmezard/go-difflib'] + version = 'v1.0.0' + hash = 'sha256-/FtmHnaGjdvEIKAJtrUfEhV7EVo5A/eYrtdnUkuxLDA=' + + [mod.'github.com/rivo/uniseg'] + version = 'v0.4.7' + hash = 'sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo=' + + [mod.'github.com/sagikazarmark/locafero'] + version = 'v0.7.0' + hash = 'sha256-ZmaGOKHDw18jJqdkwQwSpUT11F9toR6KPs3241TONeY=' + + [mod.'github.com/sourcegraph/conc'] + version = 'v0.3.0' + hash = 'sha256-mIdMs9MLAOBKf3/0quf1iI3v8uNWydy7ae5MFa+F2Ko=' + + [mod.'github.com/spf13/afero'] + version = 'v1.12.0' + hash = 'sha256-TX3DcyAdrXqf+TxmEz4TilWQo2Y4hcBXOeRY6BjDp+s=' + + [mod.'github.com/spf13/cast'] + version = 'v1.7.1' + hash = 'sha256-BjX0aY/PC37gIdMc7JhMgvhWFsksGdAcp2FgzpuvkPo=' + + [mod.'github.com/spf13/cobra'] + version = 'v1.9.1' + hash = 'sha256-dzEqquABE3UqZmJuj99244QjvfojS8cFlsPr/MXQGj0=' + + [mod.'github.com/spf13/pflag'] + version = 'v1.0.6' + hash = 'sha256-NjrK0FZPIfO/p2xtL1J7fOBQNTZAPZOC6Cb4aMMvhxI=' + + [mod.'github.com/spf13/viper'] + version = 'v1.20.1' + hash = 'sha256-gbCM0k7RAlvn7jpSuWB2LX5Nip9vgwsPNGbDXTI7JvM=' + + [mod.'github.com/stretchr/testify'] + version = 'v1.11.1' + hash = 'sha256-sWfjkuKJyDllDEtnM8sb/pdLzPQmUYWYtmeWz/5suUc=' + + [mod.'github.com/subosito/gotenv'] + version = 'v1.6.0' + hash = 'sha256-LspbjTniiq2xAICSXmgqP7carwlNaLqnCTQfw2pa80A=' + + [mod.'github.com/x448/float16'] + version = 'v0.8.4' + hash = 'sha256-VKzMTMS9pIB/cwe17xPftCSK9Mf4Y6EuBEJlB4by5mE=' + + [mod.'github.com/xo/terminfo'] + version = 'v0.0.0-20220910002029-abceb7e1c41e' + hash = 'sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU=' + + [mod.'github.com/yuin/goldmark'] + version = 'v1.7.13' + hash = 'sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE=' + + [mod.'github.com/yuin/goldmark-emoji'] + version = 'v1.0.6' + hash = 'sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY=' + + [mod.'go.opentelemetry.io/proto/otlp'] + version = 'v1.7.0' + hash = 'sha256-wqmUA6XGjODfuTCRQs32tYFkvO717ZGyZYZFcbNHvLw=' + + [mod.'go.uber.org/multierr'] + version = 'v1.11.0' + hash = 'sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0=' + + [mod.'go.yaml.in/yaml/v2'] + version = 'v2.4.2' + hash = 'sha256-oC8RWdf1zbMYCtmR0ATy/kCkhIwPR9UqFZSMOKLVF/A=' + + [mod.'go.yaml.in/yaml/v3'] + version = 'v3.0.4' + hash = 'sha256-NkGFiDPoCxbr3LFsI6OCygjjkY0rdmg5ggvVVwpyDQ4=' + + [mod.'golang.org/x/net'] + version = 'v0.48.0' + hash = 'sha256-oZpddsiJwWCH3Aipa+XXpy7G/xHY5fEagUSok7T0bXE=' + + [mod.'golang.org/x/oauth2'] + version = 'v0.34.0' + hash = 'sha256-5eqpGGxJ7FJsPmfRek6roeGmkWHBMJaWYXyz8gXJsS4=' + + [mod.'golang.org/x/sync'] + version = 'v0.19.0' + hash = 'sha256-RbRZ+sKZUurOczGhhzOoY/sojTlta3H9XjL4PXX/cno=' + + [mod.'golang.org/x/sys'] + version = 'v0.39.0' + hash = 'sha256-dxTBu/JAWUkPbjFIXXRFdhQWyn+YyEpIC+tWqGo0Y6U=' + + [mod.'golang.org/x/term'] + version = 'v0.38.0' + hash = 'sha256-46mX91XGJqii6SUESiU5mLVJDBOfKYCp+408m91hrMw=' + + [mod.'golang.org/x/text'] + version = 'v0.32.0' + hash = 'sha256-9PXtWBKKY9rG4AgjSP4N+I1DhepXhy8SF/vWSIDIoWs=' + + [mod.'golang.org/x/time'] + version = 'v0.9.0' + hash = 'sha256-ipaWVIk1+DZg0rfCzBSkz/Y6DEnB7xkX2RRYycHkhC0=' + + [mod.'google.golang.org/genproto/googleapis/api'] + version = 'v0.0.0-20251202230838-ff82c1b0f217' + hash = 'sha256-z0OqsPMjhZ8fNmUbcDfi5Q49hBcutKX8lyzW1mLBTec=' + + [mod.'google.golang.org/genproto/googleapis/rpc'] + version = 'v0.0.0-20251202230838-ff82c1b0f217' + hash = 'sha256-I3ZNpNjKKvTq4DVNw3wLKrCuORabZ0oYj0KKhOMI/MA=' + + [mod.'google.golang.org/grpc'] + version = 'v1.79.3' + hash = 'sha256-mO9ZI8ONBMEBTlrwIPeQMfb3C9ru5zPb26P+uG/pIzY=' + + [mod.'google.golang.org/protobuf'] + version = 'v1.36.10' + hash = 'sha256-gUrj1qSpjcpRKCBnrYlKMm+P0OSh7B/8EBREstwhD1w=' + + [mod.'gopkg.in/evanphx/json-patch.v4'] + version = 'v4.12.0' + hash = 'sha256-rUOokb3XW30ftpHp0fsF2WiJln1S0FSt2El7fTHq3CM=' + + [mod.'gopkg.in/inf.v0'] + version = 'v0.9.1' + hash = 'sha256-z84XlyeWLcoYOvWLxPkPFgLkpjyb2Y4pdeGMyySOZQI=' + + [mod.'gopkg.in/yaml.v3'] + version = 'v3.0.1' + hash = 'sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU=' + + [mod.'k8s.io/api'] + version = 'v0.34.2' + hash = 'sha256-8TpPB07Dh6REOxYzT78OGLCV1iTZlMVzEQCzmpA4Q3U=' + + [mod.'k8s.io/apimachinery'] + version = 'v0.34.2' + hash = 'sha256-y6GVugdaX81lHwVWgDgFoIi/v6K9A3YC2kKlGUritVU=' + + [mod.'k8s.io/client-go'] + version = 'v0.34.2' + hash = 'sha256-ELhaXsMrieNn0ms84n5kL9QlNDXQ2u8xpbny+bTvdZg=' + + [mod.'k8s.io/klog/v2'] + version = 'v2.130.1' + hash = 'sha256-n5vls1o1a0V0KYv+3SULq4q3R2Is15K8iDHhFlsSH4o=' + + [mod.'k8s.io/kube-openapi'] + version = 'v0.0.0-20250710124328-f3f2b991d03b' + hash = 'sha256-0v0MN67pora3UCA8vXSJDgkosx/1RaB2HkjUnwdVLbY=' + + [mod.'k8s.io/utils'] + version = 'v0.0.0-20250604170112-4c0f3b243397' + hash = 'sha256-USPKRYYKfbhQWU0CgT/8V1hdBirWjmhTZvJCuuUlNFo=' + + [mod.'nhooyr.io/websocket'] + version = 'v1.8.17' + hash = 'sha256-YIYdIz3KR3j3ykT/TSOoftwkCoZdX1/bUtlXm3RjfoQ=' + + [mod.'sigs.k8s.io/json'] + version = 'v0.0.0-20241014173422-cfa47c3a1cc8' + hash = 'sha256-dkegDkyjp/niYirIdhbQrBYt/uttCZQAfsBzKSzOMh0=' + + [mod.'sigs.k8s.io/randfill'] + version = 'v1.0.0' + hash = 'sha256-xldQxDwW84hmlihdSOFfjXyauhxEWV9KmIDLZMTcYNo=' + + [mod.'sigs.k8s.io/structured-merge-diff/v6'] + version = 'v6.3.0' + hash = 'sha256-2EqUZSaHUhwTrdjoZuv+Z99tZYrX1E6rxf2ejeKd2BM=' + + [mod.'sigs.k8s.io/yaml'] + version = 'v1.6.0' + hash = 'sha256-49hg7IVPzwxeovp+HTMiWa/10NMMTSTjAdCmIv6p9dw=' diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 0000000..505f53e --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1,1380 @@ +package engine + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/control-theory/gonzo/internal/ai" + "github.com/control-theory/gonzo/internal/memory" + "github.com/control-theory/gonzo/internal/tui" +) + +// Engine is the shared, concurrency-safe analysis state that the TUI +// and web dashboard read from. The processing pipeline +// writes via Ingest(); all Query*/Get* methods acquire a read lock. +type Engine struct { + mu sync.RWMutex + + // Log buffer (ring buffer) + allLogEntries []tui.LogEntry + maxLogBuffer int + + // Severity tracking + lifetimeSeverityCounts map[string]int64 + countsHistory []tui.SeverityCounts + severityTimeSeries []SeverityTimePoint // timestamped 1-second severity history + + // Heatmap data (minute-by-minute severity counts) + heatmapData []tui.HeatmapMinute + + // Pattern clustering per severity + drain3BySeverity map[string]*tui.Drain3Manager + // Combined drain3 for all logs + drain3All *tui.Drain3Manager + + // Service tracking per severity + servicesBySeverity map[string][]tui.ServiceCount + + // Dimension tracking for QueryInsightsParams + lifetimeHostCounts map[string]int64 + lifetimeServiceCounts map[string]int64 + lifetimeAttrKeyCounts map[string]map[string]int64 + + // Frequency snapshot (latest) + snapshot *memory.FrequencySnapshot + + // Word/attribute tracking for classes + lifetimeWordCounts map[string]int64 + lifetimeAttrCounts map[string]int64 + stopWords map[string]bool + + // Stream tracking + streams map[string]*StreamInfo + + // Statistics + statsStartTime time.Time + totalLogsEver int + totalBytes int64 + + // AI client for summary queries + aiClient ai.Client + + // Config + useLogTime bool +} + +// NewEngine creates a new Engine with the given configuration. +func NewEngine(maxLogBuffer int, stopWords map[string]bool, aiClient ai.Client, useLogTime bool) *Engine { + return &Engine{ + allLogEntries: make([]tui.LogEntry, 0, maxLogBuffer), + maxLogBuffer: maxLogBuffer, + lifetimeSeverityCounts: make(map[string]int64), + countsHistory: make([]tui.SeverityCounts, 0), + severityTimeSeries: make([]SeverityTimePoint, 0, 120), + heatmapData: make([]tui.HeatmapMinute, 0), + drain3BySeverity: tui.InitializeDrain3BySeverity(), + drain3All: tui.NewDrain3Manager(), + servicesBySeverity: make(map[string][]tui.ServiceCount), + lifetimeHostCounts: make(map[string]int64), + lifetimeServiceCounts: make(map[string]int64), + lifetimeAttrKeyCounts: make(map[string]map[string]int64), + lifetimeWordCounts: make(map[string]int64), + lifetimeAttrCounts: make(map[string]int64), + stopWords: stopWords, + streams: make(map[string]*StreamInfo), + statsStartTime: time.Now(), + aiClient: aiClient, + useLogTime: useLogTime, + } +} + +// Ingest adds a new log entry to the engine. Called from the processing pipeline. +// This is the only write path — it acquires a write lock. +func (e *Engine) Ingest(entry tui.LogEntry) { + e.mu.Lock() + defer e.mu.Unlock() + + // Add to buffer + e.allLogEntries = append(e.allLogEntries, entry) + if len(e.allLogEntries) > e.maxLogBuffer { + e.allLogEntries = e.allLogEntries[1:] + } + + // Update statistics + e.totalLogsEver++ + e.totalBytes += int64(len(entry.RawLine)) + + // Update lifetime severity counts + e.lifetimeSeverityCounts[entry.Severity]++ + + // Update host counts + if host := entry.Attributes["host"]; host != "" { + e.lifetimeHostCounts[host]++ + } + + // Update service counts + serviceName := getServiceName(entry) + if serviceName != "" && serviceName != "unknown" { + e.lifetimeServiceCounts[serviceName]++ + } + + // Update per-attribute-key value counts + for key, value := range entry.Attributes { + attrKey := fmt.Sprintf("%s=%s", key, value) + if len(attrKey) < 200 { + e.lifetimeAttrCounts[attrKey]++ + } + if e.lifetimeAttrKeyCounts[key] == nil { + e.lifetimeAttrKeyCounts[key] = make(map[string]int64) + } + e.lifetimeAttrKeyCounts[key][value]++ + } + + // Update word counts + words := strings.Fields(strings.ToLower(entry.Message)) + for _, word := range words { + if len(word) >= 2 && len(word) <= 50 { + word = strings.Trim(word, ".,!?;:()[]{}\"'") + if len(word) >= 3 && !e.stopWords[word] { + e.lifetimeWordCounts[word]++ + } + } + } + + // Update heatmap + e.updateHeatmapData(entry) + + // Update services by severity + e.updateServicesBySeverity(entry) + + // Update drain3 patterns + if drain3Instance, exists := e.drain3BySeverity[entry.Severity]; exists && drain3Instance != nil { + drain3Instance.AddLogMessage(entry.Message) + } + if e.drain3All != nil { + e.drain3All.AddLogMessage(entry.Message) + } + + // Update stream tracking + e.updateStreamTracking(entry) +} + +// IngestSeverityCounts adds interval-level severity counts to history. +func (e *Engine) IngestSeverityCounts(counts tui.SeverityCounts) { + e.mu.Lock() + defer e.mu.Unlock() + + e.countsHistory = append(e.countsHistory, counts) + if len(e.countsHistory) > 50 { + e.countsHistory = e.countsHistory[1:] + } + + // Also store in timestamped time series for the web severity chart + point := SeverityTimePoint{ + Timestamp: time.Now().Unix(), + Counts: map[string]int{ + "FATAL": counts.Fatal, + "ERROR": counts.Error, + "WARN": counts.Warn, + "INFO": counts.Info, + "DEBUG": counts.Debug, + "TRACE": counts.Trace, + }, + Total: counts.Total, + } + e.severityTimeSeries = append(e.severityTimeSeries, point) + // Keep up to 120 seconds (2 minutes) + if len(e.severityTimeSeries) > 120 { + e.severityTimeSeries = e.severityTimeSeries[len(e.severityTimeSeries)-120:] + } +} + +// UpdateFrequencySnapshot updates the latest frequency snapshot. +func (e *Engine) UpdateFrequencySnapshot(snapshot *memory.FrequencySnapshot) { + e.mu.Lock() + defer e.mu.Unlock() + e.snapshot = snapshot +} + +// Reset clears all engine state. +func (e *Engine) Reset() { + e.mu.Lock() + defer e.mu.Unlock() + + e.allLogEntries = e.allLogEntries[:0] + e.lifetimeSeverityCounts = make(map[string]int64) + e.countsHistory = e.countsHistory[:0] + e.severityTimeSeries = e.severityTimeSeries[:0] + e.heatmapData = e.heatmapData[:0] + e.drain3BySeverity = tui.InitializeDrain3BySeverity() + e.drain3All = tui.NewDrain3Manager() + e.servicesBySeverity = make(map[string][]tui.ServiceCount) + e.lifetimeHostCounts = make(map[string]int64) + e.lifetimeServiceCounts = make(map[string]int64) + e.lifetimeAttrKeyCounts = make(map[string]map[string]int64) + e.lifetimeWordCounts = make(map[string]int64) + e.lifetimeAttrCounts = make(map[string]int64) + e.streams = make(map[string]*StreamInfo) + e.snapshot = nil + e.totalLogsEver = 0 + e.totalBytes = 0 + e.statsStartTime = time.Now() +} + +// --- Read-path: State Accessors (for TUI backward compat) --- + +// GetAllLogEntries returns a copy of all log entries. +func (e *Engine) GetAllLogEntries() []tui.LogEntry { + e.mu.RLock() + defer e.mu.RUnlock() + result := make([]tui.LogEntry, len(e.allLogEntries)) + copy(result, e.allLogEntries) + return result +} + +// GetLogEntryCount returns the number of log entries in the buffer. +func (e *Engine) GetLogEntryCount() int { + e.mu.RLock() + defer e.mu.RUnlock() + return len(e.allLogEntries) +} + +// GetHeatmapData returns a copy of heatmap data. +func (e *Engine) GetHeatmapData() []tui.HeatmapMinute { + e.mu.RLock() + defer e.mu.RUnlock() + result := make([]tui.HeatmapMinute, len(e.heatmapData)) + copy(result, e.heatmapData) + return result +} + +// GetSeverityTimeSeries returns timestamped per-second severity counts. +// When no filters are provided, returns the pre-aggregated global series. +// When filters are present (e.g. search), builds the series from filtered log entries. +func (e *Engine) GetSeverityTimeSeries() []SeverityTimePoint { + e.mu.RLock() + defer e.mu.RUnlock() + result := make([]SeverityTimePoint, len(e.severityTimeSeries)) + copy(result, e.severityTimeSeries) + return result +} + +// QuerySeverityTimeSeries returns per-second severity counts filtered by the given filters. +// This recomputes the time series from raw log entries to support search/stream filtering. +func (e *Engine) QuerySeverityTimeSeries(_ context.Context, filters InsightsFilters) []SeverityTimePoint { + e.mu.RLock() + defer e.mu.RUnlock() + + // If no meaningful filters, return the pre-aggregated series + if filters.Search == nil && filters.Severity == nil && filters.Start == nil && filters.End == nil { + result := make([]SeverityTimePoint, len(e.severityTimeSeries)) + copy(result, e.severityTimeSeries) + return result + } + + filtered := e.filterEntries(filters) + if len(filtered) == 0 { + return nil + } + + // Bucket entries by second + buckets := make(map[int64]map[string]int) + for _, entry := range filtered { + ts := entry.Timestamp.Unix() + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp.Unix() + } + if buckets[ts] == nil { + buckets[ts] = make(map[string]int) + } + sev := strings.ToUpper(entry.Severity) + if sev == "" { + sev = "INFO" + } + buckets[ts][sev]++ + } + + // Sort timestamps + timestamps := make([]int64, 0, len(buckets)) + for ts := range buckets { + timestamps = append(timestamps, ts) + } + sort.Slice(timestamps, func(i, j int) bool { return timestamps[i] < timestamps[j] }) + + // Build result + result := make([]SeverityTimePoint, 0, len(timestamps)) + for _, ts := range timestamps { + counts := buckets[ts] + total := 0 + for _, c := range counts { + total += c + } + result = append(result, SeverityTimePoint{ + Timestamp: ts, + Counts: counts, + Total: total, + }) + } + return result +} + +// GetDrain3BySeverity returns the drain3 map (not a copy — callers should not modify). +func (e *Engine) GetDrain3BySeverity() map[string]*tui.Drain3Manager { + e.mu.RLock() + defer e.mu.RUnlock() + return e.drain3BySeverity +} + +// GetServicesBySeverity returns the services map. +func (e *Engine) GetServicesBySeverity() map[string][]tui.ServiceCount { + e.mu.RLock() + defer e.mu.RUnlock() + // Return a shallow copy + result := make(map[string][]tui.ServiceCount, len(e.servicesBySeverity)) + for k, v := range e.servicesBySeverity { + copied := make([]tui.ServiceCount, len(v)) + copy(copied, v) + result[k] = copied + } + return result +} + +// GetCountsHistory returns a copy of the counts history. +func (e *Engine) GetCountsHistory() []tui.SeverityCounts { + e.mu.RLock() + defer e.mu.RUnlock() + result := make([]tui.SeverityCounts, len(e.countsHistory)) + copy(result, e.countsHistory) + return result +} + +// GetFrequencySnapshot returns the latest frequency snapshot. +func (e *Engine) GetFrequencySnapshot() *memory.FrequencySnapshot { + e.mu.RLock() + defer e.mu.RUnlock() + return e.snapshot +} + +// GetLifetimeSeverityCounts returns a copy of lifetime severity counts. +func (e *Engine) GetLifetimeSeverityCounts() map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]int64, len(e.lifetimeSeverityCounts)) + for k, v := range e.lifetimeSeverityCounts { + result[k] = v + } + return result +} + +// GetLifetimeHostCounts returns a copy of lifetime host counts. +func (e *Engine) GetLifetimeHostCounts() map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]int64, len(e.lifetimeHostCounts)) + for k, v := range e.lifetimeHostCounts { + result[k] = v + } + return result +} + +// GetLifetimeServiceCounts returns a copy of lifetime service counts. +func (e *Engine) GetLifetimeServiceCounts() map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]int64, len(e.lifetimeServiceCounts)) + for k, v := range e.lifetimeServiceCounts { + result[k] = v + } + return result +} + +// GetLifetimeAttrCounts returns a copy of lifetime attribute counts. +func (e *Engine) GetLifetimeAttrCounts() map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]int64, len(e.lifetimeAttrCounts)) + for k, v := range e.lifetimeAttrCounts { + result[k] = v + } + return result +} + +// GetLifetimeWordCounts returns a copy of lifetime word counts. +func (e *Engine) GetLifetimeWordCounts() map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]int64, len(e.lifetimeWordCounts)) + for k, v := range e.lifetimeWordCounts { + result[k] = v + } + return result +} + +// GetLifetimeAttrKeyCounts returns a copy of per-key attribute value counts. +func (e *Engine) GetLifetimeAttrKeyCounts() map[string]map[string]int64 { + e.mu.RLock() + defer e.mu.RUnlock() + result := make(map[string]map[string]int64, len(e.lifetimeAttrKeyCounts)) + for k, v := range e.lifetimeAttrKeyCounts { + inner := make(map[string]int64, len(v)) + for ik, iv := range v { + inner[ik] = iv + } + result[k] = inner + } + return result +} + +// GetStats returns engine statistics. +func (e *Engine) GetStats() EngineStats { + e.mu.RLock() + defer e.mu.RUnlock() + return EngineStats{ + StartTime: e.statsStartTime, + TotalLogsEver: e.totalLogsEver, + TotalBytes: e.totalBytes, + BufferSize: e.maxLogBuffer, + BufferUsed: len(e.allLogEntries), + } +} + +// GetStreams returns all tracked streams. +func (e *Engine) GetStreams() []StreamInfo { + e.mu.RLock() + defer e.mu.RUnlock() + result := make([]StreamInfo, 0, len(e.streams)) + for _, s := range e.streams { + result = append(result, *s) + } + return result +} + +// --- Read-path: Query Methods (for Web API) --- + +// QueryLogSamples returns log entries matching the given filters. +func (e *Engine) QueryLogSamples(_ context.Context, filters InsightsFilters) ([]LogSample, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + + // Apply limit + limit := len(filtered) + if filters.Limit != nil && *filters.Limit > 0 && *filters.Limit < limit { + limit = *filters.Limit + } + + // Take the most recent entries (from the end) + start := len(filtered) - limit + if start < 0 { + start = 0 + } + + samples := make([]LogSample, 0, limit) + for _, entry := range filtered[start:] { + ts := entry.Timestamp.Unix() + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp.Unix() + } + samples = append(samples, LogSample{ + Timestamp: ts, + Severity: entry.Severity, + Message: entry.Message, + Attributes: entry.Attributes, + RawLine: entry.RawLine, + }) + } + + return samples, nil +} + +// QuerySeverityData returns severity distribution grouped by the specified dimension. +func (e *Engine) QuerySeverityData(_ context.Context, filters InsightsFilters) ([]SeverityGroup, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + groupBy := filters.GroupBy + if groupBy == "" { + groupBy = "service" + } + + groups := make(map[string]map[string]int) + for _, entry := range filtered { + groupVal := e.getGroupValue(entry, groupBy) + if groups[groupVal] == nil { + groups[groupVal] = make(map[string]int) + } + groups[groupVal][entry.Severity]++ + } + + result := make([]SeverityGroup, 0, len(groups)) + for groupVal, counts := range groups { + total := 0 + for _, c := range counts { + total += c + } + result = append(result, SeverityGroup{ + GroupValue: groupVal, + Counts: counts, + Total: total, + }) + } + + // Sort by total descending + sort.Slice(result, func(i, j int) bool { + return result[i].Total > result[j].Total + }) + + return result, nil +} + +// QuerySentimentData returns sentiment distribution derived from severity. +func (e *Engine) QuerySentimentData(_ context.Context, filters InsightsFilters) (*SentimentData, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + groupBy := filters.GroupBy + if groupBy == "" { + groupBy = "service" + } + + // Group entries by time bucket (1-second) and group_by dimension + type bucketKey struct { + second int64 + groupVal string + } + buckets := make(map[bucketKey][]float64) + groupValueSet := make(map[string]bool) + + for _, entry := range filtered { + groupVal := e.getGroupValue(entry, groupBy) + groupValueSet[groupVal] = true + + ts := entry.Timestamp + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp + } + sec := ts.Unix() + + key := bucketKey{second: sec, groupVal: groupVal} + sentiment := severityToSentiment(entry.Severity) + buckets[key] = append(buckets[key], sentiment) + } + + // Build response + groupValues := make([]string, 0, len(groupValueSet)) + for gv := range groupValueSet { + groupValues = append(groupValues, gv) + } + sort.Strings(groupValues) + + sentimentBuckets := make([]SentimentBucket, 0, len(buckets)) + for key, sentiments := range buckets { + avg := 0.0 + for _, s := range sentiments { + avg += s + } + avg /= float64(len(sentiments)) + + sentimentBuckets = append(sentimentBuckets, SentimentBucket{ + Timestamp: key.second, + GroupValue: key.groupVal, + Sentiment: avg, + LogCount: len(sentiments), + }) + } + + return &SentimentData{ + GroupValues: groupValues, + Buckets: sentimentBuckets, + }, nil +} + +// QueryPatterns returns drain3 patterns grouped by the specified dimension. +func (e *Engine) QueryPatterns(_ context.Context, filters InsightsFilters) ([]PatternGroup, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + groupBy := filters.GroupBy + if groupBy == "" { + groupBy = "service" + } + + limit := 20 + if filters.Limit != nil && *filters.Limit > 0 { + limit = *filters.Limit + } + + // For severity grouping, use the pre-built drain3 instances + if groupBy == "severity" { + result := make([]PatternGroup, 0) + for severity, dm := range e.drain3BySeverity { + if dm == nil { + continue + } + patterns := dm.GetTopPatterns(limit) + if len(patterns) == 0 { + continue + } + items := make([]PatternItem, len(patterns)) + for i, p := range patterns { + items[i] = PatternItem{ + Pattern: p.Template, + Count: p.Count, + Percentage: p.Percentage, + } + } + result = append(result, PatternGroup{ + GroupValue: severity, + Patterns: items, + }) + } + return result, nil + } + + // For other groupings, use the combined drain3 and return as single group + patterns := e.drain3All.GetTopPatterns(limit) + items := make([]PatternItem, len(patterns)) + for i, p := range patterns { + items[i] = PatternItem{ + Pattern: p.Template, + Count: p.Count, + Percentage: p.Percentage, + } + } + + return []PatternGroup{{ + GroupValue: "all", + Patterns: items, + }}, nil +} + +// QueryClasses returns log classification distribution. +func (e *Engine) QueryClasses(_ context.Context, filters InsightsFilters) ([]ClassItem, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + + // Classify logs by severity as a simple categorization + classCounts := make(map[string]int) + for _, entry := range filtered { + classCounts[entry.Severity]++ + } + + total := len(filtered) + result := make([]ClassItem, 0, len(classCounts)) + for name, count := range classCounts { + pct := 0.0 + if total > 0 { + pct = float64(count) * 100.0 / float64(total) + } + result = append(result, ClassItem{ + Name: name, + Count: count, + Percentage: pct, + }) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].Count > result[j].Count + }) + + if filters.Limit != nil && *filters.Limit > 0 && len(result) > *filters.Limit { + result = result[:*filters.Limit] + } + + return result, nil +} + +// QuerySummary returns an AI-generated summary of the logs. +func (e *Engine) QuerySummary(_ context.Context, filters InsightsFilters) (string, error) { + e.mu.RLock() + entries := e.filterEntries(filters) + severityCounts := make(map[string]int) + for _, entry := range entries { + severityCounts[entry.Severity]++ + } + totalLogs := len(entries) + aiClient := e.aiClient + e.mu.RUnlock() + + if aiClient == nil { + // Return a statistical summary when AI is not configured + return e.generateStatisticalSummary(totalLogs, severityCounts, entries), nil + } + + // Build a prompt for the AI + var sb strings.Builder + sb.WriteString("Analyze these log entries and provide a brief summary of key issues, patterns, and recommendations:\n\n") + sb.WriteString(fmt.Sprintf("Total logs: %d\n", totalLogs)) + sb.WriteString("Severity distribution:\n") + for sev, count := range severityCounts { + sb.WriteString(fmt.Sprintf(" %s: %d\n", sev, count)) + } + sb.WriteString("\nRecent log samples:\n") + + // Include up to 50 recent entries + sampleCount := min(50, len(entries)) + start := len(entries) - sampleCount + for _, entry := range entries[start:] { + sb.WriteString(fmt.Sprintf("[%s] %s\n", entry.Severity, entry.Message)) + } + + result, err := aiClient.AnalyzeLog(sb.String(), "SUMMARY", time.Now().Format(time.RFC3339), nil) + if err != nil { + return "", fmt.Errorf("AI analysis failed: %w", err) + } + + return result, nil +} + +// GetSentimentHeatmap returns an ASCII heatmap of sentiment over time. +func (e *Engine) GetSentimentHeatmap(_ context.Context, filters InsightsFilters) (string, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + groupBy := filters.GroupBy + if groupBy == "" { + groupBy = "service" + } + + // Build minute-by-group sentiment map + type cellKey struct { + minute time.Time + groupVal string + } + cells := make(map[cellKey][]float64) + groupValueSet := make(map[string]bool) + var minTime, maxTime time.Time + + for _, entry := range filtered { + groupVal := e.getGroupValue(entry, groupBy) + groupValueSet[groupVal] = true + + ts := entry.Timestamp + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp + } + minute := ts.Truncate(time.Minute) + + if minTime.IsZero() || minute.Before(minTime) { + minTime = minute + } + if maxTime.IsZero() || minute.After(maxTime) { + maxTime = minute + } + + key := cellKey{minute: minute, groupVal: groupVal} + cells[key] = append(cells[key], severityToSentiment(entry.Severity)) + } + + if len(cells) == 0 { + return "No data available for heatmap.", nil + } + + // Build ASCII heatmap + groupValues := make([]string, 0, len(groupValueSet)) + for gv := range groupValueSet { + groupValues = append(groupValues, gv) + } + sort.Strings(groupValues) + + // Generate time columns (every 5 minutes) + var sb strings.Builder + sb.WriteString("Sentiment Heatmap (+ positive, . neutral, - negative)\n\n") + + // Determine label width + maxLabelLen := 0 + for _, gv := range groupValues { + if len(gv) > maxLabelLen { + maxLabelLen = len(gv) + } + } + if maxLabelLen > 20 { + maxLabelLen = 20 + } + + for _, gv := range groupValues { + label := gv + if len(label) > maxLabelLen { + label = label[:maxLabelLen] + } + sb.WriteString(fmt.Sprintf("%-*s │", maxLabelLen, label)) + + // Walk through time buckets + t := minTime + for !t.After(maxTime) { + key := cellKey{minute: t, groupVal: gv} + if sentiments, ok := cells[key]; ok { + avg := 0.0 + for _, s := range sentiments { + avg += s + } + avg /= float64(len(sentiments)) + sb.WriteByte(sentimentToChar(avg)) + } else { + sb.WriteByte(' ') + } + t = t.Add(time.Minute) + } + sb.WriteByte('\n') + } + + return sb.String(), nil +} + +// GetAnomalies detects anomalies in the log data. +func (e *Engine) GetAnomalies(_ context.Context, filters InsightsFilters) (string, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + filtered := e.filterEntries(filters) + if len(filtered) == 0 { + return "No log data available for anomaly detection.", nil + } + + var sb strings.Builder + sb.WriteString("# Anomaly Detection Report\n\n") + + // Check for fatal/critical logs + fatalCount := 0 + errorCount := 0 + for _, entry := range filtered { + switch entry.Severity { + case "FATAL", "CRITICAL": + fatalCount++ + case "ERROR": + errorCount++ + } + } + + if fatalCount > 0 { + sb.WriteString(fmt.Sprintf("## Fatal/Critical Logs Detected\n- **%d** fatal/critical log entries found\n\n", fatalCount)) + } + + // Check error rate + totalCount := len(filtered) + if totalCount > 0 { + errorRate := float64(errorCount) * 100.0 / float64(totalCount) + if errorRate > 10 { + sb.WriteString(fmt.Sprintf("## High Error Rate\n- Error rate: **%.1f%%** (%d/%d logs)\n\n", errorRate, errorCount, totalCount)) + } + } + + // Check for error spikes (compare last 5 min vs overall rate) + now := time.Now() + recentCutoff := now.Add(-5 * time.Minute) + recentErrors := 0 + recentTotal := 0 + for _, entry := range filtered { + ts := entry.Timestamp + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp + } + if ts.After(recentCutoff) { + recentTotal++ + if entry.Severity == "ERROR" || entry.Severity == "FATAL" || entry.Severity == "CRITICAL" { + recentErrors++ + } + } + } + + if recentTotal > 0 { + recentErrorRate := float64(recentErrors) * 100.0 / float64(recentTotal) + overallErrorRate := float64(errorCount+fatalCount) * 100.0 / float64(totalCount) + if recentErrorRate > overallErrorRate*2 && recentErrors > 5 { + sb.WriteString(fmt.Sprintf("## Error Spike Detected\n- Last 5 minutes: **%.1f%%** error rate (%d errors in %d logs)\n- Overall: **%.1f%%** error rate\n\n", + recentErrorRate, recentErrors, recentTotal, overallErrorRate)) + } + } + + // Check sentiment variance by service + type serviceStats struct { + sentiments []float64 + } + serviceMap := make(map[string]*serviceStats) + for _, entry := range filtered { + svc := getServiceName(entry) + if svc == "" || svc == "unknown" { + continue + } + if serviceMap[svc] == nil { + serviceMap[svc] = &serviceStats{} + } + serviceMap[svc].sentiments = append(serviceMap[svc].sentiments, severityToSentiment(entry.Severity)) + } + + for svc, stats := range serviceMap { + if len(stats.sentiments) < 10 { + continue + } + mean := 0.0 + for _, s := range stats.sentiments { + mean += s + } + mean /= float64(len(stats.sentiments)) + + variance := 0.0 + for _, s := range stats.sentiments { + diff := s - mean + variance += diff * diff + } + variance /= float64(len(stats.sentiments)) + + if mean < -0.3 { + sb.WriteString(fmt.Sprintf("## Negative Sentiment: %s\n- Mean sentiment: **%.2f** (%.0f logs)\n- Variance: %.2f\n\n", + svc, mean, float64(len(stats.sentiments)), variance)) + } + } + + if sb.Len() == len("# Anomaly Detection Report\n\n") { + sb.WriteString("No anomalies detected in the current data.\n") + } + + return sb.String(), nil +} + +// QueryInsightsParams returns available filter dimension values. +func (e *Engine) QueryInsightsParams(_ context.Context, _ InsightsFilters) (*InsightsParams, error) { + e.mu.RLock() + defer e.mu.RUnlock() + + params := &InsightsParams{ + TotalLogs: int64(e.totalLogsEver), + } + + // Collect unique values from lifetime counts + for host := range e.lifetimeHostCounts { + params.Hosts = append(params.Hosts, host) + } + for svc := range e.lifetimeServiceCounts { + params.Services = append(params.Services, svc) + } + for sev := range e.lifetimeSeverityCounts { + params.Severities = append(params.Severities, sev) + } + + // Collect namespace, pod, deployment, environment, cluster from attributes + dimKeys := map[string]*[]string{ + "k8s.namespace": ¶ms.Namespaces, + "k8s.pod": ¶ms.Pods, + "k8s.deployment": ¶ms.Deployments, + "environment": ¶ms.Environments, + "env": ¶ms.Environments, + "cluster": ¶ms.Clusters, + } + for key, target := range dimKeys { + if vals, ok := e.lifetimeAttrKeyCounts[key]; ok { + for val := range vals { + *target = append(*target, val) + } + } + } + + // Time range + if len(e.allLogEntries) > 0 { + first := e.allLogEntries[0].Timestamp.Unix() + last := e.allLogEntries[len(e.allLogEntries)-1].Timestamp.Unix() + params.OldestLog = &first + params.NewestLog = &last + } + + // Sort all slices + sort.Strings(params.Hosts) + sort.Strings(params.Services) + sort.Strings(params.Severities) + sort.Strings(params.Namespaces) + sort.Strings(params.Pods) + sort.Strings(params.Deployments) + sort.Strings(params.Environments) + sort.Strings(params.Clusters) + + return params, nil +} + +// --- Internal helpers --- + +// filterEntries applies InsightsFilters to the in-memory log buffer. +// Must be called with at least a read lock held. +func (e *Engine) filterEntries(filters InsightsFilters) []tui.LogEntry { + result := make([]tui.LogEntry, 0, len(e.allLogEntries)) + + for _, entry := range e.allLogEntries { + ts := entry.Timestamp + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp + } + + // Time range filter + if filters.Start != nil && ts.Unix() < *filters.Start { + continue + } + if filters.End != nil && ts.Unix() > *filters.End { + continue + } + + // Severity filter + if filters.Severity != nil && *filters.Severity != "" { + if !strings.EqualFold(entry.Severity, *filters.Severity) { + continue + } + } + + // Search filter + if filters.Search != nil && *filters.Search != "" { + search := strings.ToLower(*filters.Search) + if !strings.Contains(strings.ToLower(entry.Message), search) && + !strings.Contains(strings.ToLower(entry.RawLine), search) { + found := false + for _, v := range entry.Attributes { + if strings.Contains(strings.ToLower(v), search) { + found = true + break + } + } + if !found { + continue + } + } + } + + // Dimension filters + if !e.matchesDimensionFilter(entry, filters) { + continue + } + + result = append(result, entry) + } + + return result +} + +// matchesDimensionFilter checks if an entry matches the dimension filters. +func (e *Engine) matchesDimensionFilter(entry tui.LogEntry, filters InsightsFilters) bool { + if filters.Namespaces != nil && len(*filters.Namespaces) > 0 { + ns := entry.Attributes["k8s.namespace"] + if !containsString(*filters.Namespaces, ns) { + return false + } + } + if filters.Pods != nil && len(*filters.Pods) > 0 { + pod := entry.Attributes["k8s.pod"] + if !containsString(*filters.Pods, pod) { + return false + } + } + if filters.Hosts != nil && len(*filters.Hosts) > 0 { + host := entry.Attributes["host"] + if !containsString(*filters.Hosts, host) { + return false + } + } + if filters.Services != nil && len(*filters.Services) > 0 { + svc := getServiceName(entry) + if !containsString(*filters.Services, svc) { + return false + } + } + if filters.Environments != nil && len(*filters.Environments) > 0 { + env := entry.Attributes["environment"] + if env == "" { + env = entry.Attributes["env"] + } + if !containsString(*filters.Environments, env) { + return false + } + } + if filters.Clusters != nil && len(*filters.Clusters) > 0 { + cluster := entry.Attributes["cluster"] + if !containsString(*filters.Clusters, cluster) { + return false + } + } + if filters.Deployments != nil && len(*filters.Deployments) > 0 { + deploy := entry.Attributes["k8s.deployment"] + if !containsString(*filters.Deployments, deploy) { + return false + } + } + return true +} + +// getGroupValue extracts the group_by dimension value from a log entry. +func (e *Engine) getGroupValue(entry tui.LogEntry, groupBy string) string { + switch groupBy { + case "service": + return getServiceName(entry) + case "host": + if h := entry.Attributes["host"]; h != "" { + return h + } + return "unknown" + case "severity": + return entry.Severity + case "namespace": + if ns := entry.Attributes["k8s.namespace"]; ns != "" { + return ns + } + return "default" + case "pod": + if pod := entry.Attributes["k8s.pod"]; pod != "" { + return pod + } + return "unknown" + case "deployment": + if dep := entry.Attributes["k8s.deployment"]; dep != "" { + return dep + } + return "unknown" + case "env", "environment": + if env := entry.Attributes["environment"]; env != "" { + return env + } + if env := entry.Attributes["env"]; env != "" { + return env + } + return "default" + case "cluster": + if cl := entry.Attributes["cluster"]; cl != "" { + return cl + } + return "default" + case "category": + return entry.Severity // Use severity as category for local + default: + return getServiceName(entry) + } +} + +// updateHeatmapData updates minute-by-minute heatmap data. +func (e *Engine) updateHeatmapData(entry tui.LogEntry) { + ts := entry.Timestamp + if e.useLogTime && !entry.OrigTimestamp.IsZero() { + ts = entry.OrigTimestamp + } + entryTime := ts.Truncate(time.Minute) + + // Find or create the minute entry + var targetMinute *tui.HeatmapMinute + for i := range e.heatmapData { + if e.heatmapData[i].Timestamp.Equal(entryTime) { + targetMinute = &e.heatmapData[i] + break + } + } + + if targetMinute == nil { + e.heatmapData = append(e.heatmapData, tui.HeatmapMinute{ + Timestamp: entryTime, + Counts: tui.SeverityCounts{}, + }) + targetMinute = &e.heatmapData[len(e.heatmapData)-1] + } + + // Update severity count + switch entry.Severity { + case "TRACE": + targetMinute.Counts.Trace++ + case "DEBUG": + targetMinute.Counts.Debug++ + case "INFO": + targetMinute.Counts.Info++ + case "WARN", "WARNING": + targetMinute.Counts.Warn++ + case "ERROR": + targetMinute.Counts.Error++ + case "FATAL": + targetMinute.Counts.Fatal++ + case "CRITICAL": + targetMinute.Counts.Critical++ + default: + targetMinute.Counts.Unknown++ + } + targetMinute.Counts.Total++ + + // Prune old data (keep 6 hours) + cutoffTime := time.Now().Add(-6 * time.Hour) + filtered := e.heatmapData[:0] + for _, minute := range e.heatmapData { + if minute.Timestamp.After(cutoffTime) { + filtered = append(filtered, minute) + } + } + e.heatmapData = filtered +} + +// updateServicesBySeverity updates the services-by-severity tracking. +func (e *Engine) updateServicesBySeverity(entry tui.LogEntry) { + severity := entry.Severity + if severity == "" { + severity = "UNKNOWN" + } + + serviceName := getServiceName(entry) + if serviceName == "" || serviceName == "unknown" { + return + } + + if e.servicesBySeverity[severity] == nil { + e.servicesBySeverity[severity] = make([]tui.ServiceCount, 0) + } + + found := false + for i := range e.servicesBySeverity[severity] { + if e.servicesBySeverity[severity][i].Service == serviceName { + e.servicesBySeverity[severity][i].Count++ + found = true + break + } + } + + if !found { + e.servicesBySeverity[severity] = append(e.servicesBySeverity[severity], tui.ServiceCount{ + Service: serviceName, + Count: 1, + }) + } + + // Keep top 10 sorted + services := e.servicesBySeverity[severity] + sort.Slice(services, func(i, j int) bool { + return services[i].Count > services[j].Count + }) + if len(services) > 10 { + e.servicesBySeverity[severity] = services[:10] + } +} + +// updateStreamTracking updates stream info for a log entry. +func (e *Engine) updateStreamTracking(entry tui.LogEntry) { + // Determine source and stream from attributes + source := "stdin" + stream := "stdin" + + if ns := entry.Attributes["k8s.namespace"]; ns != "" { + source = "k8s" + pod := entry.Attributes["k8s.pod"] + if pod != "" { + stream = ns + "/" + pod + } else { + stream = ns + } + } else if svc := entry.Attributes["service.name"]; svc != "" { + source = "otlp" + stream = svc + } else if filename := entry.Attributes["source_file"]; filename != "" { + source = "file" + stream = filename + } + + key := source + ":" + stream + if s, ok := e.streams[key]; ok { + s.LogCount++ + s.LastSeen = time.Now() + } else { + e.streams[key] = &StreamInfo{ + Source: source, + Stream: stream, + LogCount: 1, + LastSeen: time.Now(), + Active: true, + } + } +} + +// generateStatisticalSummary creates a text summary when AI is not configured. +func (e *Engine) generateStatisticalSummary(totalLogs int, severityCounts map[string]int, entries []tui.LogEntry) string { + var sb strings.Builder + sb.WriteString("## Log Analysis Summary (Statistical)\n\n") + sb.WriteString(fmt.Sprintf("**Total logs analyzed:** %d\n\n", totalLogs)) + + sb.WriteString("### Severity Distribution\n") + for sev, count := range severityCounts { + pct := float64(count) * 100.0 / float64(max(totalLogs, 1)) + sb.WriteString(fmt.Sprintf("- %s: %d (%.1f%%)\n", sev, count, pct)) + } + + sb.WriteString("\n### Top Patterns\n") + patterns := e.drain3All.GetTopPatterns(5) + for i, p := range patterns { + sb.WriteString(fmt.Sprintf("%d. `%s` (%d occurrences, %.1f%%)\n", i+1, p.Template, p.Count, p.Percentage)) + } + + if len(entries) > 0 { + uptime := time.Since(e.statsStartTime).Round(time.Second) + sb.WriteString(fmt.Sprintf("\n**Uptime:** %s | **Buffer:** %d/%d\n", uptime, len(e.allLogEntries), e.maxLogBuffer)) + } + + sb.WriteString("\n*Note: AI analysis is not configured. Set up an AI provider for deeper insights.*\n") + return sb.String() +} + +// --- Utility functions --- + +func getServiceName(entry tui.LogEntry) string { + if svc := entry.Attributes["service"]; svc != "" { + return svc + } + if svc := entry.Attributes["service.name"]; svc != "" { + return svc + } + if svc := entry.Attributes["serviceName"]; svc != "" { + return svc + } + if svc := entry.Attributes["app"]; svc != "" { + return svc + } + if svc := entry.Attributes["application"]; svc != "" { + return svc + } + if host := entry.Attributes["host"]; host != "" { + return "host:" + host + } + return "unknown" +} + +func severityToSentiment(severity string) float64 { + switch strings.ToUpper(severity) { + case "FATAL", "CRITICAL": + return -1.0 + case "ERROR": + return -0.6 + case "WARN", "WARNING": + return 0.0 + case "INFO": + return 0.5 + case "DEBUG": + return 0.7 + case "TRACE": + return 0.9 + default: + return 0.0 + } +} + +func sentimentToChar(sentiment float64) byte { + if sentiment > 0.3 { + return '+' + } else if sentiment < -0.3 { + return '-' + } + return '.' +} + +func containsString(slice []string, s string) bool { + for _, item := range slice { + if strings.EqualFold(item, s) { + return true + } + } + return false +} + diff --git a/internal/engine/streams.go b/internal/engine/streams.go new file mode 100644 index 0000000..74d5384 --- /dev/null +++ b/internal/engine/streams.go @@ -0,0 +1,31 @@ +package engine + +import "time" + +// RegisterStream explicitly registers a stream (called at startup for known inputs). +func (e *Engine) RegisterStream(source, stream string) { + e.mu.Lock() + defer e.mu.Unlock() + + key := source + ":" + stream + if _, ok := e.streams[key]; !ok { + e.streams[key] = &StreamInfo{ + Source: source, + Stream: stream, + LogCount: 0, + LastSeen: time.Now(), + Active: true, + } + } +} + +// MarkStreamInactive marks a stream as inactive (e.g., file reader finished). +func (e *Engine) MarkStreamInactive(source, stream string) { + e.mu.Lock() + defer e.mu.Unlock() + + key := source + ":" + stream + if s, ok := e.streams[key]; ok { + s.Active = false + } +} diff --git a/internal/engine/types.go b/internal/engine/types.go new file mode 100644 index 0000000..8720c51 --- /dev/null +++ b/internal/engine/types.go @@ -0,0 +1,150 @@ +package engine + +import "time" + +// InsightsFilters defines filters for querying log analysis data. +// For local Gonzo, time range filters apply to the in-memory log buffer. +// Dimension filters (environments, namespaces, etc.) map to log entry attributes. +type InsightsFilters struct { + Start *int64 `json:"start,omitempty"` + End *int64 `json:"end,omitempty"` + GroupBy string `json:"group_by,omitempty"` + Environments *[]string `json:"environments,omitempty"` + Clusters *[]string `json:"clusters,omitempty"` + Namespaces *[]string `json:"namespaces,omitempty"` + Deployments *[]string `json:"deployments,omitempty"` + Pods *[]string `json:"pods,omitempty"` + Hosts *[]string `json:"hosts,omitempty"` + Services *[]string `json:"services,omitempty"` + Search *string `json:"search,omitempty"` + Severity *string `json:"severity,omitempty"` + Limit *int `json:"limit,omitempty"` +} + +// LogSample is a single log entry for API responses. +type LogSample struct { + Timestamp int64 `json:"timestamp"` + Severity string `json:"severity"` + Message string `json:"message"` + Attributes map[string]string `json:"attributes,omitempty"` + RawLine string `json:"raw_line,omitempty"` +} + +// SeverityGroup represents severity distribution for one group_by value. +type SeverityGroup struct { + GroupValue string `json:"group_value"` + Counts map[string]int `json:"counts"` + Total int `json:"total"` +} + +// SentimentBucket represents one time bucket of sentiment data for a group. +type SentimentBucket struct { + Timestamp int64 `json:"timestamp"` + GroupValue string `json:"group_value"` + Sentiment float64 `json:"sentiment"` + IsAnomaly bool `json:"is_anomaly,omitempty"` + LogCount int `json:"log_count"` +} + +// SentimentData is the response format for sentiment queries. +type SentimentData struct { + GroupValues []string `json:"group_values"` + Buckets []SentimentBucket `json:"buckets"` +} + +// PatternItem represents a single log pattern. +type PatternItem struct { + Pattern string `json:"pattern"` + Count int `json:"count"` + Percentage float64 `json:"percentage"` + Sample string `json:"sample,omitempty"` +} + +// PatternGroup represents patterns for one group_by value. +type PatternGroup struct { + GroupValue string `json:"group_value"` + Patterns []PatternItem `json:"patterns"` +} + +// ClassItem represents a log class/category. +type ClassItem struct { + Name string `json:"name"` + Count int `json:"count"` + Percentage float64 `json:"percentage"` +} + +// HeatmapMinuteData represents one minute of heatmap data. +type HeatmapMinuteData struct { + Timestamp int64 `json:"timestamp"` + Counts map[string]int `json:"counts"` +} + +// SeverityTimePoint represents severity counts at a point in time (1-second interval). +type SeverityTimePoint struct { + Timestamp int64 `json:"timestamp"` + Counts map[string]int `json:"counts"` + Total int `json:"total"` +} + +// AnomalyInfo represents a detected anomaly. +type AnomalyInfo struct { + Type string `json:"type"` + Severity string `json:"severity"` + Description string `json:"description"` + Timestamp int64 `json:"timestamp"` + Score float64 `json:"score"` +} + +// InsightsParams describes available filter dimension values. +type InsightsParams struct { + Environments []string `json:"environments,omitempty"` + Clusters []string `json:"clusters,omitempty"` + Namespaces []string `json:"namespaces,omitempty"` + Deployments []string `json:"deployments,omitempty"` + Pods []string `json:"pods,omitempty"` + Hosts []string `json:"hosts,omitempty"` + Services []string `json:"services,omitempty"` + Severities []string `json:"severities,omitempty"` + Categories []string `json:"categories,omitempty"` + TotalLogs int64 `json:"total_logs"` + OldestLog *int64 `json:"oldest_log,omitempty"` + NewestLog *int64 `json:"newest_log,omitempty"` +} + +// StreamInfo represents an active log input stream. +type StreamInfo struct { + Source string `json:"source"` + Stream string `json:"stream"` + LogCount int64 `json:"log_count"` + LastSeen time.Time `json:"last_seen"` + Active bool `json:"active"` +} + +// StatusInfo represents server status. +type StatusInfo struct { + Uptime string `json:"uptime"` + TotalLogs int64 `json:"total_logs"` + TotalBytes int64 `json:"total_bytes"` + LogRate float64 `json:"log_rate"` + Streams []StreamInfo `json:"streams"` + BufferSize int `json:"buffer_size"` + BufferUsed int `json:"buffer_used"` + AIConfigured bool `json:"ai_configured"` +} + +// AttributeEntry represents a single top attribute key+value with count. +type AttributeEntry struct { + Key string `json:"key"` + Value string `json:"value"` + Count int64 `json:"count"` + Percentage float64 `json:"percentage"` +} + +// EngineStats holds engine-level statistics. +type EngineStats struct { + StartTime time.Time `json:"start_time"` + TotalLogsEver int `json:"total_logs_ever"` + TotalBytes int64 `json:"total_bytes"` + BufferSize int `json:"buffer_size"` + BufferUsed int `json:"buffer_used"` +} diff --git a/internal/releases/releases.go b/internal/releases/releases.go new file mode 100644 index 0000000..2316059 --- /dev/null +++ b/internal/releases/releases.go @@ -0,0 +1,127 @@ +package releases + +import ( + "context" + "encoding/json" + "io" + "net/http" + "sync" + "time" +) + +// Release represents a GitHub release. +type Release struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + URL string `json:"url"` +} + +// githubRelease is the raw GitHub API response shape (we only extract what we need). +type githubRelease struct { + TagName string `json:"tag_name"` + Name string `json:"name"` + Body string `json:"body"` + PublishedAt string `json:"published_at"` + HTMLURL string `json:"html_url"` +} + +// Fetcher fetches and caches GitHub releases. +type Fetcher struct { + releases []Release + done chan struct{} + once sync.Once +} + +// NewFetcher creates a new releases fetcher. +func NewFetcher() *Fetcher { + return &Fetcher{ + done: make(chan struct{}), + } +} + +// FetchInBackground starts a goroutine to fetch releases from the GitHub API. +// Non-blocking. Silently ignores errors. +func (f *Fetcher) FetchInBackground() { + go func() { + defer func() { + f.once.Do(func() { close(f.done) }) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", + "https://api.github.com/repos/control-theory/gonzo/releases", nil) + if err != nil { + return + } + req.Header.Set("Accept", "application/vnd.github+json") + + resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + var ghReleases []githubRelease + if err := json.Unmarshal(body, &ghReleases); err != nil { + return + } + + releases := make([]Release, 0, len(ghReleases)) + for _, gh := range ghReleases { + releases = append(releases, Release{ + TagName: gh.TagName, + Name: gh.Name, + Body: gh.Body, + PublishedAt: gh.PublishedAt, + URL: gh.HTMLURL, + }) + } + + f.releases = releases + }() +} + +// GetReleases returns fetched releases (newest first), or nil if not ready/failed. +// On the first call, waits up to 2 seconds for the fetch to complete. +func (f *Fetcher) GetReleases() []Release { + select { + case <-f.done: + return f.releases + case <-time.After(2 * time.Second): + return f.releases + } +} + +// GetReleasesNonBlocking returns whatever releases are available without waiting. +func (f *Fetcher) GetReleasesNonBlocking() []Release { + select { + case <-f.done: + return f.releases + default: + return nil + } +} + +// GetRelease returns the release matching the given tag (e.g. "v0.3.1"), or nil. +func (f *Fetcher) GetRelease(tag string) *Release { + rels := f.GetReleasesNonBlocking() + for i := range rels { + if rels[i].TagName == tag { + return &rels[i] + } + } + return nil +} diff --git a/internal/state/state.go b/internal/state/state.go new file mode 100644 index 0000000..237e121 --- /dev/null +++ b/internal/state/state.go @@ -0,0 +1,69 @@ +package state + +import ( + "encoding/json" + "os" + "path/filepath" +) + +// AppState holds persistent application state (separate from user config). +type AppState struct { + LastSeenVersion string `json:"last_seen_version"` +} + +// stateDir returns ~/.config/gonzo (same base as config). +func stateDir() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".config", "gonzo"), nil +} + +// statePath returns the full path to the state file. +func statePath() (string, error) { + dir, err := stateDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "state.json"), nil +} + +// Load reads the app state from disk. Returns zero value if file is missing or unreadable. +func Load() AppState { + p, err := statePath() + if err != nil { + return AppState{} + } + + data, err := os.ReadFile(p) + if err != nil { + return AppState{} + } + + var s AppState + if err := json.Unmarshal(data, &s); err != nil { + return AppState{} + } + return s +} + +// Save writes the app state to disk, creating the directory if needed. +func Save(s AppState) error { + p, err := statePath() + if err != nil { + return err + } + + dir := filepath.Dir(p) + if err := os.MkdirAll(dir, 0o755); err != nil { + return err + } + + data, err := json.MarshalIndent(s, "", " ") + if err != nil { + return err + } + + return os.WriteFile(p, data, 0o644) +} diff --git a/internal/tui/components.go b/internal/tui/components.go index f082bdd..069cbe2 100644 --- a/internal/tui/components.go +++ b/internal/tui/components.go @@ -99,13 +99,13 @@ func (m *DashboardModel) renderStatusLine() string { } else { // Default status showing main actions if veryNarrow { - statusText = "Tab • Space • i • ? • q" + statusText = "Tab • Space • i • d • ? • q" } else if narrow { - statusText = "?: Help • Tab: Nav • Space: Pause • i: Stats • q: Quit" + statusText = "?: Help • d: Web UI • Tab: Nav • Space: Pause • i: Stats • q: Quit" } else if medium { - statusText = "Tab: Navigate • Space: Pause • i: Stats • Enter: Select • q: Quit" + statusText = "?: Help • d: Web UI • Tab: Navigate • Space: Pause • i: Stats • Enter: Select • q: Quit" } else { - statusText = "?: Help • Click sections • Wheel: scroll • Space: Pause • Tab: Navigate • i: Stats • Enter: Select • q: Quit" + statusText = "?: Help • d: Web UI • Click sections • Wheel: scroll • Space: Pause • Tab: Navigate • i: Stats • Enter: Select • q: Quit" } } diff --git a/internal/tui/modal_help.go b/internal/tui/modal_help.go index 19396ff..f7c29ae 100644 --- a/internal/tui/modal_help.go +++ b/internal/tui/modal_help.go @@ -85,6 +85,8 @@ ACTIONS: i - Show comprehensive statistics modal i - AI analysis (when viewing log details) m - Switch AI model (shows available models) + d - Open Dstl8 Lite web dashboard in browser + w - Show what's new (release notes) ? or h - Toggle this help q/Ctrl+C - Quit diff --git a/internal/tui/modal_whats_new.go b/internal/tui/modal_whats_new.go new file mode 100644 index 0000000..e2a7f64 --- /dev/null +++ b/internal/tui/modal_whats_new.go @@ -0,0 +1,194 @@ +package tui + +import ( + "fmt" + "regexp" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" + + "github.com/control-theory/gonzo/internal/state" +) + +// gitHashRe matches full 40-character git commit hashes. +var gitHashRe = regexp.MustCompile(`\b([0-9a-f]{40})\b`) + +// renderWhatsNewModal renders the "What's New" modal with GitHub release notes. +func (m *DashboardModel) renderWhatsNewModal() string { + modalWidth := m.width - 8 + modalHeight := m.height - 4 + + contentWidth := modalWidth - 4 + contentHeight := modalHeight - 4 + + m.infoViewport.Width = contentWidth + m.infoViewport.Height = contentHeight + + // Only re-render content if width changed or cache is empty + if m.whatsNewRenderedCache == "" || m.whatsNewCacheWidth != contentWidth { + m.whatsNewRenderedCache = m.buildWhatsNewContent(contentWidth) + m.whatsNewCacheWidth = contentWidth + m.infoViewport.SetContent(m.whatsNewRenderedCache) + } + + contentPane := lipgloss.NewStyle(). + Width(contentWidth). + Height(contentHeight). + Border(lipgloss.NormalBorder()). + BorderForeground(ColorGray). + Render(m.infoViewport.View()) + + header := lipgloss.NewStyle(). + Width(contentWidth). + Foreground(ColorGreen). + Bold(true). + Render("🚀 What's New in Gonzo") + + statusBar := lipgloss.NewStyle(). + Foreground(ColorGray). + Render("↑↓/Wheel: Scroll • ESC: Close") + + modal := lipgloss.JoinVertical(lipgloss.Left, header, contentPane, statusBar) + + finalModal := lipgloss.NewStyle(). + Width(modalWidth). + Height(modalHeight). + Border(lipgloss.RoundedBorder()). + BorderForeground(ColorGreen). + Render(modal) + + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, finalModal) +} + +// buildWhatsNewContent renders all release notes via glamour. Called once and cached. +func (m *DashboardModel) buildWhatsNewContent(width int) string { + if m.releasesFetcher == nil { + return "Loading release notes..." + } + + releases := m.releasesFetcher.GetReleasesNonBlocking() + if len(releases) == 0 { + return "Could not load release notes. Check https://github.com/control-theory/gonzo/releases" + } + + renderWidth := max(40, width-4) + renderer, err := glamour.NewTermRenderer( + glamour.WithAutoStyle(), + glamour.WithWordWrap(renderWidth), + ) + + var sb strings.Builder + + for i, rel := range releases { + version := rel.TagName + name := rel.Name + if name == "" { + name = version + } + + dateStr := "" + if t, parseErr := time.Parse(time.RFC3339, rel.PublishedAt); parseErr == nil { + dateStr = t.Format("January 2, 2006") + } + + if i == 0 && m.currentVersion != "" && ("v"+m.currentVersion == version || m.currentVersion == version || baseVersion("v"+m.currentVersion) == version) { + badge := lipgloss.NewStyle().Bold(true).Foreground(ColorGreen).Render(fmt.Sprintf("★ %s (current)", name)) + if dateStr != "" { + badge += lipgloss.NewStyle().Foreground(ColorGray).Render(" " + dateStr) + } + sb.WriteString(badge) + } else { + badge := lipgloss.NewStyle().Bold(true).Foreground(ColorBlue).Render(name) + if dateStr != "" { + badge += lipgloss.NewStyle().Foreground(ColorGray).Render(" " + dateStr) + } + sb.WriteString(badge) + } + sb.WriteString("\n") + + body := gitHashRe.ReplaceAllStringFunc(strings.TrimSpace(rel.Body), func(hash string) string { + return hash[:7] + }) + if body != "" && err == nil { + rendered, renderErr := renderer.Render(body) + if renderErr == nil { + sb.WriteString(rendered) + } else { + sb.WriteString(body) + sb.WriteString("\n") + } + } else if body != "" { + sb.WriteString(body) + sb.WriteString("\n") + } else { + sb.WriteString(lipgloss.NewStyle().Foreground(ColorGray).Render(" No release notes.")) + sb.WriteString("\n") + } + + if i < len(releases)-1 { + sep := lipgloss.NewStyle().Foreground(ColorGray).Render(strings.Repeat("─", min(width-4, 60))) + sb.WriteString("\n" + sep + "\n\n") + } + } + + return sb.String() +} + +// saveWhatsNewState saves the current version as "last seen" so the modal won't auto-show again. +func (m *DashboardModel) saveWhatsNewState() tea.Cmd { + base := baseVersion(m.currentVersion) + return func() tea.Msg { + state.Save(state.AppState{LastSeenVersion: base}) + return nil + } +} + +// baseVersion strips git describe suffixes like "-13-g58cce74-dirty" from a version string. +func baseVersion(v string) string { + s := v + prefix := "" + if strings.HasPrefix(s, "v") { + prefix = "v" + s = s[1:] + } + parts := strings.SplitN(s, "-", 2) + return prefix + parts[0] +} + +// checkWhatsNewAutoShow checks if we should auto-show the what's-new modal. +func (m *DashboardModel) checkWhatsNewAutoShow() { + if m.whatsNewCheckDone || m.currentVersion == "" || m.currentVersion == "dev" { + m.whatsNewCheckDone = true + return + } + + if m.releasesFetcher == nil { + m.whatsNewCheckDone = true + return + } + + base := baseVersion(m.currentVersion) + if baseVersion(m.lastSeenVersion) == base { + m.whatsNewCheckDone = true + return + } + + rels := m.releasesFetcher.GetReleasesNonBlocking() + if rels == nil { + m.whatsNewRetries++ + if m.whatsNewRetries > 5 { + m.whatsNewCheckDone = true + } + return + } + m.whatsNewCheckDone = true + + if len(rels) > 0 { + m.showWhatsNewModal = true + m.whatsNewRenderedCache = "" // force re-render + m.infoViewport.GotoTop() + } +} diff --git a/internal/tui/model.go b/internal/tui/model.go index 67d2ba5..6983247 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -1,12 +1,15 @@ package tui import ( + "os/exec" "regexp" + "runtime" "sort" "time" "github.com/control-theory/gonzo/internal/ai" "github.com/control-theory/gonzo/internal/memory" + "github.com/control-theory/gonzo/internal/releases" versioncheck "github.com/control-theory/gonzo/internal/version" "github.com/charmbracelet/bubbles/textarea" @@ -225,6 +228,19 @@ type DashboardModel struct { // Version checking versionChecker *versioncheck.Checker // Version checker for update notifications + + // Web dashboard + webPort int // Port for the Dstl8 Lite web dashboard (for browser open shortcut) + + // What's New modal + showWhatsNewModal bool + releasesFetcher *releases.Fetcher + currentVersion string + lastSeenVersion string + whatsNewCheckDone bool // true after we've checked whether to auto-show + whatsNewRetries int // retry counter for waiting on background fetch + whatsNewRenderedCache string // pre-rendered content (glamour is expensive) + whatsNewCacheWidth int // width the cache was rendered at } // UpdateMsg contains data updates for the dashboard @@ -254,8 +270,8 @@ type AIAnalysisMsg struct { // ManualResetMsg represents a manual reset request triggered by user type ManualResetMsg struct{} -// initializeDrain3BySeverity creates separate drain3 instances for each severity level -func initializeDrain3BySeverity() map[string]*Drain3Manager { +// InitializeDrain3BySeverity creates separate drain3 instances for each severity level +func InitializeDrain3BySeverity() map[string]*Drain3Manager { severities := []string{"FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "UNKNOWN"} drain3Map := make(map[string]*Drain3Manager) @@ -317,7 +333,7 @@ func NewDashboardModel(maxLogBuffer int, updateInterval time.Duration, aiProvide allLogEntries: make([]LogEntry, 0, maxLogBuffer), countsHistory: make([]SeverityCounts, 0), heatmapData: make([]HeatmapMinute, 0), - drain3BySeverity: initializeDrain3BySeverity(), + drain3BySeverity: InitializeDrain3BySeverity(), servicesBySeverity: make(map[string][]ServiceCount), availableIntervals: availableIntervals, currentIntervalIdx: currentIdx, @@ -486,6 +502,40 @@ func (m *DashboardModel) SetVersionChecker(checker *versioncheck.Checker) { m.versionChecker = checker } +// SetWebPort sets the web dashboard port for the browser open shortcut +func (m *DashboardModel) SetWebPort(port int) { + m.webPort = port +} + +// SetReleasesFetcher sets the releases fetcher for the what's-new modal +func (m *DashboardModel) SetReleasesFetcher(f *releases.Fetcher) { + m.releasesFetcher = f +} + +// SetCurrentVersion sets the current app version for the what's-new modal +func (m *DashboardModel) SetCurrentVersion(v string) { + m.currentVersion = v +} + +// SetLastSeenVersion sets the last version the user has seen +func (m *DashboardModel) SetLastSeenVersion(v string) { + m.lastSeenVersion = v +} + +// openBrowser opens the given URL in the default browser +func (m *DashboardModel) openBrowser(url string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", url) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + default: + cmd = exec.Command("xdg-open", url) + } + cmd.Start() +} + // SetK8sSource sets the Kubernetes log source for the dashboard func (m *DashboardModel) SetK8sSource(source K8sSourceInterface) { m.k8sSource = source diff --git a/internal/tui/navigation.go b/internal/tui/navigation.go index b63232e..a95d7bc 100644 --- a/internal/tui/navigation.go +++ b/internal/tui/navigation.go @@ -201,6 +201,10 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, tea.Quit case "escape", "esc": + if m.showWhatsNewModal { + m.showWhatsNewModal = false + return m, m.saveWhatsNewState() + } if m.showModelSelectionModal { m.showModelSelectionModal = false return m, nil @@ -455,6 +459,26 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } + case "d": + // Open Dstl8 Lite web dashboard in browser + if !m.showModal && !m.filterActive && !m.searchActive && !m.showSeverityFilterModal && !m.showK8sFilterModal && !m.showColumnConfigModal { + if m.webPort > 0 { + m.openBrowser(fmt.Sprintf("http://localhost:%d", m.webPort)) + } + return m, nil + } + + case "w": + // Open What's New modal + if !m.showModal && !m.showWhatsNewModal && !m.filterActive && !m.searchActive && !m.showSeverityFilterModal && !m.showK8sFilterModal && !m.showColumnConfigModal && !m.showHelp && !m.showPatternsModal && !m.showStatsModal && !m.showCountsModal { + if m.releasesFetcher != nil { + m.showWhatsNewModal = true + m.whatsNewRenderedCache = "" // force fresh render + m.infoViewport.GotoTop() + } + return m, nil + } + case " ": // Spacebar: Global pause/unpause toggle for entire UI if !m.showModal && !m.filterActive && !m.searchActive && !m.showSeverityFilterModal && !m.showK8sFilterModal && !m.showColumnConfigModal { @@ -577,7 +601,6 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } - // Patterns modal shortcuts if m.showPatternsModal { switch msg.String() { case "up", "k": @@ -603,6 +626,35 @@ func (m *DashboardModel) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, cmd } + // What's New modal shortcuts (same pattern as stats/patterns modals) + if m.showWhatsNewModal { + switch msg.String() { + case "up", "k": + m.infoViewport.ScrollUp(1) + return m, nil + case "down", "j": + m.infoViewport.ScrollDown(1) + return m, nil + case "pgup": + m.infoViewport.HalfPageUp() + return m, nil + case "pgdown": + m.infoViewport.HalfPageDown() + return m, nil + case "w": + // Allow 'w' to toggle what's-new modal off + m.showWhatsNewModal = false + return m, m.saveWhatsNewState() + case "escape", "esc": + m.showWhatsNewModal = false + return m, m.saveWhatsNewState() + } + + var cmd tea.Cmd + m.infoViewport, cmd = m.infoViewport.Update(msg) + return m, cmd + } + // Statistics modal shortcuts if m.showStatsModal { switch msg.String() { diff --git a/internal/tui/update.go b/internal/tui/update.go index 3ca2be4..c49c560 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -34,6 +34,11 @@ func (m *DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, func() tea.Msg { return msg } case TickMsg: + // Check if we should auto-show the what's-new modal (once) + if !m.whatsNewCheckDone { + m.checkWhatsNewAutoShow() + } + // Update processing rate statistics on every tick m.updateProcessingRateStats() diff --git a/internal/tui/view.go b/internal/tui/view.go index 5fbb527..1616d0d 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -22,6 +22,11 @@ func (m *DashboardModel) View() string { return m.renderHelpModal() } + // Show what's new modal + if m.showWhatsNewModal { + return m.renderWhatsNewModal() + } + // Show patterns modal if m.showPatternsModal { return m.renderPatternsModal() diff --git a/internal/web/api.go b/internal/web/api.go new file mode 100644 index 0000000..362653c --- /dev/null +++ b/internal/web/api.go @@ -0,0 +1,233 @@ +package web + +import ( + "net/http" + "sort" + "strconv" + "time" + + "github.com/control-theory/gonzo/internal/engine" +) + +func (s *Server) handleStatus(w http.ResponseWriter, _ *http.Request) { + stats := s.engine.GetStats() + streams := s.engine.GetStreams() + + uptime := time.Since(stats.StartTime).Round(time.Second).String() + logRate := 0.0 + elapsed := time.Since(stats.StartTime).Seconds() + if elapsed > 0 { + logRate = float64(stats.TotalLogsEver) / elapsed + } + + streamInfos := make([]engine.StreamInfo, len(streams)) + copy(streamInfos, streams) + + writeJSON(w, engine.StatusInfo{ + Uptime: uptime, + TotalLogs: int64(stats.TotalLogsEver), + TotalBytes: stats.TotalBytes, + LogRate: logRate, + Streams: streamInfos, + BufferSize: stats.BufferSize, + BufferUsed: stats.BufferUsed, + AIConfigured: false, + }) +} + +func (s *Server) handleSeverity(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + data, err := s.engine.QuerySeverityData(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, data) +} + +func (s *Server) handleSentiment(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + data, err := s.engine.QuerySentimentData(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, data) +} + +func (s *Server) handlePatterns(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + data, err := s.engine.QueryPatterns(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, data) +} + +func (s *Server) handleClasses(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + data, err := s.engine.QueryClasses(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, data) +} + +func (s *Server) handleLogs(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + data, err := s.engine.QueryLogSamples(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, data) +} + +func (s *Server) handleHeatmap(w http.ResponseWriter, _ *http.Request) { + raw := s.engine.GetHeatmapData() + // Convert tui.HeatmapMinute (struct fields) to engine.HeatmapMinuteData (map) + data := make([]engine.HeatmapMinuteData, 0, len(raw)) + for _, m := range raw { + counts := map[string]int{ + "FATAL": m.Counts.Fatal, + "ERROR": m.Counts.Error, + "WARN": m.Counts.Warn, + "INFO": m.Counts.Info, + "DEBUG": m.Counts.Debug, + "TRACE": m.Counts.Trace, + } + data = append(data, engine.HeatmapMinuteData{ + Timestamp: m.Timestamp.Unix(), + Counts: counts, + }) + } + writeJSON(w, data) +} + +func (s *Server) handleAnomalies(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + report, err := s.engine.GetAnomalies(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, map[string]string{"report": report}) +} + +func (s *Server) handleStreams(w http.ResponseWriter, _ *http.Request) { + streams := s.engine.GetStreams() + writeJSON(w, streams) +} + +func (s *Server) handleInsightsParams(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + params, err := s.engine.QueryInsightsParams(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, params) +} + +func (s *Server) handleSeverityTimeSeries(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + writeJSON(w, s.engine.QuerySeverityTimeSeries(r.Context(), filters)) +} + +func (s *Server) handleTopAttributes(w http.ResponseWriter, r *http.Request) { + limitStr := r.URL.Query().Get("limit") + limit := 20 + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + } + + attrKeyCounts := s.engine.GetLifetimeAttrKeyCounts() + stats := s.engine.GetStats() + totalLogs := int64(stats.TotalLogsEver) + + // Flatten into entries and sort by count + var entries []engine.AttributeEntry + for key, vals := range attrKeyCounts { + for val, count := range vals { + pct := 0.0 + if totalLogs > 0 { + pct = float64(count) * 100.0 / float64(totalLogs) + } + entries = append(entries, engine.AttributeEntry{ + Key: key, + Value: val, + Count: count, + Percentage: pct, + }) + } + } + + // Sort descending by count + sort.Slice(entries, func(i, j int) bool { + return entries[i].Count > entries[j].Count + }) + + if len(entries) > limit { + entries = entries[:limit] + } + + writeJSON(w, entries) +} + +func (s *Server) handleSummary(w http.ResponseWriter, r *http.Request) { + filters := parseFilters(r) + summary, err := s.engine.QuerySummary(r.Context(), filters) + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + writeJSON(w, map[string]string{"summary": summary}) +} + +// parseFilters extracts InsightsFilters from query parameters. +func parseFilters(r *http.Request) engine.InsightsFilters { + q := r.URL.Query() + var filters engine.InsightsFilters + + if v := q.Get("start"); v != "" { + if ts, err := strconv.ParseInt(v, 10, 64); err == nil { + filters.Start = &ts + } + } + if v := q.Get("end"); v != "" { + if ts, err := strconv.ParseInt(v, 10, 64); err == nil { + filters.End = &ts + } + } + if v := q.Get("group_by"); v != "" { + filters.GroupBy = v + } + if v := q.Get("search"); v != "" { + filters.Search = &v + } + if v := q.Get("severity"); v != "" { + filters.Severity = &v + } + if v := q.Get("limit"); v != "" { + if limit, err := strconv.Atoi(v); err == nil { + filters.Limit = &limit + } + } + + return filters +} + +func (s *Server) handleReleases(w http.ResponseWriter, _ *http.Request) { + var rels interface{} + if s.relFetcher != nil { + rels = s.relFetcher.GetReleases() + } + writeJSON(w, map[string]interface{}{ + "version": s.version, + "releases": rels, + }) +} diff --git a/internal/web/server.go b/internal/web/server.go new file mode 100644 index 0000000..7b1046d --- /dev/null +++ b/internal/web/server.go @@ -0,0 +1,195 @@ +package web + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "log" + "net/http" + "time" + + "github.com/control-theory/gonzo/internal/engine" + "github.com/control-theory/gonzo/internal/releases" +) + +// Server is the HTTP server for the Dstl8 Lite web dashboard. +type Server struct { + engine *engine.Engine + httpServer *http.Server + hub *Hub + staticFS fs.FS // embedded React build assets (nil if not embedded) + version string + relFetcher *releases.Fetcher +} + +// NewServer creates a new web dashboard server. +func NewServer(eng *engine.Engine, staticFS fs.FS, version string, relFetcher *releases.Fetcher) *Server { + return &Server{ + engine: eng, + hub: NewHub(), + staticFS: staticFS, + version: version, + relFetcher: relFetcher, + } +} + +// Start starts the web server on the given port. +func (s *Server) Start(ctx context.Context, port int) error { + mux := http.NewServeMux() + + // API endpoints + mux.HandleFunc("GET /api/status", s.handleStatus) + mux.HandleFunc("GET /api/severity", s.handleSeverity) + mux.HandleFunc("GET /api/sentiment", s.handleSentiment) + mux.HandleFunc("GET /api/patterns", s.handlePatterns) + mux.HandleFunc("GET /api/classes", s.handleClasses) + mux.HandleFunc("GET /api/logs", s.handleLogs) + mux.HandleFunc("GET /api/heatmap", s.handleHeatmap) + mux.HandleFunc("GET /api/anomalies", s.handleAnomalies) + mux.HandleFunc("GET /api/streams", s.handleStreams) + mux.HandleFunc("POST /api/summary", s.handleSummary) + mux.HandleFunc("GET /api/insights-params", s.handleInsightsParams) + mux.HandleFunc("GET /api/severity-history", s.handleSeverityTimeSeries) + mux.HandleFunc("GET /api/top-attributes", s.handleTopAttributes) + mux.HandleFunc("GET /api/releases", s.handleReleases) + + // WebSocket endpoint + mux.HandleFunc("/ws", s.handleWebSocket) + + // Static assets (React app) + if s.staticFS != nil { + mux.Handle("/", spaHandler(s.staticFS)) + } else { + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, ` +

Dstl8 Lite

+

Web dashboard assets not embedded. Run make build to include them.

+

API available at /api/*

+`) + }) + } + + // Start WebSocket hub + go s.hub.Run(ctx) + + // Start broadcasting engine updates to WebSocket clients + go s.broadcastUpdates(ctx) + + addr := fmt.Sprintf("127.0.0.1:%d", port) + s.httpServer = &http.Server{ + Addr: addr, + Handler: corsMiddleware(mux), + ReadTimeout: 30 * time.Second, + WriteTimeout: 60 * time.Second, + IdleTimeout: 120 * time.Second, + } + + errChan := make(chan error, 1) + go func() { + if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + errChan <- err + } + }() + + select { + case err := <-errChan: + return fmt.Errorf("web server failed to start: %w", err) + case <-time.After(100 * time.Millisecond): + // Started successfully + } + + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + s.httpServer.Shutdown(shutdownCtx) + }() + + return nil +} + +// broadcastUpdates periodically sends engine state updates to WebSocket clients. +func (s *Server) broadcastUpdates(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + lastLogCount := 0 + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + stats := s.engine.GetStats() + if stats.TotalLogsEver == lastLogCount { + continue // No new data + } + lastLogCount = stats.TotalLogsEver + + // Build delta update + update := map[string]interface{}{ + "type": "update", + "total_logs": stats.TotalLogsEver, + "buffer_used": stats.BufferUsed, + } + + data, err := json.Marshal(update) + if err != nil { + continue + } + s.hub.Broadcast(data) + } + } +} + +// spaHandler serves static files and falls back to index.html for SPA routing. +func spaHandler(staticFS fs.FS) http.Handler { + fileServer := http.FileServerFS(staticFS) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Try to serve the file directly + path := r.URL.Path + if path == "/" { + path = "index.html" + } else if path[0] == '/' { + path = path[1:] + } + + // Check if file exists + if _, err := fs.Stat(staticFS, path); err == nil { + fileServer.ServeHTTP(w, r) + return + } + + // SPA fallback — serve index.html + r.URL.Path = "/" + fileServer.ServeHTTP(w, r) + }) +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +func writeJSON(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + log.Printf("Error writing JSON response: %v", err) + } +} + +func writeError(w http.ResponseWriter, code int, msg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(map[string]string{"error": msg}) +} diff --git a/internal/web/websocket.go b/internal/web/websocket.go new file mode 100644 index 0000000..cdb3791 --- /dev/null +++ b/internal/web/websocket.go @@ -0,0 +1,133 @@ +package web + +import ( + "context" + "log" + "net/http" + "sync" + "time" + + "nhooyr.io/websocket" +) + +// Hub manages WebSocket client connections and broadcasts messages. +type Hub struct { + mu sync.RWMutex + clients map[*Client]bool +} + +// Client represents a single WebSocket connection. +type Client struct { + conn *websocket.Conn + send chan []byte +} + +// NewHub creates a new WebSocket hub. +func NewHub() *Hub { + return &Hub{ + clients: make(map[*Client]bool), + } +} + +// Run starts the hub, cleaning up closed connections. +func (h *Hub) Run(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + h.mu.Lock() + for c := range h.clients { + close(c.send) + delete(h.clients, c) + } + h.mu.Unlock() + return + case <-ticker.C: + // Periodic cleanup handled by write pump detecting closed conns + } + } +} + +// Register adds a client to the hub. +func (h *Hub) Register(c *Client) { + h.mu.Lock() + h.clients[c] = true + h.mu.Unlock() +} + +// Unregister removes a client from the hub. +func (h *Hub) Unregister(c *Client) { + h.mu.Lock() + if _, ok := h.clients[c]; ok { + close(c.send) + delete(h.clients, c) + } + h.mu.Unlock() +} + +// Broadcast sends a message to all connected clients. +func (h *Hub) Broadcast(msg []byte) { + h.mu.RLock() + defer h.mu.RUnlock() + + for c := range h.clients { + select { + case c.send <- msg: + default: + // Client buffer full, skip + } + } +} + +// handleWebSocket handles WebSocket upgrade and manages the connection. +func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + InsecureSkipVerify: true, // Allow any origin (localhost only) + }) + if err != nil { + log.Printf("WebSocket accept error: %v", err) + return + } + + client := &Client{ + conn: conn, + send: make(chan []byte, 64), + } + s.hub.Register(client) + + ctx := r.Context() + + // Write pump + go func() { + defer func() { + s.hub.Unregister(client) + conn.Close(websocket.StatusNormalClosure, "") + }() + for { + select { + case <-ctx.Done(): + return + case msg, ok := <-client.send: + if !ok { + return + } + writeCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + err := conn.Write(writeCtx, websocket.MessageText, msg) + cancel() + if err != nil { + return + } + } + } + }() + + // Read pump (just consume and discard — we don't expect client messages) + for { + _, _, err := conn.Read(ctx) + if err != nil { + break + } + } +} diff --git a/web/Dstl8_logo_dark.svg b/web/Dstl8_logo_dark.svg new file mode 100644 index 0000000..8a0ddd8 --- /dev/null +++ b/web/Dstl8_logo_dark.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/Dstl8_logo_light.svg b/web/Dstl8_logo_light.svg new file mode 100644 index 0000000..094c505 --- /dev/null +++ b/web/Dstl8_logo_light.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..5f89293 --- /dev/null +++ b/web/embed.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed all:dist +var DistFS embed.FS diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..0d7066f --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + Dstl8 Lite — powered by Gonzo + + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..6d16826 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,4283 @@ +{ + "name": "dstl8-lite", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dstl8-lite", + "version": "0.0.1", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "recharts": "^2.15.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "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", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "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==", + "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", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-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.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.9.tgz", + "integrity": "sha512-OZd0e2mU11ClX8+IdXe3r0dbqMEznRiT4TfbhYIbcRPZkqJ7Qwer8ij3GZAmLsRKa+II9V1v5czCkvmHH3XZBg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "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-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "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/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "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-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ba2267a --- /dev/null +++ b/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "dstl8-lite", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "recharts": "^2.15.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@vitejs/plugin-react": "^4.3.0", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.0", + "vite": "^6.0.0" + } +} diff --git a/web/postcss.config.js b/web/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/web/public/Dstl8_Gradient_Dark.svg b/web/public/Dstl8_Gradient_Dark.svg new file mode 100644 index 0000000..8a0ddd8 --- /dev/null +++ b/web/public/Dstl8_Gradient_Dark.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/Dstl8_logo_dark.svg b/web/public/Dstl8_logo_dark.svg new file mode 100644 index 0000000..8a0ddd8 --- /dev/null +++ b/web/public/Dstl8_logo_dark.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/Dstl8_logo_light.svg b/web/public/Dstl8_logo_light.svg new file mode 100644 index 0000000..094c505 --- /dev/null +++ b/web/public/Dstl8_logo_light.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/Gonzo-Profile.png b/web/public/Gonzo-Profile.png new file mode 100644 index 0000000..279a98b Binary files /dev/null and b/web/public/Gonzo-Profile.png differ diff --git a/web/public/MCP_icon.png b/web/public/MCP_icon.png new file mode 100644 index 0000000..783e179 Binary files /dev/null and b/web/public/MCP_icon.png differ diff --git a/web/public/Mobius_Circle.svg b/web/public/Mobius_Circle.svg new file mode 100644 index 0000000..850dc7a --- /dev/null +++ b/web/public/Mobius_Circle.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/public/Mobius_Square_dark.svg b/web/public/Mobius_Square_dark.svg new file mode 100644 index 0000000..c7ac06e --- /dev/null +++ b/web/public/Mobius_Square_dark.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..8093da9 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,159 @@ +import { useState, useEffect, useCallback } from 'react' +import { ThemeProvider } from './components/ui/ThemeProvider' +import { AppHeader } from './components/layout/AppHeader' +import { AppSidebar } from './components/layout/AppSidebar' +import { WhatsNewModal } from './components/ui/WhatsNewModal' +import { WorkspacesPage } from './pages/WorkspacesPage' +import { WorkspaceDetailsPage } from './pages/WorkspaceDetailsPage' +import { StreamDetailsPage } from './pages/StreamDetailsPage' +import { SourcesPage } from './pages/SourcesPage' +import { HeatmapPage } from './pages/HeatmapPage' +import { LogViewerPage } from './pages/LogViewerPage' +import { UpgradePage } from './pages/UpgradePage' +import { fetchReleases } from './api/client' +import type { ReleaseInfo } from './api/types' + +type Page = + | { type: 'workspaces' } + | { type: 'workspace-details' } + | { type: 'logs' } + | { type: 'sources' } + | { type: 'heatmap' } + | { type: 'stream-details'; dimensionType: string; dimensionValue: string } + | { type: 'upgrade'; featureId: string } + +const LAST_SEEN_KEY = 'gonzo-last-seen-version' + +// Strip git describe suffixes: "v0.3.1-13-g58cce74-dirty" -> "v0.3.1" +function baseVersion(v: string): string { + const s = v.startsWith('v') ? v.slice(1) : v + const base = s.split('-')[0] + return v.startsWith('v') ? `v${base}` : base +} + +export default function App() { + const [page, setPage] = useState({ type: 'workspaces' }) + const [releasesCache, setReleasesCache] = useState<{ version: string; releases: ReleaseInfo[] } | null>(null) + const [showWhatsNew, setShowWhatsNew] = useState(false) + + // Check for new releases on mount + useEffect(() => { + fetchReleases() + .then((data) => { + if (!data.releases?.length || !data.version || data.version === 'dev') return + setReleasesCache({ version: data.version, releases: data.releases }) + const lastSeen = localStorage.getItem(LAST_SEEN_KEY) + if (lastSeen !== baseVersion(data.version)) { + setShowWhatsNew(true) + } + }) + .catch(() => {}) + }, []) + + const dismissWhatsNew = useCallback(() => { + if (releasesCache) { + localStorage.setItem(LAST_SEEN_KEY, baseVersion(releasesCache.version)) + } + setShowWhatsNew(false) + }, [releasesCache]) + + const openWhatsNew = useCallback(() => { + setShowWhatsNew(true) + }, []) + + const handleNavigate = useCallback((target: string) => { + if (target.startsWith('upgrade:')) { + setPage({ type: 'upgrade', featureId: target.replace('upgrade:', '') }) + } else if (target === 'workspaces') { + setPage({ type: 'workspaces' }) + } else if (target === 'workspace-details') { + setPage({ type: 'workspace-details' }) + } else if (target === 'logs') { + setPage({ type: 'logs' }) + } else if (target === 'sources') { + setPage({ type: 'sources' }) + } else if (target === 'heatmap') { + setPage({ type: 'heatmap' }) + } + }, []) + + const handleOpenStream = useCallback((dimensionType: string, dimensionValue: string) => { + setPage({ type: 'stream-details', dimensionType, dimensionValue }) + }, []) + + const activeSidebarPage = + page.type === 'upgrade' + ? page.featureId + : page.type === 'workspace-details' || page.type === 'stream-details' + ? 'workspaces' + : page.type + + const breadcrumbs = + page.type === 'workspace-details' + ? [ + { label: 'Workspaces', onClick: () => setPage({ type: 'workspaces' }) }, + { label: 'Gonzo!' }, + ] + : page.type === 'stream-details' + ? [ + { label: 'Workspaces', onClick: () => setPage({ type: 'workspaces' }) }, + { label: 'Gonzo!', onClick: () => setPage({ type: 'workspace-details' }) }, + { label: page.dimensionValue }, + ] + : page.type === 'logs' + ? [{ label: 'Logs' }] + : page.type === 'sources' + ? [{ label: 'Sources' }] + : page.type === 'heatmap' + ? [{ label: 'Heatmap' }] + : page.type === 'upgrade' + ? [ + { label: 'Workspaces', onClick: () => setPage({ type: 'workspaces' }) }, + { label: page.featureId.charAt(0).toUpperCase() + page.featureId.slice(1) }, + ] + : [] + + return ( + +
+ setPage({ type: 'workspaces' })} /> +
+ +
+ {page.type === 'workspaces' && ( + setPage({ type: 'workspace-details' })} + /> + )} + {page.type === 'workspace-details' && ( + + )} + {page.type === 'logs' && } + {page.type === 'sources' && ( + + )} + {page.type === 'heatmap' && } + {page.type === 'stream-details' && ( + + )} + {page.type === 'upgrade' && } +
+
+
+ {showWhatsNew && releasesCache && ( + + )} +
+ ) +} diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..7d5ed6f --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,101 @@ +import { API_BASE } from '../lib/constants' +import type { + StatusInfo, + StreamInfo, + SeverityGroup, + SentimentData, + PatternGroup, + ClassItem, + LogSample, + HeatmapMinuteData, + SeverityTimePoint, + InsightsParams, +} from './types' + +async function fetchJSON(path: string, init?: RequestInit): Promise { + const res = await fetch(`${API_BASE}${path}`, init) + if (!res.ok) { + const body = await res.text() + throw new Error(`API ${path}: ${res.status} ${body}`) + } + return res.json() +} + +export function fetchStatus(): Promise { + return fetchJSON('/status') +} + +export async function fetchStreams(): Promise { + return (await fetchJSON('/streams')) ?? [] +} + +export async function fetchSeverity(opts?: { groupBy?: string; search?: string }): Promise { + const params = new URLSearchParams() + if (opts?.groupBy) params.set('group_by', opts.groupBy) + if (opts?.search) params.set('search', opts.search) + const q = params.toString() + return (await fetchJSON(`/severity${q ? '?' + q : ''}`)) ?? [] +} + +export async function fetchSentiment(groupBy?: string): Promise { + const q = groupBy ? `?group_by=${groupBy}` : '' + const data = await fetchJSON(`/sentiment${q}`) + return data ?? { group_values: [], buckets: [] } +} + +export async function fetchPatterns(opts?: { severity?: string; search?: string }): Promise { + const params = new URLSearchParams() + if (opts?.severity) params.set('severity', opts.severity) + if (opts?.search) params.set('search', opts.search) + const q = params.toString() + return (await fetchJSON(`/patterns${q ? '?' + q : ''}`)) ?? [] +} + +export async function fetchClasses(): Promise { + return (await fetchJSON('/classes')) ?? [] +} + +export async function fetchLogs(opts?: { + severity?: string + search?: string + limit?: number +}): Promise { + const params = new URLSearchParams() + if (opts?.severity) params.set('severity', opts.severity) + if (opts?.search) params.set('search', opts.search) + if (opts?.limit) params.set('limit', String(opts.limit)) + const q = params.toString() + return (await fetchJSON(`/logs${q ? '?' + q : ''}`)) ?? [] +} + +export async function fetchHeatmap(): Promise { + return (await fetchJSON('/heatmap')) ?? [] +} + +export function fetchAnomalies(): Promise<{ report: string }> { + return fetchJSON('/anomalies') +} + +export function fetchSummary(): Promise<{ summary: string }> { + return fetchJSON('/summary', { method: 'POST' }) +} + +export async function fetchSeverityHistory(opts?: { search?: string }): Promise { + const params = new URLSearchParams() + if (opts?.search) params.set('search', opts.search) + const q = params.toString() + return (await fetchJSON(`/severity-history${q ? '?' + q : ''}`)) ?? [] +} + +export async function fetchInsightsParams(): Promise { + const data = await fetchJSON('/insights-params') + return data ?? { total_logs: 0 } +} + +export async function fetchTopAttributes(limit = 20): Promise { + return (await fetchJSON(`/top-attributes?limit=${limit}`)) ?? [] +} + +export function fetchReleases(): Promise { + return fetchJSON('/releases') +} diff --git a/web/src/api/types.ts b/web/src/api/types.ts new file mode 100644 index 0000000..e19b4cf --- /dev/null +++ b/web/src/api/types.ts @@ -0,0 +1,117 @@ +// TypeScript types matching Go engine/types.go response shapes + +export interface StatusInfo { + uptime: string + total_logs: number + total_bytes: number + log_rate: number + streams: StreamInfo[] + buffer_size: number + buffer_used: number + ai_configured: boolean +} + +export interface StreamInfo { + source: string + stream: string + log_count: number + last_seen: string + active: boolean +} + +export interface LogSample { + timestamp: number + severity: string + message: string + attributes?: Record + raw_line?: string +} + +export interface SeverityGroup { + group_value: string + counts: Record + total: number +} + +export interface SentimentBucket { + timestamp: number + group_value: string + sentiment: number + is_anomaly?: boolean + log_count: number +} + +export interface SentimentData { + group_values: string[] + buckets: SentimentBucket[] +} + +export interface PatternItem { + pattern: string + count: number + percentage: number + sample?: string +} + +export interface PatternGroup { + group_value: string + patterns: PatternItem[] +} + +export interface ClassItem { + name: string + count: number + percentage: number +} + +export interface HeatmapMinuteData { + timestamp: number + counts: Record +} + +export interface InsightsParams { + environments?: string[] + clusters?: string[] + namespaces?: string[] + deployments?: string[] + pods?: string[] + hosts?: string[] + services?: string[] + severities?: string[] + categories?: string[] + total_logs: number + oldest_log?: number + newest_log?: number +} + +export interface SeverityTimePoint { + timestamp: number + counts: Record + total: number +} + +export interface AttributeEntry { + key: string + value: string + count: number + percentage: number +} + +export interface WebSocketUpdate { + type: string + total_logs: number + buffer_used: number +} + +export interface ReleaseInfo { + tag_name: string + name: string + body: string + published_at: string + url: string +} + +export interface ReleasesResponse { + version: string + releases: ReleaseInfo[] | null +} diff --git a/web/src/api/websocket.ts b/web/src/api/websocket.ts new file mode 100644 index 0000000..6845ada --- /dev/null +++ b/web/src/api/websocket.ts @@ -0,0 +1,57 @@ +import type { WebSocketUpdate } from './types' + +type UpdateHandler = (update: WebSocketUpdate) => void + +export class DashboardWebSocket { + private ws: WebSocket | null = null + private handlers: Set = new Set() + private reconnectTimer: ReturnType | null = null + private url: string + + constructor() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + this.url = `${protocol}//${window.location.host}/ws` + } + + connect() { + if (this.ws?.readyState === WebSocket.OPEN) return + + this.ws = new WebSocket(this.url) + this.ws.onmessage = (event) => { + try { + const update: WebSocketUpdate = JSON.parse(event.data) + this.handlers.forEach((h) => h(update)) + } catch { + // ignore malformed messages + } + } + this.ws.onclose = () => { + this.scheduleReconnect() + } + this.ws.onerror = () => { + this.ws?.close() + } + } + + disconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer) + this.reconnectTimer = null + } + this.ws?.close() + this.ws = null + } + + subscribe(handler: UpdateHandler): () => void { + this.handlers.add(handler) + return () => this.handlers.delete(handler) + } + + private scheduleReconnect() { + if (this.reconnectTimer) return + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null + this.connect() + }, 2000) + } +} diff --git a/web/src/components/layout/AppHeader.tsx b/web/src/components/layout/AppHeader.tsx new file mode 100644 index 0000000..4f4d2c2 --- /dev/null +++ b/web/src/components/layout/AppHeader.tsx @@ -0,0 +1,93 @@ +import { useTheme } from '../../hooks/useTheme' +import { Icons } from '../../lib/icons' + +interface Breadcrumb { + label: string + onClick?: () => void +} + +interface AppHeaderProps { + breadcrumbs?: Breadcrumb[] + onLogoClick?: () => void +} + +export function AppHeader({ breadcrumbs = [], onLogoClick }: AppHeaderProps) { + const { theme, toggle } = useTheme() + + return ( +
+
+ + Dstl8 + + + + Lite + + + + Powered by Gonzo + + + + {breadcrumbs.length > 0 && ( +
+ {breadcrumbs.map((crumb, i) => { + const isLast = i === breadcrumbs.length - 1 + return ( + + {i > 0 && /} + {isLast ? ( + {crumb.label} + ) : ( + + {crumb.label} + + )} + + ) + })} +
+ )} +
+
+ +
+ Gonzo +
+ Gonzo + Local +
+
+
+
+ ) +} diff --git a/web/src/components/layout/AppSidebar.tsx b/web/src/components/layout/AppSidebar.tsx new file mode 100644 index 0000000..7f020d9 --- /dev/null +++ b/web/src/components/layout/AppSidebar.tsx @@ -0,0 +1,133 @@ +import { useState, useCallback } from 'react' +import { Icons } from '../../lib/icons' +import { upsellURL } from '../../lib/constants' + +interface AppSidebarProps { + activePage: string + onNavigate: (page: string) => void + onWhatsNew?: () => void +} + +const mainNav = [ + { id: 'workspaces', label: 'Workspaces', icon: Icons.workspaces, locked: false }, + { id: 'alerts', label: 'Alerts', icon: Icons.incidents, locked: true }, + { id: 'logs', label: 'Logs', icon: Icons.logs, locked: false }, + { id: 'sources', label: 'Sources', icon: Icons.sources, locked: false }, + { id: 'heatmap', label: 'Heatmap', icon: Icons.heatmap, locked: false }, + { id: 'users', label: 'Users', icon: Icons.users, locked: true }, + { id: 'mcp', label: 'MCP', icon: null, locked: true, useImage: true }, +] + +const bottomNav = [ + { id: 'retention', label: 'Retention', icon: Icons.settings, locked: true }, +] + +export function AppSidebar({ activePage, onNavigate, onWhatsNew }: AppSidebarProps) { + const [showMcpModal, setShowMcpModal] = useState(false) + const dismissMcpModal = useCallback(() => setShowMcpModal(false), []) + + return ( + + ) +} diff --git a/web/src/components/layout/DashboardLayout.tsx b/web/src/components/layout/DashboardLayout.tsx new file mode 100644 index 0000000..5da025c --- /dev/null +++ b/web/src/components/layout/DashboardLayout.tsx @@ -0,0 +1,21 @@ +import type { ReactNode } from 'react' + +interface DashboardLayoutProps { + header: ReactNode + footer: ReactNode + children: ReactNode +} + +export function DashboardLayout({ header, footer, children }: DashboardLayoutProps) { + return ( +
+ {header} +
+
+ {children} +
+
+ {footer} +
+ ) +} diff --git a/web/src/components/layout/Footer.tsx b/web/src/components/layout/Footer.tsx new file mode 100644 index 0000000..4db3be5 --- /dev/null +++ b/web/src/components/layout/Footer.tsx @@ -0,0 +1,37 @@ +import { useTheme } from '../../hooks/useTheme' +import { upsellURL } from '../../lib/constants' + +export function Footer() { + const { theme } = useTheme() + + return ( + + ) +} diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx new file mode 100644 index 0000000..ebbe1bc --- /dev/null +++ b/web/src/components/layout/Header.tsx @@ -0,0 +1,51 @@ +import { useTheme } from '../../hooks/useTheme' +import { upsellURL } from '../../lib/constants' +import { Button } from '../ui/Button' +import type { StatusInfo } from '../../api/types' + +interface HeaderProps { + status: StatusInfo | null +} + +export function Header({ status }: HeaderProps) { + const { theme, toggle } = useTheme() + + return ( +
+
+
+ Dstl8 +
+ + Lite + +
+ Powered by Gonzo +
+
+
+ {status && ( +
+ {status.total_logs.toLocaleString()} logs + {status.log_rate.toFixed(1)}/s + {status.uptime} uptime +
+ )} +
+
+ + + + +
+
+ ) +} diff --git a/web/src/components/layout/MobiusChat.tsx b/web/src/components/layout/MobiusChat.tsx new file mode 100644 index 0000000..968beb2 --- /dev/null +++ b/web/src/components/layout/MobiusChat.tsx @@ -0,0 +1,68 @@ +import { upsellURL } from '../../lib/constants' + +export function MobiusChat() { + return ( +
+
+ Möbius +
+
+ Möbius +
+
+
+ Online +
+
+
+ +
+
+ Möbius +
+
+ Welcome to Dstl8 Lite! I'm Möbius, your AI log analyst. + I can help you understand patterns, anomalies, and issues in your log data. +
+
Just now
+
+
+ +
+ Möbius +
+
+ For continuous AI analysis and real-time insights,{' '} + + upgrade to Dstl8 Pro + {' '} + to unlock the full Möbius experience. +
+
Just now
+
+
+
+ +
+
+ + +
+
+
+ ) +} diff --git a/web/src/components/panels/AISummaryPanel.tsx b/web/src/components/panels/AISummaryPanel.tsx new file mode 100644 index 0000000..7495427 --- /dev/null +++ b/web/src/components/panels/AISummaryPanel.tsx @@ -0,0 +1,79 @@ +import { useState } from 'react' +import { fetchSummary } from '../../api/client' +import { upsellURL } from '../../lib/constants' +import { Card, CardHeader, CardTitle } from '../ui/Card' +import { Button } from '../ui/Button' + +export function AISummaryPanel() { + const [summary, setSummary] = useState(null) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + const handleAnalyze = () => { + setLoading(true) + setError(null) + fetchSummary() + .then((res) => setSummary(res.summary)) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)) + } + + return ( + + + AI Analysis + + {summary ? ( +
+

+ {summary} +

+ +
+ ) : error ? ( +
+

{error}

+
+

+ AI analysis requires a configured AI provider. + Configure --ai-provider or try{' '} + + Mobius on Dstl8 Pro + + {' '}for built-in AI analysis. +

+
+
+ ) : ( +
+

+ Generate an AI-powered summary of your current log data. +

+ +
+

+ Want continuous AI analysis?{' '} + + Try Mobius on Dstl8 Pro + +

+
+
+ )} +
+ ) +} diff --git a/web/src/components/panels/HeatmapPanel.tsx b/web/src/components/panels/HeatmapPanel.tsx new file mode 100644 index 0000000..dab6f14 --- /dev/null +++ b/web/src/components/panels/HeatmapPanel.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react' +import { fetchHeatmap } from '../../api/client' +import type { HeatmapMinuteData } from '../../api/types' +import { SEVERITY_COLORS, SEVERITY_ORDER } from '../../lib/colors' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +interface Props { + refreshKey: number +} + +export function HeatmapPanel({ refreshKey }: Props) { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchHeatmap() + .then(setData) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey]) + + if (loading) { + return ( + + Severity Heatmap +
+ Loading... +
+
+ ) + } + + // Show last 60 minutes (or less if less data available) + const minutes = data.slice(-60) + const safeCounts = (m: HeatmapMinuteData) => + m.counts ? Object.values(m.counts).reduce((a, b) => a + b, 0) : 0 + const maxCount = minutes.length > 0 + ? Math.max(1, ...minutes.map(safeCounts)) + : 1 + + return ( + + + Severity Heatmap + + {minutes.length} min + + + {minutes.length === 0 ? ( +
+ No data yet +
+ ) : ( +
+
+ {minutes.map((minute, i) => { + const counts = minute.counts || {} + const total = Object.values(counts).reduce((a, b) => a + b, 0) + const opacity = Math.max(0.1, total / maxCount) + // Dominant severity determines color + const dominant = SEVERITY_ORDER.find((s) => (counts[s] || 0) > 0) || 'INFO' + const color = SEVERITY_COLORS[dominant] || SEVERITY_COLORS.UNKNOWN + const time = new Date(minute.timestamp * 1000) + const label = `${time.getHours().toString().padStart(2, '0')}:${time.getMinutes().toString().padStart(2, '0')}` + return ( +
+
+ {i % 10 === 0 && ( + + {label} + + )} +
+ ) + })} +
+
+ {SEVERITY_ORDER.slice(0, 5).map((s) => ( + + + {s} + + ))} +
+
+ )} + + ) +} diff --git a/web/src/components/panels/LogViewer.tsx b/web/src/components/panels/LogViewer.tsx new file mode 100644 index 0000000..16e7601 --- /dev/null +++ b/web/src/components/panels/LogViewer.tsx @@ -0,0 +1,203 @@ +import { useEffect, useState, useRef, useCallback } from 'react' +import { fetchLogs } from '../../api/client' +import type { LogSample } from '../../api/types' +import { severityColor } from '../../lib/colors' +import { Card, CardHeader, CardTitle } from '../ui/Card' +import { Button } from '../ui/Button' + +interface Props { + refreshKey: number + search?: string + compact?: boolean + fullHeight?: boolean +} + +export function LogViewer({ refreshKey, search, compact, fullHeight }: Props) { + const [logs, setLogs] = useState([]) + const [loading, setLoading] = useState(true) + const [autoScroll, setAutoScroll] = useState(true) + const [filter, setFilter] = useState('') + const [expandedRow, setExpandedRow] = useState(null) + const listRef = useRef(null) + const autoScrollRef = useRef(true) + + // Combine external search with local filter + const combinedSearch = [search, filter].filter(Boolean).join(' ') || undefined + + const toggleAutoScroll = useCallback(() => { + setAutoScroll((prev) => { + const next = !prev + autoScrollRef.current = next + if (next) { + // Resuming: fetch fresh data and scroll to bottom + fetchLogs({ limit: 200, search: combinedSearch }) + .then((data) => { + setLogs(data) + requestAnimationFrame(() => { + if (listRef.current) { + listRef.current.scrollTop = listRef.current.scrollHeight + } + }) + }) + .catch(() => {}) + } + return next + }) + }, [combinedSearch]) + + useEffect(() => { + // When paused (autoScroll off), don't fetch new data — only fetch on search change + if (!autoScrollRef.current && !loading) return + fetchLogs({ limit: 200, search: combinedSearch }) + .then(setLogs) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey, combinedSearch]) + + useEffect(() => { + if (autoScrollRef.current && listRef.current) { + const el = listRef.current + requestAnimationFrame(() => { + el.scrollTop = el.scrollHeight + }) + } + }, [logs]) + + const formatTime = (ts: number) => { + const d = new Date(ts * 1000) + return d.toLocaleTimeString() + } + + const handleRowClick = (index: number) => { + if (compact) return // no expand in compact mode + setExpandedRow(expandedRow === index ? null : index) + } + + const renderLogRow = (log: LogSample, i: number, isCompact: boolean) => { + const isExpanded = !isCompact && expandedRow === i + return ( + handleRowClick(i)} + > + + {formatTime(log.timestamp)} + + + + {log.severity} + + + + {isExpanded ? ( +
+ {log.message} + {log.attributes && Object.keys(log.attributes).length > 0 && ( +
+ {Object.entries(log.attributes).map(([k, v]) => ( +
+ {k}: {v} +
+ ))} +
+ )} +
+ ) : ( + {log.message} + )} + + + ) + } + + const renderEmpty = () => ( + loading ? ( +
+ Loading... +
+ ) : logs.length === 0 ? ( +
+ No logs yet +
+ ) : null + ) + + // Compact mode: no card wrapper, no header, constrained height + if (compact) { + return ( +
+ {renderEmpty() || ( + <> + + + {logs.map((log, i) => renderLogRow(log, i, true))} + +
+
+ + )} +
+ ) + } + + const heightClass = fullHeight ? 'h-full' : 'h-64' + + return ( + + + Log Viewer +
+ setFilter(e.target.value)} + className="rounded border border-[var(--color-border)] bg-[var(--color-bg)] px-2 py-1 text-xs text-[var(--color-text)] outline-none focus:border-[var(--color-accent)]" + /> + + + {logs.length} entries + +
+
+
+ {renderEmpty() || ( + <> + + + {logs.map((log, i) => renderLogRow(log, i, false))} + +
+
+ + )} +
+ + ) +} diff --git a/web/src/components/panels/PatternAnalysis.tsx b/web/src/components/panels/PatternAnalysis.tsx new file mode 100644 index 0000000..51d9eaf --- /dev/null +++ b/web/src/components/panels/PatternAnalysis.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react' +import { fetchPatterns } from '../../api/client' +import type { PatternGroup } from '../../api/types' +import { SeverityBadge } from '../ui/SeverityBadge' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +interface Props { + refreshKey: number + search?: string +} + +export function PatternAnalysis({ refreshKey, search }: Props) { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchPatterns({ search }) + .then(setData) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey, search]) + + const allPatterns = data.flatMap((g) => + g.patterns.map((p) => ({ ...p, group: g.group_value })) + ) + + // Sort by count descending, take top 20 + const topPatterns = allPatterns + .sort((a, b) => b.count - a.count) + .slice(0, 20) + + return ( + + + Patterns + + {allPatterns.length} patterns detected + + + {loading ? ( +
+ Loading... +
+ ) : topPatterns.length === 0 ? ( +
+ No patterns detected yet +
+ ) : ( +
+ + + + + + + + + + + {topPatterns.map((p, i) => ( + + + + + + + ))} + +
GroupPatternCount%
+ + + {p.pattern} + + {p.count.toLocaleString()} + + {p.percentage.toFixed(1)}% +
+
+ )} +
+ ) +} diff --git a/web/src/components/panels/SeverityDistribution.tsx b/web/src/components/panels/SeverityDistribution.tsx new file mode 100644 index 0000000..eb49265 --- /dev/null +++ b/web/src/components/panels/SeverityDistribution.tsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react' +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + Legend, +} from 'recharts' +import { fetchSeverityHistory } from '../../api/client' +import type { SeverityTimePoint } from '../../api/types' +import { SEVERITY_COLORS, SEVERITY_ORDER } from '../../lib/colors' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +interface Props { + refreshKey: number + search?: string +} + +export function SeverityDistribution({ refreshKey, search }: Props) { + const [data, setData] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchSeverityHistory(search ? { search } : undefined) + .then(setData) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey, search]) + + if (loading) { + return ( + + Log Severity +
+ Loading... +
+
+ ) + } + + // Build chart data from per-second time points + const chartData: Record[] = data.map((point) => { + const d = new Date(point.timestamp * 1000) + const h = d.getHours().toString().padStart(2, '0') + const m = d.getMinutes().toString().padStart(2, '0') + const s = d.getSeconds().toString().padStart(2, '0') + return { + name: `${h}:${m}:${s}`, + ...point.counts, + total: point.total, + } + }) + + const totalLogs = chartData.reduce((sum, d) => sum + ((d.total as number) || 0), 0) + + // Determine which severities are present + const activeSeverities = SEVERITY_ORDER.filter((sev) => + chartData.some((d) => ((d[sev] as number) || 0) > 0) + ) + + return ( + + + Log Severity + + {totalLogs.toLocaleString()} logs · {data.length}s + + + {chartData.length === 0 ? ( +
+ No data yet +
+ ) : ( + + + + + + + {activeSeverities.map((sev, i) => ( + + ))} + + + )} +
+ ) +} diff --git a/web/src/components/panels/SourcesSection.tsx b/web/src/components/panels/SourcesSection.tsx new file mode 100644 index 0000000..d6c2d3c --- /dev/null +++ b/web/src/components/panels/SourcesSection.tsx @@ -0,0 +1,478 @@ +import { useEffect, useState } from 'react' +import { fetchStreams, fetchStatus, fetchInsightsParams } from '../../api/client' +import type { StreamInfo, StatusInfo, InsightsParams } from '../../api/types' +import { upsellURL } from '../../lib/constants' + +interface Props { + refreshKey: number + onOpenStream?: (dimensionType: string, dimensionValue: string) => void +} + +/** Group streams by their `source` field (e.g. "file", "k8s", "otlp", "stdin"). */ +interface SourceGroup { + source: string + streams: StreamInfo[] + totalLogs: number + activeCount: number +} + +/** A dimension group like "Host", "Service", "Namespace", etc. */ +interface DimensionGroup { + label: string + values: { name: string; logCount: number; active: boolean }[] +} + +function groupStreams(streams: StreamInfo[]): SourceGroup[] { + const map = new Map() + for (const s of streams) { + const key = s.source || 'unknown' + if (!map.has(key)) map.set(key, []) + map.get(key)!.push(s) + } + const groups: SourceGroup[] = [] + for (const [source, items] of map) { + groups.push({ + source, + streams: items, + totalLogs: items.reduce((sum, s) => sum + s.log_count, 0), + activeCount: items.filter((s) => s.active).length, + }) + } + groups.sort((a, b) => b.totalLogs - a.totalLogs) + return groups +} + +/** Build dimension groups from insights params. */ +function buildDimensionGroups(params: InsightsParams, streams: StreamInfo[]): DimensionGroup[] { + const groups: DimensionGroup[] = [] + + // Determine which streams are active by name + const activeStreams = new Set(streams.filter((s) => s.active).map((s) => s.stream)) + + const addGroup = (label: string, values: string[] | undefined) => { + if (!values || values.length === 0) return + groups.push({ + label, + values: values.map((v) => ({ + name: v, + logCount: 0, // We don't have per-dimension counts from insights params + active: activeStreams.size > 0, // Approximate: if any stream is active + })), + }) + } + + addGroup('Host', params.hosts) + addGroup('Service', params.services) + addGroup('Deployment', params.deployments) + addGroup('Namespace', params.namespaces) + addGroup('Pod', params.pods) + addGroup('Environment', params.environments) + addGroup('Cluster', params.clusters) + + return groups +} + +function SourceCard({ group, expanded, onToggle, dimensionGroups, onOpenStream }: { + group: SourceGroup + expanded: boolean + onToggle: () => void + dimensionGroups: DimensionGroup[] + onOpenStream?: (dimensionType: string, dimensionValue: string) => void +}) { + const isHealthy = group.activeCount > 0 + const statusClass = isHealthy ? 'ok' : '' + const [collapsedDims, setCollapsedDims] = useState>(new Set()) + + const toggleDim = (label: string) => { + setCollapsedDims((prev) => { + const next = new Set(prev) + if (next.has(label)) next.delete(label) + else next.add(label) + return next + }) + } + + const totalStreamCount = dimensionGroups.reduce((sum, g) => sum + g.values.length, 0) + + return ( +
+ {/* Main card */} +
+
+
+
+
{group.source}
+ + ▼ + +
+ +
+
+
Streams
+
{group.streams.length}
+
+
+
Logs
+
{group.totalLogs.toLocaleString()}
+
+
+
Active
+
0 ? 'var(--health-ok)' : 'var(--text-muted)', + }}> + {group.activeCount} +
+
+
+
+ +
+
+ Stream Info +
+
+ {group.activeCount} of {group.streams.length} streams active +
+
+ {group.totalLogs.toLocaleString()} total logs ingested +
+
+
+ + {/* Expanded: dimension groups */} + {expanded && ( +
+
+ + {totalStreamCount || group.streams.length} Streams + +
+ + +
+
+ + {dimensionGroups.length === 0 ? ( + /* Fallback: show raw streams if no dimensions available */ +
+ {group.streams.map((stream, i) => ( + onOpenStream('stream', stream.stream) : undefined} + /> + ))} +
+ ) : ( +
+ {dimensionGroups.map((dim) => ( +
+ {/* Dimension header */} +
{ e.stopPropagation(); toggleDim(dim.label) }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 8, + padding: '8px 4px', + cursor: 'pointer', + userSelect: 'none', + }} + > + + ▼ + + + {dim.label} + + + {dim.values.length} {dim.values.length === 1 ? 'stream' : 'streams'} + +
+ + {/* Dimension values (streams) */} + {!collapsedDims.has(dim.label) && ( +
+ {dim.values.map((v) => ( + onOpenStream(dim.label.toLowerCase(), v.name) : undefined} + /> + ))} +
+ )} +
+ ))} +
+ )} +
+ )} +
+ ) +} + +function StreamRow({ name, active, logCount, onClick }: { + name: string + active: boolean + logCount?: number + onClick?: () => void +}) { + return ( +
{ e.currentTarget.style.background = 'var(--bg-tertiary)' }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }} + > + {/* Health dot */} + + + {/* Stream name */} + + {name} + + + {/* Log count (if available) */} + {logCount !== undefined && ( + + {logCount.toLocaleString()} logs + + )} + + {/* Arrow indicator for clickable rows */} + {onClick && ( + + )} +
+ ) +} + +export function SourcesSection({ refreshKey, onOpenStream }: Props) { + const [streams, setStreams] = useState([]) + const [status, setStatus] = useState(null) + const [params, setParams] = useState(null) + const [loading, setLoading] = useState(true) + const [activeTab, setActiveTab] = useState('all') + const [expandedSource, setExpandedSource] = useState(null) + + useEffect(() => { + Promise.all([fetchStreams(), fetchStatus(), fetchInsightsParams()]) + .then(([s, st, p]) => { setStreams(s); setStatus(st); setParams(p) }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey]) + + const groups = groupStreams(streams) + const filteredGroups = activeTab === 'all' + ? groups + : groups.filter((g) => g.source === activeTab) + + const dimensionGroups = params ? buildDimensionGroups(params, streams) : [] + + if (loading) { + return ( +
+

Sources

+
+ Loading sources... +
+
+ ) + } + + return ( +
+

Sources

+ + {/* Tab bar */} +
+ + {groups.map((g) => ( + + ))} +
+ + {/* Source cards */} + {filteredGroups.length === 0 ? ( +
+ No active sources.{' '} + {status && status.total_logs === 0 && ( + Start sending logs to Gonzo to see sources here. + )} +
+ ) : ( +
+ {filteredGroups.map((g) => ( + + setExpandedSource(expandedSource === g.source ? null : g.source) + } + dimensionGroups={dimensionGroups} + onOpenStream={onOpenStream} + /> + ))} + + {/* Upgrade teaser for more sources */} + +
+ )} +
+ ) +} diff --git a/web/src/components/panels/StreamSelector.tsx b/web/src/components/panels/StreamSelector.tsx new file mode 100644 index 0000000..9354b2e --- /dev/null +++ b/web/src/components/panels/StreamSelector.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react' +import { fetchStreams } from '../../api/client' +import type { StreamInfo } from '../../api/types' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +interface Props { + refreshKey: number +} + +export function StreamSelector({ refreshKey }: Props) { + const [streams, setStreams] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + fetchStreams() + .then(setStreams) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [refreshKey]) + + const formatLastSeen = (ts: string) => { + const d = new Date(ts) + const diff = Date.now() - d.getTime() + if (diff < 60_000) return 'just now' + if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago` + return `${Math.floor(diff / 3_600_000)}h ago` + } + + return ( + + + Streams + + {streams.filter((s) => s.active).length} active + + + {loading ? ( +
+ Loading... +
+ ) : streams.length === 0 ? ( +
+ No active streams +
+ ) : ( +
+ {streams.map((s, i) => ( +
+
+ +
+
+ {s.source} +
+
+ {s.stream} +
+
+
+
+
+ {s.log_count.toLocaleString()} +
+
+ {formatLastSeen(s.last_seen)} +
+
+
+ ))} +
+ )} +
+ ) +} diff --git a/web/src/components/ui/Button.tsx b/web/src/components/ui/Button.tsx new file mode 100644 index 0000000..4e8b17c --- /dev/null +++ b/web/src/components/ui/Button.tsx @@ -0,0 +1,36 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react' + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'ghost' + size?: 'sm' | 'md' + children: ReactNode +} + +const variants = { + primary: 'bg-[var(--color-accent)] text-white hover:opacity-90', + secondary: + 'border border-[var(--color-border)] bg-[var(--color-card)] text-[var(--color-text)] hover:bg-[var(--color-card-hover)]', + ghost: 'text-[var(--color-text-secondary)] hover:text-[var(--color-text)] hover:bg-[var(--color-card-hover)]', +} + +const sizes = { + sm: 'px-2 py-1 text-xs', + md: 'px-3 py-1.5 text-sm', +} + +export function Button({ + variant = 'secondary', + size = 'md', + className = '', + children, + ...props +}: ButtonProps) { + return ( + + ) +} diff --git a/web/src/components/ui/Card.tsx b/web/src/components/ui/Card.tsx new file mode 100644 index 0000000..efbd42c --- /dev/null +++ b/web/src/components/ui/Card.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react' + +interface CardProps { + children: ReactNode + className?: string +} + +export function Card({ children, className = '' }: CardProps) { + return ( +
+ {children} +
+ ) +} + +export function CardHeader({ children, className = '' }: CardProps) { + return ( +
+ {children} +
+ ) +} + +export function CardTitle({ children }: { children: ReactNode }) { + return ( +

{children}

+ ) +} diff --git a/web/src/components/ui/ErrorBoundary.tsx b/web/src/components/ui/ErrorBoundary.tsx new file mode 100644 index 0000000..057329b --- /dev/null +++ b/web/src/components/ui/ErrorBoundary.tsx @@ -0,0 +1,48 @@ +import { Component, type ReactNode } from 'react' +import { Card, CardHeader, CardTitle } from './Card' + +interface Props { + name: string + children: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + render() { + if (this.state.hasError) { + return ( + + + {this.props.name} + +
+

Something went wrong

+

+ {this.state.error?.message} +

+ +
+
+ ) + } + return this.props.children + } +} diff --git a/web/src/components/ui/SeverityBadge.tsx b/web/src/components/ui/SeverityBadge.tsx new file mode 100644 index 0000000..54d279c --- /dev/null +++ b/web/src/components/ui/SeverityBadge.tsx @@ -0,0 +1,21 @@ +import { severityColor } from '../../lib/colors' + +interface SeverityBadgeProps { + severity: string + count?: number +} + +export function SeverityBadge({ severity, count }: SeverityBadgeProps) { + const color = severityColor(severity) + return ( + + {severity} + {count !== undefined && ( + {count.toLocaleString()} + )} + + ) +} diff --git a/web/src/components/ui/ThemeProvider.tsx b/web/src/components/ui/ThemeProvider.tsx new file mode 100644 index 0000000..0acea80 --- /dev/null +++ b/web/src/components/ui/ThemeProvider.tsx @@ -0,0 +1,32 @@ +import { createContext, useState, useEffect, type ReactNode } from 'react' + +type Theme = 'light' | 'dark' + +interface ThemeContextType { + theme: Theme + toggle: () => void +} + +export const ThemeContext = createContext(null) + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [theme, setTheme] = useState(() => { + const stored = localStorage.getItem('dstl8-theme') + if (stored === 'light' || stored === 'dark') return stored + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' + }) + + useEffect(() => { + const root = document.documentElement + root.classList.toggle('dark', theme === 'dark') + localStorage.setItem('dstl8-theme', theme) + }, [theme]) + + const toggle = () => setTheme((t) => (t === 'dark' ? 'light' : 'dark')) + + return ( + + {children} + + ) +} diff --git a/web/src/components/ui/WhatsNewModal.tsx b/web/src/components/ui/WhatsNewModal.tsx new file mode 100644 index 0000000..e93bb82 --- /dev/null +++ b/web/src/components/ui/WhatsNewModal.tsx @@ -0,0 +1,258 @@ +import { useEffect, useState, useCallback } from 'react' +import Markdown from 'react-markdown' +import { useTheme } from '../../hooks/useTheme' +import type { ReleaseInfo } from '../../api/types' + +// Collapse full 40-char git commit hashes to short 7-char form +function collapseHashes(text: string): string { + return text.replace(/\b([0-9a-f]{40})\b/g, (_, hash: string) => hash.slice(0, 7)) +} + +interface WhatsNewModalProps { + version: string + releases: ReleaseInfo[] + onDismiss: () => void +} + +export function WhatsNewModal({ version, releases, onDismiss }: WhatsNewModalProps) { + const { theme } = useTheme() + const [expandedIdx, setExpandedIdx] = useState(null) + + // Find the current release + const currentIdx = releases.findIndex( + (r) => r.tag_name === version || r.tag_name === `v${version}` + ) + const currentRelease = currentIdx >= 0 ? releases[currentIdx] : releases[0] + const otherReleases = releases.filter((_, i) => i !== (currentIdx >= 0 ? currentIdx : 0)) + + // ESC to dismiss + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onDismiss() + } + window.addEventListener('keydown', handler) + return () => window.removeEventListener('keydown', handler) + }, [onDismiss]) + + const toggleExpanded = useCallback((idx: number) => { + setExpandedIdx((prev) => (prev === idx ? null : idx)) + }, []) + + const formatDate = (iso: string) => { + try { + return new Date(iso).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }) + } catch { + return iso + } + } + + return ( +
{ + if (e.target === e.currentTarget) onDismiss() + }} + > +
+ {/* Header */} +
+ Dstl8 +
+
+ What's New +
+
+ {currentRelease && ( + + {currentRelease.tag_name} + + )} +
+ + {/* Scrollable content */} +
+ {/* Current release */} + {currentRelease && ( +
+
+ {currentRelease.name || currentRelease.tag_name} +
+
+ {formatDate(currentRelease.published_at)} +
+
+ {collapseHashes(currentRelease.body || 'No release notes.')} +
+
+ )} + + {/* Previous releases */} + {otherReleases.length > 0 && ( +
+
+ Previous Releases +
+ {otherReleases.map((rel, idx) => ( +
+ + {expandedIdx === idx && ( +
+ {collapseHashes(rel.body || 'No release notes.')} +
+ )} +
+ ))} +
+ )} +
+ + {/* Footer */} +
+ {currentRelease?.url && ( + + View on GitHub + + )} + +
+
+
+ ) +} diff --git a/web/src/components/upsell/IncidentPlaceholder.tsx b/web/src/components/upsell/IncidentPlaceholder.tsx new file mode 100644 index 0000000..9c44d0c --- /dev/null +++ b/web/src/components/upsell/IncidentPlaceholder.tsx @@ -0,0 +1,29 @@ +import { upsellURL } from '../../lib/constants' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +export function IncidentPlaceholder() { + return ( + + + Incidents + +
+
🔒
+

+ Incident Management +

+

+ Create, track, and resolve incidents with AI-powered root cause analysis. +

+ + Available on Dstl8 Pro + +
+
+ ) +} diff --git a/web/src/components/upsell/MobiusUpgrade.tsx b/web/src/components/upsell/MobiusUpgrade.tsx new file mode 100644 index 0000000..7234d6a --- /dev/null +++ b/web/src/components/upsell/MobiusUpgrade.tsx @@ -0,0 +1,30 @@ +import { upsellURL } from '../../lib/constants' +import { Card, CardHeader, CardTitle } from '../ui/Card' + +export function MobiusUpgrade() { + return ( + + + Mobius AI + +
+
🤖
+

+ Continuous AI Analysis +

+

+ Mobius provides real-time AI insights, anomaly detection, and intelligent alerting + across all your log streams. +

+ + Try Mobius Free + +
+
+ ) +} diff --git a/web/src/components/upsell/RetentionBanner.tsx b/web/src/components/upsell/RetentionBanner.tsx new file mode 100644 index 0000000..68e5c04 --- /dev/null +++ b/web/src/components/upsell/RetentionBanner.tsx @@ -0,0 +1,27 @@ +import { upsellURL } from '../../lib/constants' + +export function RetentionBanner() { + return ( +
+
+
+

+ Analyzing last 60 minutes of log data +

+

+ Dstl8 Lite keeps a rolling 60-minute window. For unlimited retention and historical analysis, + upgrade to the full dashboard. +

+
+ + Get Full Retention + +
+
+ ) +} diff --git a/web/src/hooks/useTheme.ts b/web/src/hooks/useTheme.ts new file mode 100644 index 0000000..0c42181 --- /dev/null +++ b/web/src/hooks/useTheme.ts @@ -0,0 +1,8 @@ +import { useContext } from 'react' +import { ThemeContext } from '../components/ui/ThemeProvider' + +export function useTheme() { + const ctx = useContext(ThemeContext) + if (!ctx) throw new Error('useTheme must be used within ThemeProvider') + return ctx +} diff --git a/web/src/hooks/useWebSocket.ts b/web/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..bd164ce --- /dev/null +++ b/web/src/hooks/useWebSocket.ts @@ -0,0 +1,22 @@ +import { useEffect, useRef, useCallback, useState } from 'react' +import { DashboardWebSocket } from '../api/websocket' +import type { WebSocketUpdate } from '../api/types' + +export function useWebSocket() { + const wsRef = useRef(null) + const [lastUpdate, setLastUpdate] = useState(null) + + const onUpdate = useCallback((update: WebSocketUpdate) => { + setLastUpdate(update) + }, []) + + useEffect(() => { + const ws = new DashboardWebSocket() + wsRef.current = ws + ws.subscribe(onUpdate) + ws.connect() + return () => ws.disconnect() + }, [onUpdate]) + + return lastUpdate +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..2841678 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,851 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* ── Dstl8 Design System ── */ +:root { + --radius: 0.5rem; + + /* Light mode (default) */ + --background: hsl(0 0% 100%); + --bg-primary: hsl(210 40% 98%); + --bg-secondary: hsl(0 0% 100%); + --bg-tertiary: hsl(210 40% 96.1%); + --bg-card: hsl(0 0% 100%); + + --text-primary: hsl(222.2 84% 4.9%); + --text-secondary: hsl(215.4 16.3% 46.9%); + --text-muted: hsl(215 16.3% 56.9%); + + --border: hsl(214.3 31.8% 91.4%); + --input: hsl(214.3 31.8% 91.4%); + + --critical: hsl(0 84.2% 60.2%); + --critical-glow: hsla(0 84.2% 60.2% / 0.15); + --serious: hsl(27 87% 50%); + --serious-glow: hsla(27 87% 50% / 0.12); + --moderate: hsl(38 92% 40%); + --moderate-glow: hsla(38 92% 40% / 0.1); + --mild: hsl(142 71% 35%); + --mild-glow: hsla(142 71% 35% / 0.1); + --health-ok: hsl(160 60% 38%); + --health-ok-glow: hsla(160 60% 38% / 0.1); + + --accent-blue: hsl(217 91% 50%); + --accent-blue-hover: hsl(217 91% 45%); + --accent-glow: hsla(217 91% 50% / 0.12); + + /* Legacy compat for existing components */ + --color-bg: var(--bg-primary); + --color-bg-secondary: var(--bg-tertiary); + --color-text: var(--text-primary); + --color-text-secondary: var(--text-secondary); + --color-border: var(--border); + --color-card: var(--bg-card); + --color-card-hover: var(--bg-tertiary); + --color-accent: var(--accent-blue); +} + +.dark { + --background: hsl(222.2 84% 4.9%); + --bg-primary: hsl(222.2 84% 4.9%); + --bg-secondary: hsl(222.2 84% 6.5%); + --bg-tertiary: hsl(217.2 32.6% 17.5%); + --bg-card: hsl(222.2 84% 6.5%); + + --text-primary: hsl(210 40% 98%); + --text-secondary: hsl(215 20.2% 65.1%); + --text-muted: hsl(215 16.3% 46.9%); + + --border: hsl(217.2 32.6% 17.5%); + --input: hsl(217.2 32.6% 17.5%); + + --critical: hsl(0 84.2% 60.2%); + --critical-glow: hsla(0 84.2% 60.2% / 0.25); + --serious: hsl(30 87% 67%); + --serious-glow: hsla(30 87% 67% / 0.2); + --moderate: hsl(43 74% 66%); + --moderate-glow: hsla(43 74% 66% / 0.15); + --mild: hsl(142 71% 45%); + --mild-glow: hsla(142 71% 45% / 0.15); + --health-ok: hsl(160 60% 45%); + --health-ok-glow: hsla(160 60% 45% / 0.15); + + --accent-blue: hsl(217 91% 60%); + --accent-blue-hover: hsl(217 91% 55%); + --accent-glow: hsla(217 91% 60% / 0.2); +} + +* { box-sizing: border-box; } + +body { + margin: 0; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); + background: var(--bg-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ── App Shell ── */ +.app-shell { + display: flex; + flex-direction: column; + height: 100vh; + overflow: hidden; +} + +.app-header { + height: 56px; + background: var(--bg-card); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + flex-shrink: 0; + z-index: 100; +} + +.app-header-left { + display: flex; + align-items: center; +} + +.app-header-right { + display: flex; + align-items: center; + gap: 8px; +} + +.app-logo { + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + text-decoration: none; + cursor: pointer; +} + +.header-breadcrumb { + display: flex; + align-items: center; + gap: 8px; + margin-left: 16px; + padding-left: 16px; + border-left: 1px solid var(--border); +} + +.breadcrumb-link { + font-size: 13px; + color: var(--text-muted); + text-decoration: none; + cursor: pointer; + transition: color 0.15s ease; +} + +.breadcrumb-link:hover { color: var(--text-primary); } + +.breadcrumb-separator { color: var(--text-muted); font-size: 12px; } + +.breadcrumb-current { font-size: 13px; font-weight: 500; color: var(--text-primary); } + +.header-icon-btn { + width: 36px; + height: 36px; + border-radius: calc(var(--radius) - 2px); + border: 1px solid transparent; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; +} + +.header-icon-btn:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.header-icon-btn svg { width: 18px; height: 18px; } + +.header-user { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 12px 6px 6px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: calc(var(--radius) - 2px); + cursor: pointer; + transition: all 0.15s ease; +} + +.header-user:hover { border-color: var(--text-muted); } + +.header-user-avatar { + width: 26px; + height: 26px; + border-radius: 50%; + background: var(--accent-blue); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 600; + color: white; +} + +.header-user-info { display: flex; flex-direction: column; gap: 1px; } +.header-user-name { font-size: 12px; font-weight: 500; color: var(--text-primary); } +.header-user-role { font-size: 10px; color: var(--text-muted); } + +/* ── Sidebar ── */ +.app-container { + display: flex; + flex: 1; + overflow: hidden; +} + +.app-sidebar { + width: 220px; + background: var(--bg-card); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.sidebar-nav { + flex: 1; + padding: 16px 12px; + display: flex; + flex-direction: column; + gap: 4px; + overflow-y: auto; +} + +.sidebar-nav-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border-radius: calc(var(--radius) - 2px); + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + text-decoration: none; + cursor: pointer; + transition: all 0.15s ease; + border: none; + background: transparent; + width: 100%; + text-align: left; +} + +.sidebar-nav-item:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.sidebar-nav-item.active { + background: var(--accent-glow); + color: var(--accent-blue); +} + +.sidebar-nav-item.locked { + opacity: 0.4; +} +.sidebar-nav-item.locked:hover { + opacity: 0.6; +} + +.sidebar-nav-item svg { width: 18px; height: 18px; flex-shrink: 0; } + +.sidebar-bottom { + padding: 12px; + border-top: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 4px; +} + +/* ── Main Content ── */ +.app-main { + flex: 1; + overflow-y: auto; + background: var(--bg-primary); +} + +.page-content { + padding: 24px 32px; + max-width: 1400px; + margin: 0 auto; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.page-title { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + +/* ── Buttons ── */ +.btn-primary { + display: inline-flex; + align-items: center; + gap: 8px; + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 500; + padding: 10px 16px; + border-radius: calc(var(--radius) - 2px); + border: none; + background: var(--accent-blue); + color: white; + cursor: pointer; + transition: all 0.15s ease; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + text-decoration: none; +} + +.btn-primary:hover { + background: var(--accent-blue-hover); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); +} + +.btn-primary svg { width: 16px; height: 16px; } + +/* ── Cards ── */ +.dstl8-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); +} + +.dstl8-card-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.dstl8-card-title { + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--text-primary); +} + +.dstl8-card-body { padding: 20px; } + +/* ── Workspace Card ── */ +.workspace-card { + background: var(--bg-card); + border: 2px solid var(--border); + border-radius: var(--radius); + padding: 24px; + cursor: pointer; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); +} + +.workspace-card:hover { + transform: translateY(-4px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); +} + +.workspace-card.ok { + border-color: var(--health-ok); + box-shadow: 0 0 8px var(--health-ok-glow); +} + +.workspace-card.ok:hover { + box-shadow: 0 0 14px var(--health-ok-glow), 0 10px 15px -3px rgba(0, 0, 0, 0.1); +} + +.workspace-card.critical { + border-color: var(--critical); + box-shadow: 0 0 16px var(--critical-glow); +} + +.workspace-card.serious { + border-color: var(--serious); + box-shadow: 0 0 12px var(--serious-glow); +} + +.workspace-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.workspace-name { + font-size: 18px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.workspace-health-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.workspace-card.ok .workspace-health-dot { + background: var(--health-ok); + box-shadow: 0 0 8px var(--health-ok-glow); + animation: pulse-dot 2s ease-in-out infinite; +} + +.workspace-stats { + display: flex; + gap: 20px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.workspace-stat { display: flex; flex-direction: column; gap: 2px; } + +.workspace-stat-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.workspace-stat-value { + font-size: 20px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.workspace-meta { + display: flex; + gap: 16px; + padding-top: 12px; + border-top: 1px solid var(--border); +} + +.workspace-meta-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-secondary); +} + +/* ── Badges ── */ +.badge { + font-family: 'Inter', sans-serif; + font-size: 11px; + font-weight: 600; + padding: 3px 10px; + border-radius: 9999px; + display: inline-block; +} + +.badge-critical { background: hsla(0 84.2% 60.2% / 0.15); color: var(--critical); } +.badge-serious { background: hsla(30 87% 60% / 0.15); color: var(--serious); } +.badge-ok { background: hsla(160 60% 45% / 0.15); color: var(--health-ok); } + +/* ── Source Type Card (Details page) ── */ +.source-type-card { + background: var(--bg-card); + border-radius: var(--radius); + padding: 20px 24px; + cursor: pointer; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + border: 2px solid var(--border); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + display: flex; + gap: 24px; +} + +.source-type-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px -4px rgba(0, 0, 0, 0.12); +} + +.source-type-card.ok { + border-color: var(--health-ok); + box-shadow: 0 0 8px var(--health-ok-glow); +} + +.source-type-left { flex: 1; min-width: 0; } + +.source-type-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; +} + +.source-type-health-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.source-type-card.ok .source-type-health-dot { + background: var(--health-ok); + box-shadow: 0 0 8px var(--health-ok-glow); +} + +.source-type-name { + font-size: 17px; + font-weight: 600; + letter-spacing: -0.01em; +} + +.source-type-stats { + display: flex; + gap: 24px; + margin-bottom: 12px; +} + +.source-type-stat { display: flex; flex-direction: column; gap: 2px; } + +.source-type-stat-label { + font-size: 11px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.source-type-stat-value { + font-size: 20px; + font-weight: 700; + font-variant-numeric: tabular-nums; +} + +.source-type-right { + width: 320px; + flex-shrink: 0; + border-left: 1px solid var(--border); + padding-left: 24px; + display: flex; + flex-direction: column; +} + +/* ── Chat Widget (Mobius) ── */ +.chat-widget { + display: flex; + flex-direction: column; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.chat-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 10px; +} + +.chat-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: linear-gradient(135deg, hsl(30 80% 55%) 0%, hsl(25 90% 50%) 100%); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: white; + font-size: 14px; +} + +.chat-status { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + color: var(--text-muted); +} + +.chat-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--health-ok); + animation: pulse-status 2s ease-in-out infinite; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.chat-message { display: flex; gap: 12px; } +.chat-message.user { flex-direction: row-reverse; } + +.chat-message-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; +} + +.chat-message.assistant .chat-message-avatar { + background: linear-gradient(135deg, hsl(30 80% 55%), hsl(25 90% 50%)); + color: white; +} + +.chat-message.user .chat-message-avatar { + background: var(--accent-blue); + color: white; +} + +.chat-message-content { flex: 1; min-width: 0; } + +.chat-message-bubble { + padding: 10px 14px; + border-radius: var(--radius); + font-size: 13px; + line-height: 1.5; +} + +.chat-message.assistant .chat-message-bubble { + background: var(--bg-tertiary); + color: var(--text-primary); + border-top-left-radius: 4px; +} + +.chat-message.user .chat-message-bubble { + background: var(--accent-blue); + color: white; + border-top-right-radius: 4px; +} + +.chat-message-time { + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; + padding: 0 4px; +} + +.chat-message.user .chat-message-time { text-align: right; } + +.chat-input-container { + padding: 12px 16px; + border-top: 1px solid var(--border); + background: var(--bg-tertiary); +} + +.chat-input-wrapper { display: flex; gap: 10px; align-items: flex-end; } + +.chat-input { + flex: 1; + padding: 10px 14px; + border-radius: calc(var(--radius) - 2px); + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text-primary); + font-family: 'Inter', sans-serif; + font-size: 13px; + resize: none; + min-height: 40px; + outline: none; + transition: border-color 0.15s ease; +} + +.chat-input::placeholder { color: var(--text-muted); } +.chat-input:focus { border-color: var(--accent-blue); } + +.chat-send-btn { + width: 40px; + height: 40px; + border-radius: calc(var(--radius) - 2px); + border: none; + background: var(--accent-blue); + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s ease; + flex-shrink: 0; +} + +.chat-send-btn:hover { background: var(--accent-blue-hover); } +.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } +.chat-send-btn svg { width: 18px; height: 18px; } + +/* ── Layout Grids ── */ +.main-layout { + display: grid; + grid-template-columns: 1fr 320px; + gap: 24px; + flex: 1; +} + +.main-layout-full { grid-template-columns: 1fr; } + +.left-column { min-width: 0; } + +.right-column { + display: flex; + flex-direction: column; + position: sticky; + top: 0; + height: calc(100vh - 56px - 48px); + align-self: start; +} + +.workspace-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); + gap: 24px; +} + +/* ── Summary Stats Bar ── */ +.stats-bar { + display: flex; + gap: 32px; + padding: 16px 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 24px; +} + +.stat-item { display: flex; align-items: center; gap: 10px; } + +.stat-icon { + width: 36px; + height: 36px; + border-radius: calc(var(--radius) - 2px); + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; +} + +.stat-icon.blue { background: var(--accent-glow); color: var(--accent-blue); } +.stat-icon.green { background: var(--health-ok-glow); color: var(--health-ok); } +.stat-icon.red { background: var(--critical-glow); color: var(--critical); } +.stat-icon.orange { background: var(--serious-glow); color: var(--serious); } + +.stat-value { + font-size: 20px; + font-weight: 700; + font-variant-numeric: tabular-nums; + color: var(--text-primary); +} + +.stat-label { font-size: 12px; color: var(--text-muted); } + +/* ── Upgrade Page ── */ +.upgrade-page { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80px 32px; + text-align: center; +} + +.upgrade-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.4; } +.upgrade-title { font-size: 24px; font-weight: 600; margin-bottom: 8px; color: var(--text-primary); } +.upgrade-desc { font-size: 14px; color: var(--text-secondary); max-width: 400px; margin-bottom: 24px; } + +/* ── Scrollbar ── */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } + +/* ── Animations ── */ +@keyframes pulse-dot { + 0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--health-ok-glow); } + 50% { opacity: 0.6; box-shadow: 0 0 12px var(--health-ok-glow); } +} + +@keyframes pulse-status { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ── What's New Modal markdown ── */ +.whats-new-markdown { + font-size: 13px; + line-height: 1.6; + color: var(--text-secondary); +} +.whats-new-markdown h1, +.whats-new-markdown h2, +.whats-new-markdown h3 { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin: 12px 0 4px; +} +.whats-new-markdown h1 { font-size: 15px; } +.whats-new-markdown p { + margin: 4px 0; +} +.whats-new-markdown ul, +.whats-new-markdown ol { + padding-left: 20px; + margin: 4px 0; +} +.whats-new-markdown li { + margin: 2px 0; +} +.whats-new-markdown code { + font-size: 12px; + background: var(--bg-tertiary); + padding: 1px 5px; + border-radius: 3px; +} +.whats-new-markdown pre { + background: var(--bg-tertiary); + padding: 8px 12px; + border-radius: 6px; + overflow-x: auto; + margin: 8px 0; +} +.whats-new-markdown pre code { + background: none; + padding: 0; +} +.whats-new-markdown a { + color: var(--accent-blue); + text-decoration: none; +} +.whats-new-markdown a:hover { + text-decoration: underline; +} diff --git a/web/src/lib/colors.ts b/web/src/lib/colors.ts new file mode 100644 index 0000000..283551f --- /dev/null +++ b/web/src/lib/colors.ts @@ -0,0 +1,24 @@ +// Severity colors matching Dstl8 cloud dashboard +export const SEVERITY_COLORS: Record = { + FATAL: '#b91c1c', + CRITICAL: '#b91c1c', + ERROR: '#dc2626', + WARN: '#f97316', + WARNING: '#f97316', + INFO: '#3b82f6', + DEBUG: '#6b7280', + TRACE: '#10b981', + UNKNOWN: '#9ca3af', +} + +export const SEVERITY_ORDER = ['FATAL', 'CRITICAL', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'] + +// Heatmap gradient (negative → neutral → positive) +export const HEATMAP_COLORS = [ + '#7f1d1d', '#991b1b', '#b91c1c', '#dc2626', '#ef4444', '#f87171', + '#fbbf24', '#a3e635', '#4ade80', '#22c55e', '#16a34a', '#166534', +] + +export function severityColor(sev: string): string { + return SEVERITY_COLORS[sev.toUpperCase()] || SEVERITY_COLORS.UNKNOWN +} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts new file mode 100644 index 0000000..0e30d20 --- /dev/null +++ b/web/src/lib/constants.ts @@ -0,0 +1,10 @@ +export const API_BASE = '/api' + +export const UTM_PARAMS = { + source: 'gonzo', + medium: 'dstl8lite', +} as const + +export function upsellURL(campaign: string): string { + return `https://app.dstl8.ai/lp/gonzo-to-dstl8-pro/?utm_source=${UTM_PARAMS.source}&utm_medium=${UTM_PARAMS.medium}&utm_campaign=${campaign}` +} diff --git a/web/src/lib/icons.tsx b/web/src/lib/icons.tsx new file mode 100644 index 0000000..358f470 --- /dev/null +++ b/web/src/lib/icons.tsx @@ -0,0 +1,68 @@ +const A = { + viewBox: '0 0 24 24', + fill: 'none', + stroke: 'currentColor', + strokeWidth: 2, + strokeLinecap: 'round' as const, + strokeLinejoin: 'round' as const, +} + +export const Icons = { + workspaces: ( + + ), + overview: ( + + ), + incidents: ( + + ), + sources: ( + + ), + users: ( + + ), + audit: ( + + ), + account: ( + + ), + settings: ( + + ), + support: ( + + ), + sparkle: ( + + ), + search: ( + + ), + moon: ( + + ), + sun: ( + + ), + send: ( + + ), + plus: ( + + ), + chevronRight: ( + + ), + lock: ( + + ), + logs: ( + + ), + heatmap: ( + + ), +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..fab1219 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from './App' +import './index.css' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/web/src/pages/HeatmapPage.tsx b/web/src/pages/HeatmapPage.tsx new file mode 100644 index 0000000..3324f6a --- /dev/null +++ b/web/src/pages/HeatmapPage.tsx @@ -0,0 +1,418 @@ +import { useEffect, useState, useRef, useMemo, useCallback } from 'react' +import { fetchSentiment, fetchInsightsParams } from '../api/client' +import type { SentimentData, InsightsParams } from '../api/types' +import { useWebSocket } from '../hooks/useWebSocket' + +// Sentiment color scale matching Dstl8 portal-ui +const SENTIMENT_COLORS: { min: number; max: number; color: string; label: string }[] = [ + { min: -1.0, max: -0.75, color: '#b71c1c', label: 'Critical' }, + { min: -0.75, max: -0.5, color: '#d32f2f', label: 'Poor' }, + { min: -0.5, max: -0.25, color: '#ff6f00', label: 'Weak' }, + { min: -0.25, max: 0.0, color: '#eee472', label: 'Fair' }, + { min: 0.0, max: 0.38, color: '#b3c49f', label: 'Neutral' }, + { min: 0.38, max: 1.01, color: '#388e3c', label: 'Positive' }, +] + +const EMPTY_COLOR = 'var(--bg-tertiary)' + +function sentimentColor(value: number): string { + for (const band of SENTIMENT_COLORS) { + if (value >= band.min && value < band.max) return band.color + } + return SENTIMENT_COLORS[SENTIMENT_COLORS.length - 1].color +} + +function formatTime(ts: number): string { + const d = new Date(ts * 1000) + const m = d.getMinutes().toString().padStart(2, '0') + const s = d.getSeconds().toString().padStart(2, '0') + return `${m}:${s}` +} + +function sentimentLabel(value: number): string { + for (const band of SENTIMENT_COLORS) { + if (value >= band.min && value < band.max) return band.label + } + return 'Positive' +} + +interface CellData { + sentiment: number + logCount: number + timestamp: number +} + +export function HeatmapPage() { + const [data, setData] = useState(null) + const [groupBy, setGroupBy] = useState('pod') + const [loading, setLoading] = useState(true) + const [params, setParams] = useState(null) + const [tooltip, setTooltip] = useState<{ x: number; y: number; flipBelow?: boolean; cell: CellData; group: string } | null>(null) + const containerRef = useRef(null) + const [containerWidth, setContainerWidth] = useState(0) + const wsUpdate = useWebSocket() + const initialLoadDone = useRef(false) + + // Fetch available dimensions once + useEffect(() => { + fetchInsightsParams().then(setParams).catch(() => {}) + }, []) + + // Auto-select best groupBy based on available data + useEffect(() => { + if (!params) return + // Pick the first dimension that has more than 1 distinct value (i.e. not just "unknown") + const candidates: { key: string; values?: string[] }[] = [ + { key: 'pod', values: params.pods }, + { key: 'namespace', values: params.namespaces }, + { key: 'service', values: params.services }, + { key: 'host', values: params.hosts }, + { key: 'deployment', values: params.deployments }, + ] + for (const c of candidates) { + const real = c.values?.filter((v) => v !== 'unknown' && v !== 'default') ?? [] + if (real.length > 1) { + setGroupBy(c.key) + return + } + } + }, [params]) + + const loadData = useCallback(() => { + fetchSentiment(groupBy) + .then(setData) + .catch(() => {}) + .finally(() => { + if (!initialLoadDone.current) { + initialLoadDone.current = true + setLoading(false) + } + }) + }, [groupBy]) + + // Initial load + refresh on groupBy change + useEffect(() => { + initialLoadDone.current = false + setLoading(true) + loadData() + }, [groupBy]) // eslint-disable-line react-hooks/exhaustive-deps + + // Real-time updates (no loading flash) + useEffect(() => { + if (wsUpdate && initialLoadDone.current) { + loadData() + } + }, [wsUpdate, loadData]) + + // Build grid data: rows = group_values, columns = sorted unique timestamps + const { rows, columns, grid } = useMemo(() => { + if (!data || data.group_values.length === 0) { + return { rows: [] as string[], columns: [] as number[], grid: new Map>() } + } + + // Collect all unique timestamps + const tsSet = new Set() + for (const b of data.buckets) tsSet.add(b.timestamp) + const cols = Array.from(tsSet).sort((a, b) => a - b) + + // Build grid map + const g = new Map>() + for (const b of data.buckets) { + if (!g.has(b.group_value)) g.set(b.group_value, new Map()) + g.get(b.group_value)!.set(b.timestamp, { + sentiment: b.sentiment, + logCount: b.log_count, + timestamp: b.timestamp, + }) + } + + // Sort rows alphabetically + const sortedRows = [...data.group_values].sort((a, b) => a.localeCompare(b)) + + return { rows: sortedRows, columns: cols, grid: g } + }, [data]) + + // Track container width for column limiting + // Re-run when loading changes so we attach after the card div mounts + useEffect(() => { + const el = containerRef.current + if (!el) return + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + setContainerWidth(entry.contentRect.width) + } + }) + ro.observe(el) + return () => ro.disconnect() + }, [loading]) + + const CELL_SIZE = 20 + const CELL_GAP = 2 + const LABEL_WIDTH = 280 + + // Show as many columns as fit at fixed cell size, dropping oldest from left + const visibleColumns = useMemo(() => { + if (columns.length === 0) return columns + if (containerWidth === 0) return [] as number[] + // containerWidth = card width (from ResizeObserver on the card div) + // Subtract label column and inner padding (16px each side = 32px) + const available = containerWidth - LABEL_WIDTH - 32 + const maxCols = Math.max(1, Math.floor(available / (CELL_SIZE + CELL_GAP))) + // available space is correct — data just hasn't accumulated enough columns yet + if (columns.length <= maxCols) return columns + return columns.slice(columns.length - maxCols) + }, [columns, containerWidth]) + + // Build group-by options, showing count of distinct values + const groupByOptions = useMemo(() => { + const dims: { value: string; label: string; values?: string[] }[] = [ + { value: 'pod', label: 'Pod', values: params?.pods }, + { value: 'namespace', label: 'Namespace', values: params?.namespaces }, + { value: 'service', label: 'Service', values: params?.services }, + { value: 'host', label: 'Host', values: params?.hosts }, + { value: 'deployment', label: 'Deployment', values: params?.deployments }, + ] + return dims.map((d) => { + const real = d.values?.filter((v) => v !== 'unknown' && v !== 'default') ?? [] + return { + value: d.value, + label: real.length > 0 ? `${d.label} (${real.length})` : d.label, + hasData: real.length > 0, + } + }) + }, [params]) + + return ( +
+
+

Severity Heatmap

+
+ Group by + +
+
+ + {loading ? ( +
+ Loading heatmap data... +
+ ) : rows.length === 0 ? ( +
+ No data yet. Start sending logs to see the severity heatmap. +
+ ) : ( +
+ {/* Heatmap container — vertically scrollable, columns fit horizontally */} +
+
+ {/* Time axis header — right-aligned so data grows from the right */} +
+ {visibleColumns.map((ts) => { + // Show label every 5 seconds + const showLabel = ts % 5 === 0 + return ( +
+ {formatTime(ts)} +
+ ) + })} +
+ + {/* Heatmap rows */} + {rows.map((rowName) => ( +
+ {/* Row label */} +
+ {rowName} +
+ + {/* Cells — right-aligned so they grow from the right edge */} +
+ {visibleColumns.map((ts) => { + const cell = grid.get(rowName)?.get(ts) + return ( +
{ + if (!cell) return + const rect = e.currentTarget.getBoundingClientRect() + const containerRect = containerRef.current?.getBoundingClientRect() + if (containerRect) { + const relY = rect.top - containerRect.top + const distFromBottom = containerRect.height - relY - rect.height + // Show below cell if near top, but not if also near bottom + const flipBelow = relY < 60 && distFromBottom > 100 + setTooltip({ + x: rect.left - containerRect.left + rect.width / 2, + y: flipBelow ? relY + rect.height + 8 : relY - 8, + flipBelow, + cell, + group: rowName, + }) + } + }} + onMouseLeave={() => setTooltip(null)} + /> + ) + })} +
+
+ ))} +
+ + {/* Tooltip */} + {tooltip && ( +
+
{tooltip.group}
+
+ {formatTime(tooltip.cell.timestamp)} · {tooltip.cell.logCount} logs +
+
+ + {sentimentLabel(tooltip.cell.sentiment)} ({tooltip.cell.sentiment.toFixed(2)}) +
+
+ )} +
+ + {/* Legend */} +
+ {SENTIMENT_COLORS.map((band) => ( +
+ +
+
+ {band.label} +
+
+ [{band.min.toFixed(2)}, {band.max === 1.01 ? '1.0' : band.max.toFixed(2)}) +
+
+
+ ))} +
+
+ )} +
+ ) +} diff --git a/web/src/pages/LogViewerPage.tsx b/web/src/pages/LogViewerPage.tsx new file mode 100644 index 0000000..8e78ebe --- /dev/null +++ b/web/src/pages/LogViewerPage.tsx @@ -0,0 +1,621 @@ +import { useEffect, useState, useRef, useCallback } from 'react' +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + Legend, + CartesianGrid, +} from 'recharts' +import { fetchSeverityHistory, fetchLogs } from '../api/client' +import type { LogSample } from '../api/types' +import { SEVERITY_COLORS, SEVERITY_ORDER, severityColor } from '../lib/colors' +import { useWebSocket } from '../hooks/useWebSocket' + +const SEVERITY_LABELS: Record = { + FATAL: 'Fatal', + ERROR: 'Error', + WARN: 'Warn', + INFO: 'Info', + DEBUG: 'Debug', + TRACE: 'Trace', +} + +function formatTimestamp(ts: number): string { + const d = new Date(ts * 1000) + const mo = (d.getMonth() + 1).toString().padStart(2, '0') + const day = d.getDate().toString().padStart(2, '0') + const h = d.getHours().toString().padStart(2, '0') + const m = d.getMinutes().toString().padStart(2, '0') + const s = d.getSeconds().toString().padStart(2, '0') + return `${mo}/${day} ${h}:${m}:${s}` +} + +/** Stable identity for a log entry (timestamp + first 80 chars of message) */ +function logKey(log: LogSample): string { + return `${log.timestamp}:${log.message.slice(0, 80)}` +} + +export function LogViewerPage() { + const [chartData, setChartData] = useState[]>([]) + const [logs, setLogs] = useState([]) + const [search, setSearch] = useState('') + const [searchInput, setSearchInput] = useState('') + const [loading, setLoading] = useState(true) + const [expandedKey, setExpandedKey] = useState(null) + const [paused, setPaused] = useState(false) + const pausedRef = useRef(false) + const listRef = useRef(null) + const wsUpdate = useWebSocket() + const debounceRef = useRef>(null) + const [severityFilter, setSeverityFilter] = useState>(new Set()) + + // Debounced search + const handleSearchInput = useCallback((val: string) => { + setSearchInput(val) + if (debounceRef.current) clearTimeout(debounceRef.current) + debounceRef.current = setTimeout(() => setSearch(val), 300) + }, []) + + // Build fetch opts + const severityParam = severityFilter.size > 0 ? Array.from(severityFilter).join(',') : undefined + + // Fetch chart data — only when not paused + useEffect(() => { + if (pausedRef.current) return + fetchSeverityHistory(search ? { search } : undefined) + .then((data) => { + const mapped = data.map((point) => { + const d = new Date(point.timestamp * 1000) + const h = d.getHours().toString().padStart(2, '0') + const m = d.getMinutes().toString().padStart(2, '0') + const s = d.getSeconds().toString().padStart(2, '0') + return { + name: `${h}:${m}:${s}`, + ...point.counts, + total: point.total, + } + }) + setChartData(mapped) + }) + .catch(() => {}) + }, [wsUpdate, search]) // eslint-disable-line react-hooks/exhaustive-deps + + // Fetch logs — only when not paused + useEffect(() => { + if (pausedRef.current) return + fetchLogs({ limit: 500, search: search || undefined, severity: severityParam }) + .then(setLogs) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [wsUpdate, search, severityParam]) // eslint-disable-line react-hooks/exhaustive-deps + + // Auto-scroll when not paused + useEffect(() => { + if (!pausedRef.current && listRef.current) { + const el = listRef.current + requestAnimationFrame(() => { + el.scrollTop = el.scrollHeight + }) + } + }, [logs]) + + const togglePaused = useCallback(() => { + setPaused((prev) => { + const next = !prev + pausedRef.current = next + // When resuming, clear expanded row and scroll to bottom + if (!next) { + setExpandedKey(null) + if (listRef.current) { + requestAnimationFrame(() => { + listRef.current!.scrollTop = listRef.current!.scrollHeight + }) + } + } + return next + }) + }, []) + + const handleRowClick = useCallback((key: string) => { + // If user is selecting text (drag-to-copy), don't toggle the row + const sel = window.getSelection() + if (sel && sel.toString().length > 0) return + + setExpandedKey((prev) => { + if (prev === key) { + // Collapsing — resume and scroll to bottom + setPaused(false) + pausedRef.current = false + if (listRef.current) { + requestAnimationFrame(() => { + listRef.current!.scrollTop = listRef.current!.scrollHeight + }) + } + return null + } else { + // Expanding — pause + setPaused(true) + pausedRef.current = true + return key + } + }) + }, []) + + const toggleSeverity = useCallback((sev: string) => { + setSeverityFilter((prev) => { + const next = new Set(prev) + if (next.has(sev)) { + next.delete(sev) + } else { + next.add(sev) + } + return next + }) + }, []) + + const activeSeverities = SEVERITY_ORDER.filter((sev) => + chartData.some((d) => ((d[sev] as number) || 0) > 0) + ) + + const totalLogs = chartData.reduce((sum, d) => sum + ((d.total as number) || 0), 0) + + return ( +
+ {/* Header */} +
+

Log Viewer

+
+ {/* Search */} +
+ + + + + handleSearchInput(e.target.value)} + style={{ + width: 220, + height: 32, + paddingLeft: 28, + paddingRight: 8, + fontSize: 12, + border: '1px solid var(--border)', + borderRadius: 6, + background: 'var(--bg-card)', + color: 'var(--text-primary)', + outline: 'none', + }} + /> +
+ {/* Live / Paused toggle */} + + + {totalLogs.toLocaleString()} logs + +
+
+ + {/* Severity filter pills */} +
+ Severity: + {SEVERITY_ORDER.map((sev) => { + const active = severityFilter.has(sev) + const anyActive = severityFilter.size > 0 + return ( + + ) + })} + {severityFilter.size > 0 && ( + + )} +
+ + {/* Severity Chart */} +
+
+ + Log Severity Over Time + + + {paused ? 'paused' : `${chartData.length}s window`} + +
+ {chartData.length === 0 ? ( +
+ No data yet +
+ ) : ( + + + + + + + SEVERITY_LABELS[value] || value} + /> + {activeSeverities.map((sev, i) => ( + + ))} + + + )} +
+ + {/* Log Table */} +
+ {/* Table header */} +
+
+ Timestamp +
+
+ Severity +
+
+ Message +
+
+ + {/* Scrollable rows */} +
+ {loading ? ( +
+ Loading logs... +
+ ) : logs.length === 0 ? ( +
+ No logs yet. Start sending logs to see them here. +
+ ) : ( + <> + {logs.map((log, i) => { + const key = logKey(log) + const isExpanded = expandedKey === key + const attrs = log.attributes || {} + const badges = ['service', 'pod', 'namespace', 'host', 'deployment', 'environment', 'cluster', 'category'] + .filter((k) => attrs[k] && attrs[k] !== 'unknown' && attrs[k] !== 'default') + .map((k) => ({ key: k, value: attrs[k] })) + + return ( +
handleRowClick(key)} + style={{ + display: 'flex', + alignItems: isExpanded ? 'flex-start' : 'center', + borderBottom: '1px solid var(--border)', + padding: '6px 0', + minHeight: 36, + cursor: 'pointer', + transition: 'background 0.1s', + background: isExpanded ? 'var(--bg-tertiary)' : undefined, + }} + onMouseEnter={(e) => { if (!isExpanded) e.currentTarget.style.background = 'var(--bg-tertiary)' }} + onMouseLeave={(e) => { if (!isExpanded) e.currentTarget.style.background = 'transparent' }} + > + {/* Timestamp */} +
+ {formatTimestamp(log.timestamp)} +
+ + {/* Severity */} +
+ + {log.severity} + +
+ + {/* Message */} +
+ {isExpanded ? ( +
+
+ + + +
+ {log.message} +
+
+ + {/* Metadata badges */} + {badges.length > 0 && ( +
+ {badges.map((b) => ( + + {b.key}: {b.value} + + ))} +
+ )} + + {/* Full attributes */} + {Object.keys(attrs).length > 0 && ( +
+ {Object.entries(attrs).map(([k, v]) => ( +
+ {k} + = + {v} +
+ ))} +
+ )} +
+ ) : ( +
+ + + + + {log.message} + + {badges.length > 0 && ( +
+ {badges.slice(0, 3).map((b) => ( + + {b.value} + + ))} + {badges.length > 3 && ( + + +{badges.length - 3} + + )} +
+ )} +
+ )} +
+
+ ) + })} +
+ + )} +
+
+
+ ) +} diff --git a/web/src/pages/SourcesPage.tsx b/web/src/pages/SourcesPage.tsx new file mode 100644 index 0000000..60bda9c --- /dev/null +++ b/web/src/pages/SourcesPage.tsx @@ -0,0 +1,378 @@ +import { useEffect, useState, useCallback } from 'react' +import { fetchStreams, fetchStatus, fetchInsightsParams } from '../api/client' +import type { StreamInfo, StatusInfo, InsightsParams } from '../api/types' +import { useWebSocket } from '../hooks/useWebSocket' +import { upsellURL } from '../lib/constants' + +interface Props { + onOpenStream?: (dimensionType: string, dimensionValue: string) => void +} + +interface SourceGroup { + source: string + streams: StreamInfo[] + totalLogs: number + activeCount: number +} + +interface DimensionGroup { + label: string + values: { name: string; active: boolean }[] +} + +function groupStreams(streams: StreamInfo[]): SourceGroup[] { + const map = new Map() + for (const s of streams) { + const key = s.source || 'unknown' + if (!map.has(key)) map.set(key, []) + map.get(key)!.push(s) + } + const groups: SourceGroup[] = [] + for (const [source, items] of map) { + groups.push({ + source, + streams: items, + totalLogs: items.reduce((sum, s) => sum + s.log_count, 0), + activeCount: items.filter((s) => s.active).length, + }) + } + groups.sort((a, b) => b.totalLogs - a.totalLogs) + return groups +} + +function buildDimensionGroups(params: InsightsParams, streams: StreamInfo[]): DimensionGroup[] { + const groups: DimensionGroup[] = [] + const activeStreams = new Set(streams.filter((s) => s.active).map((s) => s.stream)) + const addGroup = (label: string, values: string[] | undefined) => { + if (!values || values.length === 0) return + groups.push({ + label, + values: values.map((v) => ({ + name: v, + active: activeStreams.size > 0, + })), + }) + } + addGroup('Host', params.hosts) + addGroup('Service', params.services) + addGroup('Deployment', params.deployments) + addGroup('Namespace', params.namespaces) + addGroup('Pod', params.pods) + addGroup('Environment', params.environments) + addGroup('Cluster', params.clusters) + return groups +} + +const sourceIcons: Record = { + file: '\u{1F4C4}', + stdin: '\u{2328}', + k8s: '\u{2699}', + otlp: '\u{1F4E1}', + unknown: '\u{2753}', +} + +function formatLastSeen(lastSeen: string): string { + const d = new Date(lastSeen) + const now = Date.now() + const diff = Math.floor((now - d.getTime()) / 1000) + if (diff < 60) return `${diff}s ago` + if (diff < 3600) return `${Math.floor(diff / 60)}m ago` + if (diff < 86400) return `${Math.floor(diff / 3600)}h ago` + return `${Math.floor(diff / 86400)}d ago` +} + +export function SourcesPage({ onOpenStream }: Props) { + const [streams, setStreams] = useState([]) + const [, setStatus] = useState(null) + const [params, setParams] = useState(null) + const [loading, setLoading] = useState(true) + const [expandedSource, setExpandedSource] = useState(null) + const wsUpdate = useWebSocket() + + useEffect(() => { + Promise.all([fetchStreams(), fetchStatus(), fetchInsightsParams()]) + .then(([s, st, p]) => { setStreams(s); setStatus(st); setParams(p) }) + .catch(() => {}) + .finally(() => setLoading(false)) + }, [wsUpdate]) + + const groups = groupStreams(streams) + const dimensionGroups = params ? buildDimensionGroups(params, streams) : [] + const totalSources = groups.length + const activeSources = groups.filter((g) => g.activeCount > 0).length + const totalStreams = streams.length + const activeStreamCount = streams.filter((s) => s.active).length + + const [showAddModal, setShowAddModal] = useState(false) + const dismissAddModal = useCallback(() => setShowAddModal(false), []) + + return ( +
+
+

Sources

+ +
+ + {showAddModal && ( +
{ if (e.target === e.currentTarget) dismissAddModal() }} + > +
+

+ Add Source +

+

+ Add always-on sources for AWS, Vercel, Supabase, OTel and more for continuous runtime feedback for your AI tooling. +

+
+ + + Learn More + +
+
+
+ )} + + {/* Summary stats row — matches Dstl8 sources page */} +
+ + + +
+ + {/* Source list */} + {loading ? ( +
+ Loading sources... +
+ ) : groups.length === 0 ? ( +
+ No active sources. Start sending logs to Gonzo to see sources here. +
+ ) : ( +
+ {groups.map((g) => { + const isExpanded = expandedSource === g.source + const isHealthy = g.activeCount > 0 + return ( +
+ {/* Source row */} +
setExpandedSource(isExpanded ? null : g.source)} + style={{ + display: 'flex', + alignItems: 'center', + gap: 16, + padding: '16px 24px', + background: 'var(--bg-card)', + border: '1px solid var(--border)', + borderBottom: isExpanded ? 'none' : undefined, + borderTopLeftRadius: 'var(--radius)', + borderTopRightRadius: 'var(--radius)', + borderBottomLeftRadius: isExpanded ? 0 : 'var(--radius)', + borderBottomRightRadius: isExpanded ? 0 : 'var(--radius)', + marginBottom: isExpanded ? 0 : 8, + cursor: 'pointer', + transition: 'all 0.15s ease', + }} + onMouseEnter={(e) => { if (!isExpanded) e.currentTarget.style.background = 'var(--bg-tertiary)' }} + onMouseLeave={(e) => { if (!isExpanded) e.currentTarget.style.background = 'var(--bg-card)' }} + > + {/* Source icon */} + + {sourceIcons[g.source] || sourceIcons.unknown} + + + {/* Source name + meta */} +
+
+ {g.source.charAt(0).toUpperCase() + g.source.slice(1)} +
+
+ {g.streams.length} stream{g.streams.length !== 1 ? 's' : ''} · Last seen { + g.streams.length > 0 ? formatLastSeen(g.streams[0].last_seen) : 'never' + } +
+
+ + {/* Health badge */} +
+ + + {isHealthy ? 'Healthy' : 'Inactive'} + +
+ + {/* Chevron */} + + ▼ + +
+ + {/* Expanded streams panel */} + {isExpanded && ( +
+ {/* Dimension groups — shows streams grouped by Host, Service, etc. */} + {dimensionGroups.length > 0 && ( +
+
+ Dimensions +
+ {dimensionGroups.map((dim) => ( +
+
+ {dim.label} ({dim.values.length}) +
+ {dim.values.map((v) => ( +
{ + e.stopPropagation() + onOpenStream?.(dim.label.toLowerCase(), v.name) + }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 12, + padding: '6px 12px 6px 20px', + cursor: onOpenStream ? 'pointer' : 'default', + borderRadius: 'calc(var(--radius) - 2px)', + transition: 'background 0.15s ease', + }} + onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--bg-tertiary)' }} + onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }} + > + + + {v.name} + + {onOpenStream && ( + + )} +
+ ))} +
+ ))} +
+ )} +
+ )} +
+ ) + })} + +
+ )} +
+ ) +} + +function SummaryStat({ icon, value, label, color }: { icon: string; value: number | string; label: string; color: string }) { + return ( +
+ {icon} +
+
{value}
+
{label}
+
+
+ ) +} diff --git a/web/src/pages/StreamDetailsPage.tsx b/web/src/pages/StreamDetailsPage.tsx new file mode 100644 index 0000000..fa66e9d --- /dev/null +++ b/web/src/pages/StreamDetailsPage.tsx @@ -0,0 +1,146 @@ +import { useEffect, useState, useCallback } from 'react' +import { ErrorBoundary } from '../components/ui/ErrorBoundary' +import { MobiusChat } from '../components/layout/MobiusChat' +import { SeverityDistribution } from '../components/panels/SeverityDistribution' +import { PatternAnalysis } from '../components/panels/PatternAnalysis' +import { LogViewer } from '../components/panels/LogViewer' +import { useWebSocket } from '../hooks/useWebSocket' +import { fetchStatus } from '../api/client' +import type { StatusInfo } from '../api/types' + +interface Props { + dimensionType: string + dimensionValue: string +} + +export function StreamDetailsPage({ dimensionType, dimensionValue }: Props) { + const [status, setStatus] = useState(null) + const [refreshKey, setRefreshKey] = useState(0) + const [activeTab, setActiveTab] = useState<'patterns' | 'logs'>('patterns') + const wsUpdate = useWebSocket() + + const loadStatus = useCallback(() => { + fetchStatus().then(setStatus).catch(() => {}) + }, []) + + useEffect(() => { + loadStatus() + const interval = setInterval(loadStatus, 5000) + return () => clearInterval(interval) + }, [loadStatus]) + + useEffect(() => { + if (wsUpdate) setRefreshKey((k) => k + 1) + }, [wsUpdate]) + + const search = dimensionValue + + return ( +
+ {/* Stream header */} +
+
+

{dimensionValue}

+ + {dimensionType} + + {status && ( +
+ {status.total_logs.toLocaleString()} total logs + {status.log_rate.toFixed(1)}/s +
+ )} +
+
+ + {/* Main content + Mobius sidebar */} +
+
+ + {/* Severity distribution (real-time stacked bar chart) */} +
+ + + +
+ + {/* Tabbed section: Patterns | Logs */} +
+ {/* Tab bar */} +
+ + +
+ + {/* Tab content */} +
+ {activeTab === 'patterns' && ( + + + + )} + {activeTab === 'logs' && ( + + + + )} +
+
+
+ +
+ +
+
+
+ ) +} diff --git a/web/src/pages/UpgradePage.tsx b/web/src/pages/UpgradePage.tsx new file mode 100644 index 0000000..18bacbd --- /dev/null +++ b/web/src/pages/UpgradePage.tsx @@ -0,0 +1,71 @@ +import { upsellURL } from '../lib/constants' +import { Icons } from '../lib/icons' + +const pageLabels: Record = { + alerts: { + title: 'Alerts', + description: + 'Create, track, and resolve alerts with AI-powered root cause analysis and team collaboration.', + }, + sources: { + title: 'Source Management', + description: + 'Connect to AWS, GCP, Azure, Kubernetes, and 20+ other log sources with one-click setup.', + }, + users: { + title: 'Team Management', + description: + '...And share runtime context across your team and AI tooling.', + }, + retention: { + title: 'Retention', + description: + 'Configure log retention policies, storage tiers, and archival rules for long-term analysis.', + }, + support: { + title: 'Priority Support', + description: + 'Get direct access to our engineering team with priority response times and dedicated support.', + }, +} + +interface Props { + featureId: string +} + +export function UpgradePage({ featureId }: Props) { + const info = pageLabels[featureId] ?? { + title: 'Premium Feature', + description: 'This feature is available on the full Dstl8 dashboard.', + } + + return ( +
+
+
{Icons.lock}
+

{info.title}

+

{info.description}

+ + Upgrade to Dstl8 Pro + +

+ Dstl8 Lite is powered by{' '} + + Gonzo + + {' '}— the full Dstl8 dashboard unlocks all features. +

+
+
+ ) +} diff --git a/web/src/pages/WorkspaceDetailsPage.tsx b/web/src/pages/WorkspaceDetailsPage.tsx new file mode 100644 index 0000000..d316f7f --- /dev/null +++ b/web/src/pages/WorkspaceDetailsPage.tsx @@ -0,0 +1,423 @@ +import { useEffect, useState, useCallback, useRef } from 'react' +import { ErrorBoundary } from '../components/ui/ErrorBoundary' +import { MobiusChat } from '../components/layout/MobiusChat' +import { SourcesSection } from '../components/panels/SourcesSection' +import { SeverityDistribution } from '../components/panels/SeverityDistribution' +import { LogViewer } from '../components/panels/LogViewer' +import { useWebSocket } from '../hooks/useWebSocket' +import { fetchStatus, fetchTopAttributes } from '../api/client' +import { upsellURL } from '../lib/constants' +import type { StatusInfo, AttributeEntry } from '../api/types' + +interface Props { + onOpenStream?: (dimensionType: string, dimensionValue: string) => void +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB` + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB` +} + +function StatRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +export function WorkspaceDetailsPage({ onOpenStream }: Props) { + const [status, setStatus] = useState(null) + const [attributes, setAttributes] = useState([]) + const [refreshKey, setRefreshKey] = useState(0) + const [logModalOpen, setLogModalOpen] = useState(false) + const [paused, setPaused] = useState(false) + const pausedRef = useRef(false) + const wsUpdate = useWebSocket() + + const togglePaused = useCallback(() => { + setPaused((prev) => { + const next = !prev + pausedRef.current = next + return next + }) + }, []) + + const loadStatus = useCallback(() => { + fetchStatus().then(setStatus).catch(() => {}) + }, []) + + const loadAttributes = useCallback(() => { + fetchTopAttributes(5).then(setAttributes).catch(() => {}) + }, []) + + useEffect(() => { + loadStatus() + loadAttributes() + const interval = setInterval(() => { + if (!pausedRef.current) { + loadStatus() + loadAttributes() + } + }, 5000) + return () => clearInterval(interval) + }, [loadStatus, loadAttributes]) + + useEffect(() => { + if (wsUpdate && !pausedRef.current) setRefreshKey((k) => k + 1) + }, [wsUpdate]) + + return ( +
+ {/* Workspace header with inline stats */} +
+
+

Gonzo!

+ + {status && ( +
+ {status.total_logs.toLocaleString()} logs + {status.log_rate.toFixed(1)}/s + {status.uptime} +
+ )} +
+
+ + {/* Main content + Mobius sidebar */} +
+
+ + {/* ── General Statistics + Severity Distribution ── */} +
+ {/* General Statistics card */} + {status && ( +
+

+ General Statistics +

+ + + + + + s.active).length.toString()} /> +
+ )} + + {/* Severity Distribution chart */} + + + +
+ + {/* ── Top Attributes table ── */} + {attributes.length > 0 && ( +
+
+

+ Top Attributes +

+
+ + + + + + + + + + + {attributes.map((attr, i) => ( + + + + + + + ))} + +
KeyValueCount%
{attr.key}{attr.value} + {attr.count.toLocaleString()} + +
+
+
+
+ + {attr.percentage.toFixed(1)}% + +
+
+
+ )} + + {/* ── Live Log Stream preview ── */} +
+
+

+ Live Logs +

+ +
+
+ + + +
+
+ + {/* ── Full-screen Log Modal ── */} + {logModalOpen && ( +
{ + if (e.target === e.currentTarget) setLogModalOpen(false) + }} + > +
+
+

+ Live Logs +

+ +
+
+ + + +
+
+
+ )} + + {/* ── Sources section ── */} + + + +
+ +
+ {/* Mobius Analysis upsell */} +
+
+ Möbius + + Möbius Analysis + +
+

+ Get AI-powered summaries, correlated anomaly detection, and + root-cause analysis across all your sources. +

+ + Upgrade for Möbius AI Analysis + Root cause analysis, summaries, alerts and chat + +
+ + {/* Alerts upsell */} +
+
+ + Alerts + + + Cloud + +
+

+ Create, track, and resolve alerts with automatic correlation, + timeline views, and team collaboration. +

+ + Upgrade for Alerts + +
+ + +
+
+
+ ) +} diff --git a/web/src/pages/WorkspacesPage.tsx b/web/src/pages/WorkspacesPage.tsx new file mode 100644 index 0000000..36a5980 --- /dev/null +++ b/web/src/pages/WorkspacesPage.tsx @@ -0,0 +1,210 @@ +import { useEffect, useState } from 'react' +import { fetchStatus } from '../api/client' +import type { StatusInfo } from '../api/types' +import { upsellURL } from '../lib/constants' +import { Icons } from '../lib/icons' + +interface Props { + onOpenWorkspace: () => void +} + +export function WorkspacesPage({ onOpenWorkspace }: Props) { + const [status, setStatus] = useState(null) + const [showNewModal, setShowNewModal] = useState(false) + + useEffect(() => { + fetchStatus().then(setStatus).catch(() => {}) + const interval = setInterval(() => { + fetchStatus().then(setStatus).catch(() => {}) + }, 5000) + return () => clearInterval(interval) + }, []) + + const activeStreams = status?.streams.filter((s) => s.active) ?? [] + const streams = activeStreams.length + const sources = new Set(activeStreams.map((s) => s.source)).size + const totalLogs = status?.total_logs ?? 0 + + return ( +
+
+

Workspaces

+ +
+ + {/* Stats bar */} +
+
+
+ + + + +
+
+
1
+
Workspace
+
+
+
+
+ + + +
+
+
{streams}
+
Active Streams
+
+
+
+
+ + + +
+
+
{totalLogs.toLocaleString()}
+
Total Logs
+
+
+
+ + {/* Workspace grid */} +
+
+
+
Gonzo!
+
+
+ +
+
+
Streams
+
{streams}
+
+
+
Logs
+
{totalLogs.toLocaleString()}
+
+
+
Rate
+
+ {status ? `${status.log_rate.toFixed(0)}/s` : '—'} +
+
+
+ + {/* Simple sparkline placeholder */} +
+ {Array.from({ length: 24 }, (_, i) => { + const h = Math.max(4, Math.random() * 32 + (i > 18 ? 10 : 0)) + return ( +
+ ) + })} +
+ +
+
+ + + + {sources} sources +
+
+ + + + {status?.uptime ?? '—'} uptime +
+
+
+
+ + {/* New Workspace upsell modal */} + {showNewModal && ( +
{ if (e.target === e.currentTarget) setShowNewModal(false) }} + > +
+
+

+ Add Workspace +

+ +
+

+ Dstl8 Lite supports a single local workspace powered by Gonzo. +

+

+ Upgrade to Dstl8 Pro for multiple workspaces, team collaboration, + cloud log sources (AWS, GCP, Azure, Supabase, Vercel), and more. +

+
+ + Upgrade to Dstl8 Pro + + +
+
+
+ )} +
+ ) +} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tailwind.config.ts b/web/tailwind.config.ts new file mode 100644 index 0000000..f2ab0fd --- /dev/null +++ b/web/tailwind.config.ts @@ -0,0 +1,36 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: ['./index.html', './src/**/*.{ts,tsx}'], + darkMode: 'class', + theme: { + extend: { + colors: { + // Dstl8 severity colors + severity: { + fatal: '#b91c1c', + error: '#dc2626', + warn: '#f97316', + info: '#3b82f6', + debug: '#6b7280', + trace: '#10b981', + unknown: '#9ca3af', + }, + // Dstl8 brand + brand: { + primary: '#6366f1', + secondary: '#8b5cf6', + }, + // Dashboard surface colors + surface: { + DEFAULT: '#ffffff', + secondary: '#f8fafc', + dark: '#0f172a', + 'dark-secondary': '#1e293b', + }, + }, + }, + }, + plugins: [], +} +export default config diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..39a405b --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..a0018c4 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + build: { + outDir: 'dist', + sourcemap: false, + // Keep bundle small for embedding + rollupOptions: { + output: { + manualChunks: { + recharts: ['recharts'], + }, + }, + }, + }, + server: { + proxy: { + '/api': 'http://localhost:5718', + '/ws': { + target: 'ws://localhost:5718', + ws: true, + }, + }, + }, +})