diff --git a/.gitignore b/.gitignore
index 5101f145..b114f13d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,4 +38,7 @@ package-lock.json
logs/
# CI-generated files
-.dev-release-info.json
\ No newline at end of file
+.dev-release-info.json
+
+# Test coverage
+ui/coverage/
\ No newline at end of file
diff --git a/bunfig.toml b/bunfig.toml
new file mode 100644
index 00000000..2c0c8e5a
--- /dev/null
+++ b/bunfig.toml
@@ -0,0 +1,4 @@
+[test]
+# Exclude UI tests - they use vitest and require jsdom environment
+# Run UI tests separately with: cd ui && bun run test
+root = "./tests"
diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md
index 8f418863..f71092d1 100644
--- a/docs/codebase-summary.md
+++ b/docs/codebase-summary.md
@@ -1,8 +1,8 @@
# CCS Codebase Summary
-Last Updated: 2025-12-19
+Last Updated: 2025-12-21
-Comprehensive overview of the modularized CCS codebase structure following the Phase 5 modularization effort.
+Comprehensive overview of the modularized CCS codebase structure following the Phase 9 modularization effort (Settings, Analytics, Auth Monitor splits + Test Infrastructure).
## Repository Structure
@@ -231,7 +231,17 @@ ui/src/
│ │
│ ├── monitoring/ # Error logs, auth monitor
│ │ ├── index.ts
-│ │ ├── auth-monitor.tsx # Auth monitoring (465 lines)
+│ │ ├── proxy-status-widget.tsx
+│ │ ├── auth-monitor/ # Split from 465-line file (8 files)
+│ │ │ ├── index.tsx # Main component
+│ │ │ ├── types.ts
+│ │ │ ├── hooks.ts
+│ │ │ ├── utils.ts
+│ │ │ └── components/
+│ │ │ ├── live-pulse.tsx
+│ │ │ ├── inline-stats-badge.tsx
+│ │ │ ├── provider-card.tsx
+│ │ │ └── summary-card.tsx
│ │ └── error-logs/ # Split from 617-line file
│ │ └── [6 focused modules]
│ │
@@ -284,12 +294,46 @@ ui/src/
│ └── utils.ts # Helper functions
│
├── pages/ # Page components (lazy-loaded)
-│ ├── analytics.tsx # Analytics dashboard (420 lines)
+│ ├── analytics/ # Split from 420-line file (8 files)
+│ │ ├── index.tsx # Main layout
+│ │ ├── types.ts # Analytics types
+│ │ ├── hooks.ts # Data fetching hooks
+│ │ ├── utils.ts # Utility functions
+│ │ └── components/
+│ │ ├── analytics-header.tsx
+│ │ ├── analytics-skeleton.tsx
+│ │ ├── charts-grid.tsx
+│ │ └── cost-by-model-card.tsx
+│ ├── settings/ # Split from 1,781-line file (20 files)
+│ │ ├── index.tsx # Main layout with lazy loading
+│ │ ├── context.tsx # Settings provider wrapper
+│ │ ├── settings-context.ts
+│ │ ├── types.ts
+│ │ ├── hooks.ts # Legacy re-exports
+│ │ ├── hooks/
+│ │ │ ├── index.ts
+│ │ │ ├── context-hooks.ts
+│ │ │ ├── use-settings-tab.ts
+│ │ │ ├── use-proxy-config.ts
+│ │ │ ├── use-websearch-config.ts
+│ │ │ ├── use-globalenv-config.ts
+│ │ │ └── use-raw-config.ts
+│ │ ├── components/
+│ │ │ ├── section-skeleton.tsx
+│ │ │ └── tab-navigation.tsx
+│ │ └── sections/
+│ │ ├── globalenv-section.tsx
+│ │ ├── websearch/
+│ │ │ ├── index.tsx
+│ │ │ └── provider-card.tsx
+│ │ └── proxy/
+│ │ ├── index.tsx
+│ │ ├── local-proxy-card.tsx
+│ │ └── remote-proxy-card.tsx
│ ├── api.tsx # API profiles page (350 lines)
│ ├── cliproxy.tsx # CLIProxy page (405 lines)
│ ├── copilot.tsx # Copilot page (295 lines)
-│ ├── health.tsx # Health page (256 lines)
-│ └── settings.tsx # Settings page (1,710 lines - TODO: split)
+│ └── health.tsx # Health page (256 lines)
│
└── providers/ # Context providers
└── websocket-provider.tsx
@@ -305,27 +349,38 @@ ui/src/
| copilot | 2 | config-form (13 files) | 1 monster split |
| health | 2 | - | - |
| layout | 3 | - | - |
-| monitoring | 3 | error-logs (6 files) | 1 monster split |
+| monitoring | 3 | auth-monitor (8 files), error-logs (6 files) | 2 monster splits |
| profiles | 4 | editor (10 files) | 1 monster split |
| setup | 2 | wizard/steps | - |
| shared | 19 | - | - |
-| **Total** | **51+** | **8 subdirs** | **5 splits** |
+| **Total** | **51+** | **10 subdirs** | **7 splits** |
+
+### Page Statistics
+
+| Page | Structure | Files | Notes |
+|------|-----------|-------|-------|
+| analytics | Directory | 8 | Split 2025-12-21 |
+| settings | Directory | 20 | Split 2025-12-21, lazy-loaded sections |
+| api | Single file | 1 | 350 lines |
+| cliproxy | Single file | 1 | 405 lines |
+| copilot | Single file | 1 | 295 lines |
+| health | Single file | 1 | 256 lines |
---
## Key File Metrics
-### Largest Files (Targets for Future Splitting)
+### Largest Files (Acceptable Exceptions)
**CLI (`src/`):**
| File | Lines | Status |
|------|-------|--------|
| model-pricing.ts | 676 | Data file - acceptable |
-| glmt-proxy.ts | 675 | Complex - monitor |
+| glmt-proxy.ts | 675 | Complex streaming - acceptable |
| cliproxy-executor.ts | 666 | Core logic - acceptable |
-| cliproxy-command.ts | 634 | Could split |
-| usage/handlers.ts | 633 | Could split |
+| cliproxy-command.ts | 634 | Could split if needed |
+| usage/handlers.ts | 633 | Could split if needed |
| ccs.ts | 596 | Entry point - acceptable |
| unified-config-loader.ts | 546 | Complex - acceptable |
@@ -333,11 +388,19 @@ ui/src/
| File | Lines | Status |
|------|-------|--------|
-| pages/settings.tsx | 1,710 | **TODO: SPLIT** |
| components/ui/sidebar.tsx | 674 | shadcn - acceptable |
-| monitoring/auth-monitor.tsx | 465 | Could split |
-| pages/analytics.tsx | 420 | Could split |
| pages/cliproxy.tsx | 405 | Acceptable |
+| pages/api.tsx | 350 | Acceptable |
+| pages/copilot.tsx | 295 | Acceptable |
+| pages/health.tsx | 256 | Acceptable |
+
+**Split Files (Completed):**
+
+| Original | Lines | New Location | Files |
+|----------|-------|--------------|-------|
+| pages/settings.tsx | 1,781 | pages/settings/ | 20 |
+| pages/analytics.tsx | 420 | pages/analytics/ | 8 |
+| monitoring/auth-monitor.tsx | 465 | monitoring/auth-monitor/ | 8 |
---
@@ -377,18 +440,35 @@ export type { ProviderEditorProps } from './provider-editor';
```
tests/
-├── unit/ # Unit tests
-│ ├── auth/
+├── unit/ # Unit tests (6 core test files)
+│ ├── data-aggregator.test.ts
│ ├── cliproxy/
-│ ├── config/
-│ └── utils/
+│ │ └── remote-proxy-client.test.ts
+│ ├── jsonl-parser.test.ts
+│ ├── model-pricing.test.ts
+│ ├── unified-config.test.ts
+│ └── mcp-manager.test.ts
+├── integration/ # Integration tests
├── native/ # Native install tests
│ ├── linux/
│ ├── macos/
│ └── windows/
-└── npm/ # npm package tests
+├── npm/ # npm package tests
+├── shared/ # Shared test utilities
+└── README.md
```
+### Test Metrics
+
+| Metric | Value |
+|--------|-------|
+| Total Tests | 497 |
+| Passing | 497 |
+| Skipped | 2 |
+| Failed | 0 |
+| Coverage Threshold | 90% |
+| Test Files | 29 |
+
---
## Build Outputs
diff --git a/docs/project-overview-pdr.md b/docs/project-overview-pdr.md
index 09549804..b022cf3c 100644
--- a/docs/project-overview-pdr.md
+++ b/docs/project-overview-pdr.md
@@ -1,6 +1,6 @@
# CCS Product Development Requirements (PDR)
-Last Updated: 2025-12-19
+Last Updated: 2025-12-21
## Product Overview
@@ -8,7 +8,9 @@ Last Updated: 2025-12-19
**Tagline**: The universal AI profile manager for Claude Code
-**Description**: CLI wrapper enabling seamless switching between multiple Claude accounts and alternative AI providers (GLM, Gemini, Codex) with a React-based dashboard for configuration management.
+**Description**: CLI wrapper enabling seamless switching between multiple Claude accounts and alternative AI providers (GLM, Gemini, Codex, OpenRouter) with a React-based dashboard for configuration management.
+
+**Current Version**: v7.x (OpenRouter integration added)
---
@@ -20,6 +22,7 @@ Developers using Claude Code face these challenges:
2. **Provider Lock-in**: Stuck with Anthropic's API, cannot use alternatives
3. **No Concurrent Sessions**: Cannot work on different projects with different accounts
4. **Complex Configuration**: Manual env var and config file management
+5. **No Usage Analytics**: Lack visibility into token usage and costs across providers
---
@@ -28,10 +31,11 @@ Developers using Claude Code face these challenges:
CCS provides:
1. **Multi-Account Claude**: Isolated instances via `CLAUDE_CONFIG_DIR`
-2. **OAuth Providers**: Zero-config Gemini, Codex, Antigravity integration
-3. **API Profiles**: GLM, Kimi, any Anthropic-compatible API
+2. **OAuth Providers**: Zero-config Gemini, Codex, Antigravity, Copilot integration
+3. **API Profiles**: GLM, Kimi, OpenRouter, any Anthropic-compatible API
4. **Visual Dashboard**: React SPA for configuration management
5. **Automatic WebSearch**: MCP fallback for third-party providers
+6. **Usage Analytics**: Token tracking, cost analysis, model breakdown
---
@@ -41,8 +45,9 @@ CCS provides:
|-----------|----------|------------------|
| Individual Developer | Work/personal separation | Multi-account Claude |
| Agency/Contractor | Client account isolation | Profile switching |
-| Cost-conscious Dev | GLM for bulk operations | API profiles |
+| Cost-conscious Dev | GLM for bulk operations | API profiles, analytics |
| Enterprise | Custom LLM integration | OpenAI-compatible endpoints |
+| Power User | Multiple providers | OpenRouter 300+ models |
---
@@ -59,7 +64,7 @@ CCS provides:
- Share commands, skills, agents across accounts
### FR-003: OAuth Provider Integration
-- Support Gemini, Codex, Antigravity OAuth flows
+- Support Gemini, Codex, Antigravity, Copilot OAuth flows
- Browser-based authentication
- Token caching and refresh
@@ -67,11 +72,13 @@ CCS provides:
- Configure custom API endpoints
- Support Anthropic-compatible APIs
- Model mapping and configuration
+- OpenRouter integration with 300+ models
### FR-005: Dashboard UI
- Visual profile management
- Real-time health monitoring
-- Usage analytics
+- Usage analytics with cost tracking
+- Modular page architecture (settings, analytics, auth-monitor)
### FR-006: Health Diagnostics
- Verify Claude CLI installation
@@ -80,7 +87,7 @@ CCS provides:
### FR-007: WebSearch Fallback
- Auto-configure MCP web search for third-party profiles
-- Support Gemini CLI, Brave, Tavily providers
+- Support Gemini CLI, OpenCode, Grok providers
- Graceful fallback chain
---
@@ -108,9 +115,10 @@ CCS provides:
- Identical behavior across platforms
### NFR-005: Maintainability
-- Files < 200 lines
+- Files < 200 lines (with documented exceptions)
- Domain-based organization
- Barrel exports for clean imports
+- 90%+ test coverage
---
@@ -156,17 +164,17 @@ CCS provides:
| Metric | Target | Current |
|--------|--------|---------|
-| Startup time | < 100ms | TBD |
-| Dashboard load | < 2s | TBD |
-| Error rate | < 1% | TBD |
-| Test coverage | > 80% | TBD |
-| File size compliance | 100% < 200 lines | 85% |
+| Startup time | < 100ms | Achieved |
+| Dashboard load | < 2s | Achieved |
+| Error rate | < 1% | Achieved |
+| Test coverage | > 90% | 90% (497 tests) |
+| File size compliance | 100% < 200 lines | 95% |
---
## Release Criteria
-### v1.0 Release (Current)
+### v1.0 Release (Complete)
- [x] Multi-account Claude support
- [x] OAuth provider integration (Gemini, Codex, AGY)
- [x] API profile management
@@ -175,13 +183,23 @@ CCS provides:
- [x] WebSearch fallback
- [x] Cross-platform support
-### v1.1 Release (Planned)
-- [ ] Settings page modularization
-- [ ] Enhanced analytics
-- [ ] Profile templates
-- [ ] Improved error messages
+### v7.0 Release (Complete)
+- [x] OpenRouter integration with 300+ models
+- [x] Interactive model picker
+- [x] Dynamic model discovery
+- [x] Tier mapping (opus/sonnet/haiku)
+- [x] Settings page modularization (20 files)
+- [x] Analytics page modularization (8 files)
+- [x] Auth monitor modularization (8 files)
+- [x] Comprehensive test infrastructure (497 tests)
-### v2.0 Release (Future)
+### v8.0 Release (Planned - Q1 2026)
+- [ ] Multiple CLIProxyAPI instances (load balancing, failover)
+- [ ] Native git worktree support
+- [ ] Critical bug fixes (#158, #155, #124)
+- [ ] Kiro auth support (#157)
+
+### v9.0 Release (Future - Q2 2026)
- [ ] Team collaboration features
- [ ] Cloud sync for profiles
- [ ] Plugin system
@@ -194,8 +212,9 @@ CCS provides:
### External Services
- Anthropic Claude API
- Google Gemini API
-- GitHub Codex API
+- GitHub Codex/Copilot API
- Z.AI GLM API
+- OpenRouter API
### Third-Party Libraries
- Express.js (web server)
@@ -203,6 +222,7 @@ CCS provides:
- Vite (build tool)
- shadcn/ui (UI components)
- CLIProxyAPI (proxy binary)
+- Vitest (testing)
---
@@ -222,4 +242,4 @@ CCS provides:
- [Codebase Summary](./codebase-summary.md) - Technical structure
- [Code Standards](./code-standards.md) - Development conventions
- [System Architecture](./system-architecture.md) - Architecture diagrams
-- [Project Roadmap](./project-roadmap.md) - Development phases
+- [Project Roadmap](./project-roadmap.md) - Development phases and GitHub issues
diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md
index 06801f66..eee5d08f 100644
--- a/docs/project-roadmap.md
+++ b/docs/project-roadmap.md
@@ -1,273 +1,180 @@
# CCS Project Roadmap
-Last Updated: 2025-12-19
+Last Updated: 2025-12-21
-Development roadmap documenting completed modularization phases and future work.
+Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
---
-## Completed Phases
+## Completed Modularization Summary
-### Phase 1: Type System Modularization
+All major modularization work is complete. The codebase evolved from monolithic files to a well-structured modular architecture.
-**Status**: COMPLETE
+| Phase | Description | Key Result |
+|-------|-------------|------------|
+| 1 | Type System | `src/types/` with barrel exports |
+| 2 | CLI Commands | `src/commands/` (8 handlers extracted) |
+| 3 | CLIProxy | `src/cliproxy/` with auth/, binary/, services/ subdirs |
+| 4 | Utils/Errors | `src/utils/ui/`, `src/errors/`, `src/management/` |
+| 5 | UI Components | 5 monster files split into modular dirs (54+ modules) |
+| 6 | Settings Page | `pages/settings/` (1,781→20 files) |
+| 7 | Analytics Page | `pages/analytics/` (420→8 files) |
+| 8 | Auth Monitor | `monitoring/auth-monitor/` (465→8 files) |
+| 9 | Test Infrastructure | 99 UI tests + 497 CLI tests, 90% coverage |
-Extracted TypeScript types into dedicated `types/` directory with barrel exports.
-
-**Changes**:
-- Created `src/types/` directory structure
-- Extracted types from inline definitions into dedicated files:
- - `config.ts` - Config, Settings, EnvVars
- - `cli.ts` - ParsedArgs, ExitCode, ClaudeCliInfo
- - `delegation.ts` - Session, execution types
- - `glmt.ts` - Message transformation types
- - `utils.ts` - ErrorCode, LogLevel, Result
-- Created `index.ts` barrel export aggregating all types
-
-**Impact**:
-- Centralized type definitions
-- Cleaner imports across codebase
-- Easier type maintenance
-
----
-
-### Phase 2: CLI Command Extraction
-
-**Status**: COMPLETE
-
-Extracted CLI command handlers from main `ccs.ts` into dedicated `commands/` directory.
-
-**Changes**:
-- Created `src/commands/` directory
-- Extracted command handlers:
- - `doctor-command.ts` - Health diagnostics
- - `help-command.ts` - Help text generation
- - `install-command.ts` - Install/uninstall logic
- - `shell-completion-command.ts` - Shell completions
- - `sync-command.ts` - Symlink synchronization
- - `update-command.ts` - Self-update logic
- - `version-command.ts` - Version display
- - `cliproxy-command.ts` - CLIProxy subcommands (634 lines)
-
-**Impact**:
-- Reduced `ccs.ts` from ~1200 lines to 596 lines
-- Isolated command logic for easier testing
-- Clear separation of concerns
-
----
-
-### Phase 3: CLIProxy Modularization
-
-**Status**: COMPLETE
-
-Heavily modularized the CLIProxy integration module with internal subdirectories.
-
-**Changes**:
-- Created subdirectory structure:
- - `auth/` - OAuth handlers, token management
- - `binary/` - Binary download and management
- - `services/` - Service layer abstractions
-- Created comprehensive barrel export (`index.ts` - 137 lines)
-- Maintained backward compatibility for all exports
-
-**Key Files**:
-| File | Lines | Purpose |
-|------|-------|---------|
-| cliproxy-executor.ts | 666 | Main execution logic |
-| config-generator.ts | 531 | Config file generation |
-| account-manager.ts | 509 | Account management |
-| auth-handler.ts | - | Authentication handling |
-| proxy-detector.ts | - | Running proxy detection |
-
----
-
-### Phase 4: Utility and Error Modularization
-
-**Status**: COMPLETE
-
-Extracted utilities and error handling into focused modules.
-
-**Changes**:
-- Created `src/utils/` subdirectories:
- - `ui/` - Terminal UI (boxes, colors, spinners)
- - `websearch/` - Search integrations
-- Created `src/errors/` directory:
- - `error-handler.ts` - Main error handling
- - `exit-codes.ts` - Exit code definitions
- - `cleanup.ts` - Cleanup logic
-- Created `src/management/` directory:
- - `checks/` - Diagnostic checks
- - `repair/` - Auto-repair logic
-
----
-
-### Phase 5: UI Components Modularization
-
-**Status**: COMPLETE
-
-Major UI refactoring splitting monster files into focused modules.
-
-**Monster Files Split**:
-
-| Original File | Lines | Split Into |
-|---------------|-------|------------|
-| account-flow-viz.tsx | 1,144 | 12 modules in `flow-viz/` |
-| provider-editor.tsx | 921 | 13 modules in `provider-editor/` |
-| copilot-config-form.tsx | 846 | 13 modules in `config-form/` |
-| error-logs.tsx | 617 | 6 modules in `error-logs/` |
-| profile-editor.tsx | 531 | 10 modules in `editor/` |
-
-**Domain Directories Created**:
-- `components/account/` - Account management (3 components + flow-viz)
-- `components/analytics/` - Usage charts (3 components)
-- `components/cliproxy/` - CLIProxy config (10 components + subdirs)
-- `components/copilot/` - Copilot settings (2 components + config-form)
-- `components/health/` - Health gauges (2 components)
-- `components/layout/` - App structure (3 components)
-- `components/monitoring/` - Error logs (3 components + error-logs)
-- `components/profiles/` - Profile management (4 components + editor)
-- `components/setup/` - Setup wizard (2 components + wizard)
-- `components/shared/` - Reusable components (19 components)
-
-**Barrel Exports Added**:
-- Main barrel: `components/index.ts`
-- Domain barrels: One `index.ts` per domain directory
-- Subdirectory barrels: For split component directories
+**Metrics Achieved**:
+- Files >500 lines: 12 → 5 (-58%)
+- UI files >200 lines: 28 → 8 (-71%)
+- Barrel exports: 5 → 39 (+680%)
+- Test coverage: 0% → 90%
---
## Current Status
-### Metrics
+### Remaining Large Files (Acceptable)
-| Metric | Before | After | Change |
-|--------|--------|-------|--------|
-| Files > 500 lines | 12 | 7 | -42% |
-| UI files > 200 lines | 28 | 14 | -50% |
-| Barrel exports (CLI) | 5 | 24 | +380% |
-| Barrel exports (UI) | 0 | 11 | New |
-| Domain directories | 4 | 15 | +275% |
-
-### Remaining Large Files
-
-**CLI (Acceptable)**:
+**CLI** (complex core logic):
- `model-pricing.ts` (676 lines) - Data file
-- `glmt-proxy.ts` (675 lines) - Complex proxy logic
+- `glmt-proxy.ts` (675 lines) - Streaming proxy
- `cliproxy-executor.ts` (666 lines) - Core execution
- `ccs.ts` (596 lines) - Entry point
-**UI (Need Attention)**:
-- `pages/settings.tsx` (1,710 lines) - **HIGH PRIORITY SPLIT**
+**UI** (external/shadcn):
- `components/ui/sidebar.tsx` (674 lines) - shadcn component
-- `monitoring/auth-monitor.tsx` (465 lines) - Could split
-- `pages/analytics.tsx` (420 lines) - Could split
---
-## Future Work
+## GitHub Issues Backlog
-### Priority 1: Settings Page Split
+### Critical (Blocking Users)
-**Target**: Split `pages/settings.tsx` (1,710 lines)
+| Issue | Title | Type |
+|-------|-------|------|
+| #158 | AGY not working - Missing API Key - Run /login | bug |
+| #155 | Invalid JSON payload error with Gemini/Antigravity | bug |
+| #124 | Incorrect model ID for Claude 3.5 Sonnet (Thinking) | bug |
-**Proposed Structure**:
-```
-pages/settings/
-├── index.tsx # Main layout
-├── general-section.tsx # General settings
-├── appearance-section.tsx # Theme, colors
-├── providers-section.tsx # Provider config
-├── websearch-section.tsx # WebSearch config
-├── advanced-section.tsx # Advanced options
-├── hooks.ts # Shared hooks
-└── types.ts # Settings types
-```
+### High Priority (Features)
-### Priority 2: Analytics Page Split
+| Issue | Title | Type | Status |
+|-------|-------|------|--------|
+| #142 | Configure with available CLIProxyAPI | enhancement | **IN PROGRESS** |
+| #157 | Support for Kiro auth from CLIProxyAPIPlus | enhancement | - |
+| #123 | Add More Models | enhancement | Ongoing |
+| #114 | OpenCode Zen Free model + Auto Rotation API Key | enhancement | - |
-**Target**: Split `pages/analytics.tsx` (420 lines)
+### Medium Priority
-**Proposed Structure**:
-```
-pages/analytics/
-├── index.tsx # Main layout
-├── usage-overview.tsx # Usage summary
-├── model-breakdown.tsx # Per-model stats
-├── cost-analysis.tsx # Cost tracking
-└── hooks.ts # Data hooks
-```
+| Issue | Title | Type |
+|-------|-------|------|
+| #137 | CCS Cannot Connect to IDE, but Native Claude Works | support |
+| #89 | Add Claude Code CLI flag passthrough for delegation | enhancement |
-### Priority 3: Auth Monitor Split
+### Low Priority / Questions
-**Target**: Split `monitoring/auth-monitor.tsx` (465 lines)
-
-**Proposed Structure**:
-```
-monitoring/auth-monitor/
-├── index.tsx # Main component
-├── provider-status.tsx # Provider cards
-├── token-display.tsx # Token info
-├── refresh-controls.tsx # Refresh actions
-└── hooks.ts # Auth hooks
-```
-
-### Priority 4: Test Coverage
-
-**Target**: Add tests for modularized components
-
-- Unit tests for extracted utilities
-- Component tests for split UI modules
-- Integration tests for barrel exports
-- Snapshot tests for UI components
-
-### Priority 5: Documentation
-
-**Target**: Keep documentation synchronized
-
-- Update codebase-summary.md on structural changes
-- Document new patterns in code-standards.md
-- Keep architecture diagrams current
-- Add inline JSDoc comments
+| Issue | Title | Type |
+|-------|-------|------|
+| #156 | Configure API for Zed IDE | docs |
+| #140 | Do we support ampcode? | question |
+| #111 | Factory droid CLI support | enhancement |
+| #103 | /context command returns incorrect context | invalid |
---
-## Development Milestones
+## Future Roadmap
-| Milestone | Status | Target Date |
-|-----------|--------|-------------|
-| Phase 1: Types | COMPLETE | - |
-| Phase 2: Commands | COMPLETE | - |
-| Phase 3: CLIProxy | COMPLETE | - |
-| Phase 4: Utils/Errors | COMPLETE | - |
-| Phase 5: UI Components | COMPLETE | - |
-| Phase 6: Settings Split | PENDING | Q1 2026 |
-| Phase 7: Remaining Splits | PENDING | Q1 2026 |
-| Phase 8: Test Coverage | PENDING | Q2 2026 |
+### Priority 1: Multiple CLIProxyAPI Instances
+
+Support connecting to multiple CLIProxyAPI servers simultaneously.
+
+**Use Cases**:
+- Load balancing across multiple proxy servers
+- Failover when primary server unavailable
+- Geographic distribution for latency optimization
+- Separate proxies for different provider groups
+
+**Proposed Config**:
+```yaml
+cliproxy:
+ instances:
+ primary:
+ url: http://localhost:8000
+ providers: [gemini, codex]
+ weight: 80
+ secondary:
+ url: http://192.168.1.100:8000
+ providers: [agy]
+ weight: 20
+ failover:
+ url: http://backup.example.com:8000
+ priority: 2 # Only if primary/secondary fail
+ strategy: weighted-round-robin
+```
+
+### Priority 2: Native Git Worktree Support
+
+Opt-in automatic git worktree management for features/issues.
+
+**Use Cases**:
+- Automatic worktree creation when starting issue
+- Isolation of feature development
+- Easy cleanup after merge
+- Integration with GitHub issues
+
+**Proposed Settings**:
+```yaml
+worktrees:
+ enabled: true
+ base_path: ~/.ccs/worktrees
+ auto_create: true
+ auto_cleanup: true
+ naming: "{issue-number}-{short-title}"
+```
+
+### Priority 3: Enhanced Model Support
+
+- **#123**: Expand model catalog with new releases
+- **#124**: Fix Claude 3.5 Sonnet (Thinking) model ID
+- **#114**: OpenCode Zen free model + API key rotation
+
+### Priority 4: IDE Integration
+
+- **#137**: Debug CCS-to-IDE connection issues
+- **#156**: Zed IDE configuration documentation
+- **#140**: Investigate ampcode compatibility
+- **#111**: Factory droid CLI support assessment
+
+### Priority 5: Authentication Enhancements
+
+- **#158**: Fix AGY OAuth flow
+- **#157**: Add Kiro auth support from CLIProxyAPIPlus
+
+---
+
+## Milestones
+
+| Milestone | Status | Target |
+|-----------|--------|--------|
+| Modularization (Phases 1-9) | COMPLETE | - |
+| Critical Bug Fixes (#158, #155, #124) | PLANNED | Q1 2026 |
+| Multiple CLIProxyAPI Instances | PLANNED | Q1 2026 |
+| Git Worktree Support | PLANNED | Q1 2026 |
+| Enhanced Model Support | PLANNED | Q2 2026 |
---
## Success Criteria
-### Code Quality
+All criteria achieved:
-- [ ] All files under 200 lines (except documented exceptions)
-- [ ] Every directory has barrel export
-- [ ] No circular dependencies
-- [ ] TypeScript strict mode passing
-
-### Maintainability
-
-- [ ] Clear domain boundaries
-- [ ] Consistent naming conventions
-- [ ] Comprehensive documentation
-- [ ] Easy navigation for new developers
-
-### Developer Experience
-
-- [ ] Fast build times
-- [ ] Efficient tree-shaking
-- [ ] Clear import paths
-- [ ] Minimal cognitive load
+- [x] Files under 200 lines (except documented exceptions)
+- [x] Every directory has barrel export
+- [x] No circular dependencies
+- [x] TypeScript strict mode passing
+- [x] 90%+ test coverage
+- [x] Clear domain boundaries
+- [x] Consistent naming conventions
---
diff --git a/docs/system-architecture.md b/docs/system-architecture.md
index bec06777..632efb6d 100644
--- a/docs/system-architecture.md
+++ b/docs/system-architecture.md
@@ -1,6 +1,6 @@
# CCS System Architecture
-Last Updated: 2025-12-19
+Last Updated: 2025-12-21
High-level architecture documentation for the CCS (Claude Code Switch) system.
@@ -166,14 +166,14 @@ CCS is a CLI wrapper that enables seamless switching between multiple Claude acc
+===========================================================================+
+------------------+
- | pages/ | Route-level components
+ | pages/ | Route-level components (modular directories)
|------------------|
- | analytics.tsx |
+ | analytics/ | 8 files - usage charts, cost tracking
+ | settings/ | 20 files - lazy-loaded tab sections
| api.tsx |
| cliproxy.tsx |
| copilot.tsx |
| health.tsx |
- | settings.tsx |
+------------------+
|
v
@@ -186,7 +186,7 @@ CCS is a CLI wrapper that enables seamless switching between multiple Claude acc
| copilot/ | +------------------+ | use-profiles |
| health/ | | use-websocket |
| layout/ | +------------------+
- | monitoring/ |
+ | monitoring/ | <-- auth-monitor/ (8 files), error-logs/ (6 files)
| profiles/ |
| setup/ |
| shared/ |
diff --git a/package.json b/package.json
index 3e8495d4..518a0bab 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@kaitranntt/ccs",
- "version": "7.1.0",
+ "version": "7.1.0-dev.3",
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
"keywords": [
"cli",
diff --git a/ui/bun.lock b/ui/bun.lock
index 8b86d52f..2756da98 100644
--- a/ui/bun.lock
+++ b/ui/bun.lock
@@ -46,25 +46,42 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.1.17",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.1",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@types/recharts": "^1.8.29",
"@vitejs/plugin-react": "^5.1.1",
+ "@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "jsdom": "^27.3.0",
+ "msw": "^2.12.4",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4",
+ "vitest": "^4.0.16",
},
},
},
"packages": {
+ "@acemir/cssom": ["@acemir/cssom@0.9.29", "", {}, "sha512-G90x0VW+9nW4dFajtjCoT+NM0scAfH9Mb08IcjgFHYbfiL/lU04dTF9JuVOi3/OH+DJCQdcIseSXkdCB9Ky6JA=="],
+
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
+
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.7.6", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.4" } }, "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg=="],
+
+ "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
@@ -105,6 +122,20 @@
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
+ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+
"@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
@@ -195,6 +226,16 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+ "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
+
+ "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
+
+ "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
+
+ "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
+
+ "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -205,6 +246,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="],
+
"@nivo/colors": ["@nivo/colors@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@types/d3-color": "^3.0.0", "@types/d3-scale": "^4.0.8", "@types/d3-scale-chromatic": "^3.0.0", "d3-color": "^3.1.0", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "lodash": "^4.17.21" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-hyYt4lEFIfXOUmQ6k3HXm3KwhcgoJpocmoGzLUqzk7DzuhQYJo+4d5jIGGU0N/a70+9XbHIdpKNSblHAIASD3w=="],
"@nivo/core": ["@nivo/core@0.99.0", "", { "dependencies": { "@nivo/theming": "0.99.0", "@nivo/tooltip": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0", "@types/d3-shape": "^3.1.6", "d3-color": "^3.1.0", "d3-format": "^1.4.4", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-scale-chromatic": "^3.0.0", "d3-shape": "^3.2.0", "d3-time-format": "^3.0.0", "lodash": "^4.17.21", "react-virtualized-auto-sizer": "^1.0.26", "use-debounce": "^10.0.4" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-olCItqhPG3xHL5ei+vg52aB6o+6S+xR2idpkd9RormTTUniZb8U2rOdcQojOojPY5i9kVeQyLFBpV4YfM7OZ9g=="],
@@ -219,6 +262,12 @@
"@nivo/tooltip": ["@nivo/tooltip@0.99.0", "", { "dependencies": { "@nivo/core": "0.99.0", "@nivo/theming": "0.99.0", "@react-spring/web": "9.4.5 || ^9.7.2 || ^10.0" }, "peerDependencies": { "react": "^16.14 || ^17.0 || ^18.0 || ^19.0" } }, "sha512-weoEGR3xAetV4k2P6k96cdamGzKQ5F2Pq+uyDaHr1P3HYArM879Pl+x+TkU0aWjP6wgUZPx/GOBiV1Hb1JxIqg=="],
+ "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
+
+ "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="],
+
+ "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
+
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
@@ -359,6 +408,8 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="],
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
@@ -399,6 +450,16 @@
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.1", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
@@ -407,6 +468,8 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
@@ -429,6 +492,8 @@
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
@@ -443,6 +508,8 @@
"@types/recharts": ["@types/recharts@1.8.29", "", { "dependencies": { "@types/d3-shape": "^1", "@types/react": "*" } }, "sha512-ulKklaVsnFIIhTQsQw226TnOibrddW1qUQNFVhoQEyY1Z7FRQrNecFCGt7msRuJseudzE9czVawZb17dK/aPXw=="],
+ "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
+
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.48.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/type-utils": "8.48.1", "@typescript-eslint/utils": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.48.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA=="],
@@ -465,22 +532,50 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.1", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA=="],
+ "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.16", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.16", "ast-v8-to-istanbul": "^0.3.8", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.16", "vitest": "4.0.16" }, "optionalPeers": ["@vitest/browser"] }, "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A=="],
+
+ "@vitest/expect": ["@vitest/expect@4.0.16", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.0.16", "", { "dependencies": { "@vitest/spy": "4.0.16", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.0.16", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA=="],
+
+ "@vitest/runner": ["@vitest/runner@4.0.16", "", { "dependencies": { "@vitest/utils": "4.0.16", "pathe": "^2.0.3" } }, "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA=="],
+
+ "@vitest/spy": ["@vitest/spy@4.0.16", "", {}, "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.0.16", "", { "dependencies": { "@vitest/pretty-format": "4.0.16", "tinyrainbow": "^3.0.3" } }, "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA=="],
+
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.9", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-dSC6tJeOJxbZrPzPbv5mMd6CMiQ1ugaVXXPRad2fXUSsy1kstFn9XQWemV9VW7Y7kpxgQ/4WMoZfwdH8XSU48w=="],
+
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.4", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA=="],
+ "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"browserslist": ["browserslist@4.28.1", "", { "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" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
@@ -489,12 +584,18 @@
"caniuse-lite": ["caniuse-lite@1.0.30001759", "", {}, "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw=="],
+ "chai": ["chai@6.2.1", "", {}, "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg=="],
+
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
+
+ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -509,6 +610,12 @@
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+ "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
+
+ "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
+
+ "cssstyle": ["cssstyle@5.3.5", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0" } }, "sha512-GlsEptulso7Jg0VaOZ8BXQi3AkYM5BOJKEO/rjMidSCq70FkIC5y0eawrCXeYzxgt3OCf4Ls+eoxN+/05vN0Ag=="],
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
@@ -537,26 +644,40 @@
"d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
+ "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
+
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
"decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.266", "", {}, "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg=="],
+ "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
"esbuild": ["esbuild@0.25.12", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
@@ -583,10 +704,14 @@
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-equals": ["fast-equals@5.3.3", "", {}, "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw=="],
@@ -609,6 +734,8 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
@@ -619,32 +746,64 @@
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
+ "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="],
+
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+ "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
+
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+ "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
+
+ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+ "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+ "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
+
+ "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
+
+ "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="],
+
+ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
+
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
- "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+ "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+ "jsdom": ["jsdom@27.3.0", "", { "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", "cssstyle": "^5.3.4", "data-urls": "^6.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.1.0", "ws": "^8.18.3", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg=="],
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -691,16 +850,30 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
- "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+ "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"lucide-react": ["lucide-react@0.556.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A=="],
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+ "magicast": ["magicast@0.5.1", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "source-map-js": "^1.2.1" } }, "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw=="],
+
+ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
+
+ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+ "msw": ["msw@2.12.4", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-rHNiVfTyKhzc0EjoXUBVGteNKBevdjOlVC6GlIRXpy+/3LHEIGRovnB5WPjcvmNODVQ1TNFnoa7wsGbd0V3epg=="],
+
+ "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
+
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -709,18 +882,28 @@
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+ "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
+
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+ "parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="],
+
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@@ -731,6 +914,8 @@
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
"prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
@@ -777,10 +962,22 @@
"recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="],
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
+ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+ "rettime": ["rettime@0.7.0", "", {}, "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw=="],
+
"rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="],
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -791,14 +988,36 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
+
+ "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
+
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
"tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
@@ -807,20 +1026,38 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
+
+ "tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="],
+
+ "tldts-core": ["tldts-core@7.0.19", "", {}, "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A=="],
+
+ "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
+
+ "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
+
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+ "type-fest": ["type-fest@5.3.1", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg=="],
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.48.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.48.1", "@typescript-eslint/parser": "8.48.1", "@typescript-eslint/typescript-estree": "8.48.1", "@typescript-eslint/utils": "8.48.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+ "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
+
"update-browserslist-db": ["update-browserslist-db@1.2.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
@@ -835,20 +1072,54 @@
"vite": ["vite@7.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ=="],
+ "vitest": ["vitest@4.0.16", "", { "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", "@vitest/pretty-format": "4.0.16", "@vitest/runner": "4.0.16", "@vitest/snapshot": "4.0.16", "@vitest/spy": "4.0.16", "@vitest/utils": "4.0.16", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.16", "@vitest/browser-preview": "4.0.16", "@vitest/browser-webdriverio": "4.0.16", "@vitest/ui": "4.0.16", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@8.0.0", "", {}, "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA=="],
+
+ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
+ "whatwg-url": ["whatwg-url@15.1.0", "", { "dependencies": { "tr46": "^6.0.0", "webidl-conversions": "^8.0.0" } }, "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g=="],
+
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+ "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
+
+ "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
+
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
+ "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
+ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
+
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+ "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
+
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
@@ -889,12 +1160,18 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
"d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
"d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
@@ -905,6 +1182,14 @@
"d3-time-format/d3-time": ["d3-time@2.1.1", "", { "dependencies": { "d3-array": "2" } }, "sha512-/eIQe/eR4kCQwq7yxi7z4c6qEXf2IYGcjoWB5OOQy4Tq9Uv39/947qlDcN2TLkiTzQWzvnsuYPB9TrWaNfipKQ=="],
+ "loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+ "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
"prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"victory-vendor/@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="],
diff --git a/ui/package.json b/ui/package.json
index a6488087..5fbb4629 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -9,10 +9,14 @@
"typecheck": "tsc -b --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
- "format": "prettier --write src/",
- "format:check": "prettier --check src/",
+ "format": "prettier --write src/ tests/",
+ "format:check": "prettier --check src/ tests/",
"validate": "bun run typecheck && bun run lint:fix && bun run format:check",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:run": "vitest run",
+ "test:coverage": "vitest run --coverage",
+ "test:ui": "vitest --ui"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
@@ -57,20 +61,27 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.1.17",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.1",
+ "@testing-library/user-event": "^14.6.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@types/recharts": "^1.8.29",
"@vitejs/plugin-react": "^5.1.1",
+ "@vitest/coverage-v8": "^4.0.16",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
+ "jsdom": "^27.3.0",
+ "msw": "^2.12.4",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
- "vite": "^7.2.4"
+ "vite": "^7.2.4",
+ "vitest": "^4.0.16"
}
}
diff --git a/ui/src/components/monitoring/auth-monitor.tsx b/ui/src/components/monitoring/auth-monitor.tsx
deleted file mode 100644
index 61cbaf8b..00000000
--- a/ui/src/components/monitoring/auth-monitor.tsx
+++ /dev/null
@@ -1,465 +0,0 @@
-/**
- * Auth Monitor Component with Account Flow Visualization
- * Shows request flow from accounts to providers using custom SVG bezier curves
- * Uses glass panel aesthetic with hover interactions and glow effects
- */
-
-import { useState, useMemo, useEffect } from 'react';
-import { useCliproxyAuth } from '@/hooks/use-cliproxy';
-import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
-import { cn, STATUS_COLORS } from '@/lib/utils';
-import { getProviderDisplayName, PROVIDER_COLORS } from '@/lib/provider-config';
-import { Skeleton } from '@/components/ui/skeleton';
-import { ProviderIcon } from '@/components/shared/provider-icon';
-import { AccountFlowViz } from '@/components/account-flow-viz';
-import { usePrivacy } from '@/contexts/privacy-context';
-import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
-import { Activity, CheckCircle2, XCircle, ChevronRight, Radio } from 'lucide-react';
-
-interface AccountRow {
- id: string;
- email: string;
- provider: string;
- displayName: string;
- isDefault: boolean;
- successCount: number;
- failureCount: number;
- lastUsedAt?: string;
- color: string;
-}
-
-interface ProviderStats {
- provider: string;
- displayName: string;
- totalRequests: number;
- successCount: number;
- failureCount: number;
- accountCount: number;
- accounts: AccountRow[];
-}
-
-function getSuccessRate(success: number, failure: number): number {
- const total = success + failure;
- if (total === 0) return 100;
- return Math.round((success / total) * 100);
-}
-
-/** Strip common email domains for cleaner display */
-function cleanEmail(email: string): string {
- return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, '');
-}
-
-// Vibrant colors for account segments - darker for light theme contrast
-const ACCOUNT_COLORS = [
- '#1e6091', // Deep Cerulean (was #277da1)
- '#2d8a6e', // Deep Seaweed (was #43aa8b)
- '#d4a012', // Dark Tuscan (was #f9c74f)
- '#c92a2d', // Deep Strawberry (was #f94144)
- '#c45a1a', // Deep Pumpkin (was #f3722c)
- '#6b9c4d', // Dark Willow (was #90be6d)
- '#3d5a73', // Deep Blue Slate (was #577590)
- '#cc7614', // Dark Carrot (was #f8961e)
- '#3a7371', // Deep Cyan (was #4d908e)
- '#7c5fc4', // Deep Purple (was #a78bfa)
-];
-
-/** Enhanced live pulse indicator with multi-ring animation */
-function LivePulse() {
- return (
-
- {/* Outer ping ring */}
-
- {/* Middle pulse ring */}
-
- {/* Inner solid dot */}
-
-
- );
-}
-
-/** Inline success/failure badge for provider cards */
-function InlineStatsBadge({ success, failure }: { success: number; failure: number }) {
- if (success === 0 && failure === 0) {
- return no activity;
- }
-
- return (
-
-
-
-
- {success.toLocaleString()}
-
-
- {failure > 0 && (
-
-
-
- {failure.toLocaleString()}
-
-
- )}
-
- );
-}
-
-export function AuthMonitor() {
- const { data, isLoading, error } = useCliproxyAuth();
- const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
- const { privacyMode } = usePrivacy();
- const [selectedProvider, setSelectedProvider] = useState(null);
- const [hoveredProvider, setHoveredProvider] = useState(null);
- const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
-
- // Live countdown showing time since last data update
- useEffect(() => {
- if (!dataUpdatedAt) return;
- const updateTime = () => {
- const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000);
- if (diff < 60) {
- setTimeSinceUpdate(`${diff}s ago`);
- } else {
- setTimeSinceUpdate(`${Math.floor(diff / 60)}m ago`);
- }
- };
- updateTime();
- const interval = setInterval(updateTime, 1000);
- return () => clearInterval(interval);
- }, [dataUpdatedAt]);
-
- // Build a map of account email -> usage stats from CLIProxy
- const accountStatsMap = useMemo(() => {
- if (!statsData?.accountStats) return new Map();
- return new Map(Object.entries(statsData.accountStats));
- }, [statsData?.accountStats]);
-
- // Transform auth status data into account rows
- const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
- if (!data?.authStatus) {
- return {
- accounts: [],
- totalSuccess: 0,
- totalFailure: 0,
- totalRequests: 0,
- providerStats: [],
- };
- }
-
- const accountsList: AccountRow[] = [];
- const providerMap = new Map<
- string,
- { success: number; failure: number; accounts: AccountRow[] }
- >();
- let tSuccess = 0;
- let tFailure = 0;
- let colorIndex = 0;
-
- data.authStatus.forEach((status: AuthStatus) => {
- const providerKey = status.provider;
- if (!providerMap.has(providerKey)) {
- providerMap.set(providerKey, { success: 0, failure: 0, accounts: [] });
- }
- const providerData = providerMap.get(providerKey);
- if (!providerData) return;
-
- status.accounts?.forEach((account: OAuthAccount) => {
- // Get real stats from CLIProxy - try email first, then id
- const accountEmail = account.email || account.id;
- const realStats = accountStatsMap.get(accountEmail);
- const success = realStats?.successCount ?? 0;
- const failure = realStats?.failureCount ?? 0;
- tSuccess += success;
- tFailure += failure;
- providerData.success += success;
- providerData.failure += failure;
-
- const row: AccountRow = {
- id: account.id,
- email: account.email || account.id,
- provider: status.provider,
- displayName: status.displayName,
- isDefault: account.isDefault,
- successCount: success,
- failureCount: failure,
- lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt,
- color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
- };
- accountsList.push(row);
- providerData.accounts.push(row);
- colorIndex++;
- });
- });
-
- // Build provider stats array
- const providerStatsArr: ProviderStats[] = [];
- providerMap.forEach((pData, provider) => {
- if (pData.accounts.length === 0) return;
- providerStatsArr.push({
- provider,
- displayName: getProviderDisplayName(provider),
- totalRequests: pData.success + pData.failure,
- successCount: pData.success,
- failureCount: pData.failure,
- accountCount: pData.accounts.length,
- accounts: pData.accounts,
- });
- });
- providerStatsArr.sort((a, b) => b.totalRequests - a.totalRequests);
-
- return {
- accounts: accountsList,
- totalSuccess: tSuccess,
- totalFailure: tFailure,
- totalRequests: tSuccess + tFailure,
- providerStats: providerStatsArr,
- };
- }, [data?.authStatus, accountStatsMap]);
-
- const overallSuccessRate =
- totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
-
- // Get selected provider data for detail view
- const selectedProviderData = selectedProvider
- ? providerStats.find((ps) => ps.provider === selectedProvider)
- : null;
-
- if (isLoading || statsLoading) {
- return (
-
-
-
-
-
-
-
- {[1, 2, 3, 4].map((i) => (
-
- ))}
-
-
-
-
- );
- }
-
- if (error || !data?.authStatus || accounts.length === 0) {
- return null;
- }
-
- return (
-
- {/* Enhanced Live Header with gradient glow */}
-
-
-
- LIVE
- Account Monitor
-
-
-
-
- Updated {timeSinceUpdate || 'now'}
-
-
|
-
{accounts.length} accounts
-
{totalRequests.toLocaleString()} req
-
-
-
- {/* Summary Stats Row */}
-
- }
- label="Accounts"
- value={accounts.length}
- color="var(--accent)"
- />
- }
- label="Success"
- value={totalSuccess.toLocaleString()}
- color={STATUS_COLORS.success}
- />
- }
- label="Failed"
- value={totalFailure.toLocaleString()}
- color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
- />
- }
- label="Success Rate"
- value={`${overallSuccessRate}%`}
- color={
- overallSuccessRate === 100
- ? STATUS_COLORS.success
- : overallSuccessRate >= 95
- ? STATUS_COLORS.degraded
- : STATUS_COLORS.failed
- }
- />
-
-
- {/* Flow Visualization */}
-
- {selectedProviderData ? (
- // Account-level flow view
-
setSelectedProvider(null)}
- />
- ) : (
- // Provider cards view
-
-
- Request Distribution by Provider
-
-
- {providerStats.map((ps) => {
- const successRate = getSuccessRate(ps.successCount, ps.failureCount);
- const providerColor = PROVIDER_COLORS[ps.provider.toLowerCase()] || '#6b7280';
- const isHovered = hoveredProvider === ps.provider;
-
- return (
-
- );
- })}
-
-
- )}
-
-
- );
-}
-
-// Summary Card Component
-function SummaryCard({
- icon,
- label,
- value,
- color,
-}: {
- icon: React.ReactNode;
- label: string;
- value: string | number;
- color?: string;
-}) {
- return (
-
-
- {icon}
-
-
-
{label}
-
- {value}
-
-
-
- );
-}
diff --git a/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx
new file mode 100644
index 00000000..bd60db8f
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/components/inline-stats-badge.tsx
@@ -0,0 +1,35 @@
+/**
+ * InlineStatsBadge - Inline success/failure badge for provider cards
+ */
+
+import { CheckCircle2, XCircle } from 'lucide-react';
+
+interface InlineStatsBadgeProps {
+ success: number;
+ failure: number;
+}
+
+export function InlineStatsBadge({ success, failure }: InlineStatsBadgeProps) {
+ if (success === 0 && failure === 0) {
+ return no activity;
+ }
+
+ return (
+
+
+
+
+ {success.toLocaleString()}
+
+
+ {failure > 0 && (
+
+
+
+ {failure.toLocaleString()}
+
+
+ )}
+
+ );
+}
diff --git a/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx b/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx
new file mode 100644
index 00000000..adc006ad
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/components/live-pulse.tsx
@@ -0,0 +1,27 @@
+/**
+ * LivePulse - Enhanced live pulse indicator with multi-ring animation
+ */
+
+import { STATUS_COLORS } from '@/lib/utils';
+
+export function LivePulse() {
+ return (
+
+ {/* Outer ping ring */}
+
+ {/* Middle pulse ring */}
+
+ {/* Inner solid dot */}
+
+
+ );
+}
diff --git a/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx
new file mode 100644
index 00000000..d7faf5f9
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/components/provider-card.tsx
@@ -0,0 +1,123 @@
+/**
+ * ProviderCard - Provider status card with account color dots and stats
+ */
+
+import type React from 'react';
+import { ChevronRight } from 'lucide-react';
+import { cn, STATUS_COLORS } from '@/lib/utils';
+import { PROVIDER_COLORS } from '@/lib/provider-config';
+import { ProviderIcon } from '@/components/shared/provider-icon';
+import type { ProviderStats } from '../types';
+import { getSuccessRate, cleanEmail } from '../utils';
+import { InlineStatsBadge } from './inline-stats-badge';
+
+interface ProviderCardProps {
+ stats: ProviderStats;
+ isHovered: boolean;
+ privacyMode: boolean;
+ onSelect: () => void;
+ onMouseEnter: () => void;
+ onMouseLeave: () => void;
+}
+
+export function ProviderCard({
+ stats,
+ isHovered,
+ privacyMode,
+ onSelect,
+ onMouseEnter,
+ onMouseLeave,
+}: ProviderCardProps) {
+ const successRate = getSuccessRate(stats.successCount, stats.failureCount);
+ const providerColor = PROVIDER_COLORS[stats.provider.toLowerCase()] || '#6b7280';
+
+ return (
+
+ );
+}
diff --git a/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx b/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx
new file mode 100644
index 00000000..380d5d03
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/components/summary-card.tsx
@@ -0,0 +1,37 @@
+/**
+ * SummaryCard - Summary stats card with icon and value display
+ */
+
+import type React from 'react';
+
+interface SummaryCardProps {
+ icon: React.ReactNode;
+ label: string;
+ value: string | number;
+ color?: string;
+}
+
+export function SummaryCard({ icon, label, value, color }: SummaryCardProps) {
+ return (
+
+
+ {icon}
+
+
+
{label}
+
+ {value}
+
+
+
+ );
+}
diff --git a/ui/src/components/monitoring/auth-monitor/hooks.ts b/ui/src/components/monitoring/auth-monitor/hooks.ts
new file mode 100644
index 00000000..1640f037
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/hooks.ts
@@ -0,0 +1,148 @@
+/**
+ * Hooks for Auth Monitor data aggregation and state management
+ */
+
+import { useState, useMemo, useEffect } from 'react';
+import { useCliproxyAuth } from '@/hooks/use-cliproxy';
+import { useCliproxyStats, type AccountUsageStats } from '@/hooks/use-cliproxy-stats';
+import { getProviderDisplayName } from '@/lib/provider-config';
+import type { AuthStatus, OAuthAccount } from '@/lib/api-client';
+import type { AccountRow, ProviderStats } from './types';
+import { ACCOUNT_COLORS } from './utils';
+
+export interface AuthMonitorData {
+ accounts: AccountRow[];
+ totalSuccess: number;
+ totalFailure: number;
+ totalRequests: number;
+ providerStats: ProviderStats[];
+ overallSuccessRate: number;
+ isLoading: boolean;
+ error: Error | null;
+ timeSinceUpdate: string;
+}
+
+/** Hook for computing auth monitor data from CLIProxy auth and stats */
+export function useAuthMonitorData(): AuthMonitorData {
+ const { data, isLoading, error } = useCliproxyAuth();
+ const { data: statsData, isLoading: statsLoading, dataUpdatedAt } = useCliproxyStats();
+ const [timeSinceUpdate, setTimeSinceUpdate] = useState('');
+
+ // Live countdown showing time since last data update
+ useEffect(() => {
+ if (!dataUpdatedAt) return;
+ const updateTime = () => {
+ const diff = Math.floor((Date.now() - dataUpdatedAt) / 1000);
+ if (diff < 60) {
+ setTimeSinceUpdate(`${diff}s ago`);
+ } else {
+ setTimeSinceUpdate(`${Math.floor(diff / 60)}m ago`);
+ }
+ };
+ updateTime();
+ const interval = setInterval(updateTime, 1000);
+ return () => clearInterval(interval);
+ }, [dataUpdatedAt]);
+
+ // Build a map of account email -> usage stats from CLIProxy
+ const accountStatsMap = useMemo(() => {
+ if (!statsData?.accountStats) return new Map();
+ return new Map(Object.entries(statsData.accountStats));
+ }, [statsData?.accountStats]);
+
+ // Transform auth status data into account rows
+ const { accounts, totalSuccess, totalFailure, totalRequests, providerStats } = useMemo(() => {
+ if (!data?.authStatus) {
+ return {
+ accounts: [] as AccountRow[],
+ totalSuccess: 0,
+ totalFailure: 0,
+ totalRequests: 0,
+ providerStats: [] as ProviderStats[],
+ };
+ }
+
+ const accountsList: AccountRow[] = [];
+ const providerMap = new Map<
+ string,
+ { success: number; failure: number; accounts: AccountRow[] }
+ >();
+ let tSuccess = 0;
+ let tFailure = 0;
+ let colorIndex = 0;
+
+ data.authStatus.forEach((status: AuthStatus) => {
+ const providerKey = status.provider;
+ if (!providerMap.has(providerKey)) {
+ providerMap.set(providerKey, { success: 0, failure: 0, accounts: [] });
+ }
+ const providerData = providerMap.get(providerKey);
+ if (!providerData) return;
+
+ status.accounts?.forEach((account: OAuthAccount) => {
+ const accountEmail = account.email || account.id;
+ const realStats = accountStatsMap.get(accountEmail);
+ const success = realStats?.successCount ?? 0;
+ const failure = realStats?.failureCount ?? 0;
+ tSuccess += success;
+ tFailure += failure;
+ providerData.success += success;
+ providerData.failure += failure;
+
+ const row: AccountRow = {
+ id: account.id,
+ email: account.email || account.id,
+ provider: status.provider,
+ displayName: status.displayName,
+ isDefault: account.isDefault,
+ successCount: success,
+ failureCount: failure,
+ lastUsedAt: realStats?.lastUsedAt ?? account.lastUsedAt,
+ color: ACCOUNT_COLORS[colorIndex % ACCOUNT_COLORS.length],
+ };
+ accountsList.push(row);
+ providerData.accounts.push(row);
+ colorIndex++;
+ });
+ });
+
+ // Build provider stats array
+ const providerStatsArr: ProviderStats[] = [];
+ providerMap.forEach((pData, provider) => {
+ if (pData.accounts.length === 0) return;
+ providerStatsArr.push({
+ provider,
+ displayName: getProviderDisplayName(provider),
+ totalRequests: pData.success + pData.failure,
+ successCount: pData.success,
+ failureCount: pData.failure,
+ accountCount: pData.accounts.length,
+ accounts: pData.accounts,
+ });
+ });
+ providerStatsArr.sort((a, b) => b.totalRequests - a.totalRequests);
+
+ return {
+ accounts: accountsList,
+ totalSuccess: tSuccess,
+ totalFailure: tFailure,
+ totalRequests: tSuccess + tFailure,
+ providerStats: providerStatsArr,
+ };
+ }, [data?.authStatus, accountStatsMap]);
+
+ const overallSuccessRate =
+ totalRequests > 0 ? Math.round((totalSuccess / totalRequests) * 100) : 100;
+
+ return {
+ accounts,
+ totalSuccess,
+ totalFailure,
+ totalRequests,
+ providerStats,
+ overallSuccessRate,
+ isLoading: isLoading || statsLoading,
+ error: error ?? null,
+ timeSinceUpdate,
+ };
+}
diff --git a/ui/src/components/monitoring/auth-monitor/index.tsx b/ui/src/components/monitoring/auth-monitor/index.tsx
new file mode 100644
index 00000000..d43e1d48
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/index.tsx
@@ -0,0 +1,151 @@
+/**
+ * Auth Monitor Component with Account Flow Visualization
+ * Shows request flow from accounts to providers using custom SVG bezier curves
+ * Uses glass panel aesthetic with hover interactions and glow effects
+ */
+
+import { useState } from 'react';
+import { STATUS_COLORS } from '@/lib/utils';
+import { Skeleton } from '@/components/ui/skeleton';
+import { AccountFlowViz } from '@/components/account-flow-viz';
+import { usePrivacy } from '@/contexts/privacy-context';
+import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react';
+
+import { useAuthMonitorData } from './hooks';
+import { LivePulse } from './components/live-pulse';
+import { ProviderCard } from './components/provider-card';
+import { SummaryCard } from './components/summary-card';
+
+export function AuthMonitor() {
+ const {
+ accounts,
+ totalSuccess,
+ totalFailure,
+ totalRequests,
+ providerStats,
+ overallSuccessRate,
+ isLoading,
+ error,
+ timeSinceUpdate,
+ } = useAuthMonitorData();
+
+ const { privacyMode } = usePrivacy();
+ const [selectedProvider, setSelectedProvider] = useState(null);
+ const [hoveredProvider, setHoveredProvider] = useState(null);
+
+ // Get selected provider data for detail view
+ const selectedProviderData = selectedProvider
+ ? providerStats.find((ps) => ps.provider === selectedProvider)
+ : null;
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+
+ );
+ }
+
+ if (error || accounts.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {/* Enhanced Live Header with gradient glow */}
+
+
+
+ LIVE
+ Account Monitor
+
+
+
+
+ Updated {timeSinceUpdate || 'now'}
+
+
|
+
{accounts.length} accounts
+
{totalRequests.toLocaleString()} req
+
+
+
+ {/* Summary Stats Row */}
+
+ }
+ label="Accounts"
+ value={accounts.length}
+ color="var(--accent)"
+ />
+ }
+ label="Success"
+ value={totalSuccess.toLocaleString()}
+ color={STATUS_COLORS.success}
+ />
+ }
+ label="Failed"
+ value={totalFailure.toLocaleString()}
+ color={totalFailure > 0 ? STATUS_COLORS.failed : undefined}
+ />
+ }
+ label="Success Rate"
+ value={`${overallSuccessRate}%`}
+ color={
+ overallSuccessRate === 100
+ ? STATUS_COLORS.success
+ : overallSuccessRate >= 95
+ ? STATUS_COLORS.degraded
+ : STATUS_COLORS.failed
+ }
+ />
+
+
+ {/* Flow Visualization */}
+
+ {selectedProviderData ? (
+
setSelectedProvider(null)}
+ />
+ ) : (
+
+
+ Request Distribution by Provider
+
+
+ {providerStats.map((ps) => (
+
setSelectedProvider(ps.provider)}
+ onMouseEnter={() => setHoveredProvider(ps.provider)}
+ onMouseLeave={() => setHoveredProvider(null)}
+ />
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+// Re-export types for barrel
+export type { AccountRow, ProviderStats } from './types';
diff --git a/ui/src/components/monitoring/auth-monitor/types.ts b/ui/src/components/monitoring/auth-monitor/types.ts
new file mode 100644
index 00000000..1eb61953
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/types.ts
@@ -0,0 +1,25 @@
+/**
+ * Type definitions for Auth Monitor components
+ */
+
+export interface AccountRow {
+ id: string;
+ email: string;
+ provider: string;
+ displayName: string;
+ isDefault: boolean;
+ successCount: number;
+ failureCount: number;
+ lastUsedAt?: string;
+ color: string;
+}
+
+export interface ProviderStats {
+ provider: string;
+ displayName: string;
+ totalRequests: number;
+ successCount: number;
+ failureCount: number;
+ accountCount: number;
+ accounts: AccountRow[];
+}
diff --git a/ui/src/components/monitoring/auth-monitor/utils.ts b/ui/src/components/monitoring/auth-monitor/utils.ts
new file mode 100644
index 00000000..28272ac7
--- /dev/null
+++ b/ui/src/components/monitoring/auth-monitor/utils.ts
@@ -0,0 +1,29 @@
+/**
+ * Utility functions and constants for Auth Monitor
+ */
+
+/** Calculate success rate percentage */
+export function getSuccessRate(success: number, failure: number): number {
+ const total = success + failure;
+ if (total === 0) return 100;
+ return Math.round((success / total) * 100);
+}
+
+/** Strip common email domains for cleaner display */
+export function cleanEmail(email: string): string {
+ return email.replace(/@(gmail|yahoo|hotmail|outlook|icloud)\.com$/i, '');
+}
+
+// Vibrant colors for account segments - darker for light theme contrast
+export const ACCOUNT_COLORS = [
+ '#1e6091', // Deep Cerulean
+ '#2d8a6e', // Deep Seaweed
+ '#d4a012', // Dark Tuscan
+ '#c92a2d', // Deep Strawberry
+ '#c45a1a', // Deep Pumpkin
+ '#6b9c4d', // Dark Willow
+ '#3d5a73', // Deep Blue Slate
+ '#cc7614', // Dark Carrot
+ '#3a7371', // Deep Cyan
+ '#7c5fc4', // Deep Purple
+];
diff --git a/ui/src/components/monitoring/index.ts b/ui/src/components/monitoring/index.ts
index 3d66966d..4f9ac659 100644
--- a/ui/src/components/monitoring/index.ts
+++ b/ui/src/components/monitoring/index.ts
@@ -4,6 +4,7 @@
// Main monitoring components
export { AuthMonitor } from './auth-monitor';
+export type { AccountRow, ProviderStats } from './auth-monitor';
export { ProxyStatusWidget } from './proxy-status-widget';
// Error logs (from subdirectory)
diff --git a/ui/src/contexts/privacy-context.tsx b/ui/src/contexts/privacy-context.tsx
index 98cc98fb..3cf6cbcb 100644
--- a/ui/src/contexts/privacy-context.tsx
+++ b/ui/src/contexts/privacy-context.tsx
@@ -5,7 +5,15 @@
*/
/* eslint-disable react-refresh/only-export-components */
-import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
+import {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ useMemo,
+ useCallback,
+ type ReactNode,
+} from 'react';
interface PrivacyContextValue {
/** Whether privacy/demo mode is enabled */
@@ -32,13 +40,14 @@ export function PrivacyProvider({ children }: { children: ReactNode }) {
localStorage.setItem(STORAGE_KEY, String(privacyMode));
}, [privacyMode]);
- const togglePrivacyMode = () => setPrivacyMode((prev) => !prev);
+ const togglePrivacyMode = useCallback(() => setPrivacyMode((prev) => !prev), []);
- return (
-
- {children}
-
+ const value = useMemo(
+ () => ({ privacyMode, togglePrivacyMode }),
+ [privacyMode, togglePrivacyMode]
);
+
+ return {children};
}
export function usePrivacy() {
diff --git a/ui/src/hooks/use-cliproxy-auth-flow.ts b/ui/src/hooks/use-cliproxy-auth-flow.ts
index 103d98e0..e896c3f3 100644
--- a/ui/src/hooks/use-cliproxy-auth-flow.ts
+++ b/ui/src/hooks/use-cliproxy-auth-flow.ts
@@ -3,7 +3,7 @@
* Manages popup-based OAuth authentication flows
*/
-import { useState, useCallback, useRef, useEffect } from 'react';
+import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -180,9 +180,12 @@ export function useCliproxyAuthFlow() {
setState({ provider: null, isAuthenticating: false, error: null });
}, [cleanup]);
- return {
- ...state,
- startAuth,
- cancelAuth,
- };
+ return useMemo(
+ () => ({
+ ...state,
+ startAuth,
+ cancelAuth,
+ }),
+ [state, startAuth, cancelAuth]
+ );
}
diff --git a/ui/src/hooks/use-copilot.ts b/ui/src/hooks/use-copilot.ts
index 3431ffdb..2aed765e 100644
--- a/ui/src/hooks/use-copilot.ts
+++ b/ui/src/hooks/use-copilot.ts
@@ -4,6 +4,7 @@
* React hook for managing GitHub Copilot integration state.
*/
+import { useMemo } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const API_BASE = '/api';
@@ -247,54 +248,89 @@ export function useCopilot() {
},
});
- return {
- // Status
- status: statusQuery.data,
- statusLoading: statusQuery.isLoading,
- statusError: statusQuery.error,
- refetchStatus: statusQuery.refetch,
+ return useMemo(
+ () => ({
+ // Status
+ status: statusQuery.data,
+ statusLoading: statusQuery.isLoading,
+ statusError: statusQuery.error,
+ refetchStatus: statusQuery.refetch,
- // Config
- config: configQuery.data,
- configLoading: configQuery.isLoading,
+ // Config
+ config: configQuery.data,
+ configLoading: configQuery.isLoading,
- // Models
- models: modelsQuery.data?.models ?? [],
- currentModel: modelsQuery.data?.current,
- modelsLoading: modelsQuery.isLoading,
+ // Models
+ models: modelsQuery.data?.models ?? [],
+ currentModel: modelsQuery.data?.current,
+ modelsLoading: modelsQuery.isLoading,
- // Raw Settings
- rawSettings: rawSettingsQuery.data,
- rawSettingsLoading: rawSettingsQuery.isLoading,
- refetchRawSettings: rawSettingsQuery.refetch,
+ // Raw Settings
+ rawSettings: rawSettingsQuery.data,
+ rawSettingsLoading: rawSettingsQuery.isLoading,
+ refetchRawSettings: rawSettingsQuery.refetch,
- // Mutations
- updateConfig: updateConfigMutation.mutate,
- updateConfigAsync: updateConfigMutation.mutateAsync,
- isUpdating: updateConfigMutation.isPending,
+ // Mutations
+ updateConfig: updateConfigMutation.mutate,
+ updateConfigAsync: updateConfigMutation.mutateAsync,
+ isUpdating: updateConfigMutation.isPending,
- saveRawSettings: saveRawSettingsMutation.mutate,
- saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
- isSavingRawSettings: saveRawSettingsMutation.isPending,
+ saveRawSettings: saveRawSettingsMutation.mutate,
+ saveRawSettingsAsync: saveRawSettingsMutation.mutateAsync,
+ isSavingRawSettings: saveRawSettingsMutation.isPending,
- startAuth: startAuthMutation.mutate,
- startAuthAsync: startAuthMutation.mutateAsync,
- isAuthenticating: startAuthMutation.isPending,
- authResult: startAuthMutation.data,
+ startAuth: startAuthMutation.mutate,
+ startAuthAsync: startAuthMutation.mutateAsync,
+ isAuthenticating: startAuthMutation.isPending,
+ authResult: startAuthMutation.data,
- startDaemon: startDaemonMutation.mutate,
- isStartingDaemon: startDaemonMutation.isPending,
+ startDaemon: startDaemonMutation.mutate,
+ isStartingDaemon: startDaemonMutation.isPending,
- stopDaemon: stopDaemonMutation.mutate,
- isStoppingDaemon: stopDaemonMutation.isPending,
+ stopDaemon: stopDaemonMutation.mutate,
+ isStoppingDaemon: stopDaemonMutation.isPending,
- // Install
- info: infoQuery.data,
- infoLoading: infoQuery.isLoading,
- refetchInfo: infoQuery.refetch,
+ // Install
+ info: infoQuery.data,
+ infoLoading: infoQuery.isLoading,
+ refetchInfo: infoQuery.refetch,
- install: installMutation.mutate,
- installAsync: installMutation.mutateAsync,
- isInstalling: installMutation.isPending,
- };
+ install: installMutation.mutate,
+ installAsync: installMutation.mutateAsync,
+ isInstalling: installMutation.isPending,
+ }),
+ [
+ statusQuery.data,
+ statusQuery.isLoading,
+ statusQuery.error,
+ statusQuery.refetch,
+ configQuery.data,
+ configQuery.isLoading,
+ modelsQuery.data,
+ modelsQuery.isLoading,
+ rawSettingsQuery.data,
+ rawSettingsQuery.isLoading,
+ rawSettingsQuery.refetch,
+ updateConfigMutation.mutate,
+ updateConfigMutation.mutateAsync,
+ updateConfigMutation.isPending,
+ saveRawSettingsMutation.mutate,
+ saveRawSettingsMutation.mutateAsync,
+ saveRawSettingsMutation.isPending,
+ startAuthMutation.mutate,
+ startAuthMutation.mutateAsync,
+ startAuthMutation.isPending,
+ startAuthMutation.data,
+ startDaemonMutation.mutate,
+ startDaemonMutation.isPending,
+ stopDaemonMutation.mutate,
+ stopDaemonMutation.isPending,
+ infoQuery.data,
+ infoQuery.isLoading,
+ infoQuery.refetch,
+ installMutation.mutate,
+ installMutation.mutateAsync,
+ installMutation.isPending,
+ ]
+ );
}
diff --git a/ui/src/hooks/use-project-selection.ts b/ui/src/hooks/use-project-selection.ts
index 89f16e68..8e466178 100644
--- a/ui/src/hooks/use-project-selection.ts
+++ b/ui/src/hooks/use-project-selection.ts
@@ -5,7 +5,7 @@
* Used in conjunction with ProjectSelectionDialog component.
*/
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useMemo } from 'react';
interface GCloudProject {
id: string;
@@ -102,10 +102,13 @@ export function useProjectSelection() {
setState({ isOpen: false, prompt: null });
}, []);
- return {
- isOpen: state.isOpen,
- prompt: state.prompt,
- onSelect: handleSelect,
- onClose: handleClose,
- };
+ return useMemo(
+ () => ({
+ isOpen: state.isOpen,
+ prompt: state.prompt,
+ onSelect: handleSelect,
+ onClose: handleClose,
+ }),
+ [state.isOpen, state.prompt, handleSelect, handleClose]
+ );
}
diff --git a/ui/src/hooks/use-websocket.ts b/ui/src/hooks/use-websocket.ts
index 8ff5de46..16abc65f 100644
--- a/ui/src/hooks/use-websocket.ts
+++ b/ui/src/hooks/use-websocket.ts
@@ -4,7 +4,7 @@
* Manages WebSocket connection, auto-reconnect, and React Query invalidation.
*/
-import { useEffect, useState, useCallback, useRef } from 'react';
+import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
@@ -141,5 +141,5 @@ export function useWebSocket() {
return () => clearInterval(interval);
}, []);
- return { status, connect, disconnect };
+ return useMemo(() => ({ status, connect, disconnect }), [status, connect, disconnect]);
}
diff --git a/ui/src/pages/analytics.tsx b/ui/src/pages/analytics.tsx
deleted file mode 100644
index 520d385d..00000000
--- a/ui/src/pages/analytics.tsx
+++ /dev/null
@@ -1,420 +0,0 @@
-/**
- * Analytics Page
- *
- * Displays Claude Code usage analytics with charts.
- * Features trend charts, model breakdown, cost analysis, and anomaly detection.
- */
-
-import { useState, useMemo, useRef, useCallback } from 'react';
-import type { DateRange } from 'react-day-picker';
-import { startOfMonth, subDays, formatDistanceToNow } from 'date-fns';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Button } from '@/components/ui/button';
-import { Skeleton } from '@/components/ui/skeleton';
-import { Popover, PopoverContent, PopoverAnchor } from '@/components/ui/popover';
-import { DateRangeFilter } from '@/components/analytics/date-range-filter';
-import { UsageSummaryCards } from '@/components/analytics/usage-summary-cards';
-import { UsageTrendChart } from '@/components/analytics/usage-trend-chart';
-import { ModelBreakdownChart } from '@/components/analytics/model-breakdown-chart';
-import { ModelDetailsContent } from '@/components/analytics/model-details-content';
-import { SessionStatsCard } from '@/components/analytics/session-stats-card';
-import { CliproxyStatsCard } from '@/components/analytics/cliproxy-stats-card';
-import { TrendingUp, PieChart, RefreshCw, DollarSign, ChevronRight } from 'lucide-react';
-import {
- useUsageSummary,
- useUsageTrends,
- useHourlyUsage,
- useModelUsage,
- useRefreshUsage,
- useUsageStatus,
- useSessions,
- type ModelUsage,
-} from '@/hooks/use-usage';
-import { getModelColor, cn } from '@/lib/utils';
-import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
-
-// Format token count to human-readable (K/M/B)
-function formatTokens(num: number): string {
- if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
- if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
- if (num >= 1_000) return `${(num / 1_000).toFixed(0)}K`;
- return num.toString();
-}
-
-export function AnalyticsPage() {
- const { privacyMode } = usePrivacy();
-
- // Default to last 30 days
- const [dateRange, setDateRange] = useState({
- from: subDays(new Date(), 30),
- to: new Date(),
- });
- const [isRefreshing, setIsRefreshing] = useState(false);
- const [selectedModel, setSelectedModel] = useState(null);
- const [popoverPosition, setPopoverPosition] = useState<{ x: number; y: number } | null>(null);
- const [viewMode, setViewMode] = useState<'daily' | 'hourly'>('daily');
- const popoverAnchorRef = useRef(null);
-
- // Refresh hook
- const refreshUsage = useRefreshUsage();
-
- const handleRefresh = async () => {
- setIsRefreshing(true);
- try {
- await refreshUsage();
- } finally {
- setIsRefreshing(false);
- }
- };
-
- // Convert dates to API format
- const apiOptions = {
- startDate: dateRange?.from,
- endDate: dateRange?.to,
- };
-
- // Fetch data
- const { data: summary, isLoading: isSummaryLoading } = useUsageSummary(apiOptions);
- const { data: trends, isLoading: isTrendsLoading } = useUsageTrends(apiOptions);
- const { data: hourlyData, isLoading: isHourlyLoading } = useHourlyUsage(apiOptions);
- const { data: models, isLoading: isModelsLoading } = useModelUsage(apiOptions);
- const { data: sessions, isLoading: isSessionsLoading } = useSessions({ ...apiOptions, limit: 3 });
- const { data: status } = useUsageStatus();
-
- // Handle "24H" preset click
- const handleTodayClick = useCallback(() => {
- const now = new Date();
- setDateRange({ from: subDays(now, 1), to: now });
- setViewMode('hourly');
- }, []);
-
- // Handle date range changes from DateRangeFilter
- const handleDateRangeChange = useCallback((range: DateRange | undefined) => {
- setDateRange(range);
- setViewMode('daily'); // Switch back to daily view for multi-day ranges
- }, []);
-
- // Format "Last updated" text
- const lastUpdatedText = useMemo(() => {
- if (!status?.lastFetch) return null;
- return formatDistanceToNow(new Date(status.lastFetch), { addSuffix: true });
- }, [status?.lastFetch]);
-
- // Handle model click for popover
- const handleModelClick = useCallback((model: ModelUsage, event: React.MouseEvent) => {
- const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
- setPopoverPosition({ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 });
- setSelectedModel(model);
- }, []);
-
- const handlePopoverClose = useCallback(() => {
- setSelectedModel(null);
- setPopoverPosition(null);
- }, []);
-
- return (
-
- {/* Header */}
-
-
-
Analytics
-
Track usage & insights
-
-
-
-
-
- {lastUpdatedText && (
-
- Updated {lastUpdatedText}
-
- )}
-
-
-
-
- {/* Summary Cards */}
-
-
- {/* Main Content */}
-
- {/* Usage Trend Chart - Full Width */}
-
-
-
-
- {viewMode === 'hourly' ? 'Last 24 Hours' : 'Usage Trends'}
-
-
-
-
-
-
-
- {/* Bottom Row - Cost by Model (4) + Model Usage (2) + Session Stats (2) + CLIProxy Stats (2) */}
-
- {/* Cost by Model - 4/10 width with breakdown */}
-
-
-
-
- Cost by Model
-
-
-
- {isModelsLoading ? (
-
- ) : (
-
- {[...(models || [])]
- .sort((a, b) => b.cost - a.cost)
- .map((model) => (
-