diff --git a/CHANGELOG.md b/CHANGELOG.md index cbd0ea0da5..8c53445602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [0.8.8] β 2026-02-17 + +### Added + +- π **Analytics page redesign** β Rebuilt analytics dashboard with Recharts (SVG-based) charts replacing the previous implementation. New layout: stat cards β model usage bar chart β provider breakdown table with success rates and avg latency +- π― **6 global routing strategies** β Expanded from 3 (Fill-First, Round-Robin, P2C) to 6, adding Random, Least-Used, and Cost-Optimized. All strategies now have descriptions and icons in the Settings β Routing tab +- π§ **Editable rate limits** β Rate limit defaults (RPM, Min Gap, Max Concurrent) are now editable in Settings β Resilience with save/cancel functionality. Values persist via the resilience API +- π **Policies in Resilience tab** β Moved PoliciesPanel (circuit breaker status + locked identifiers) from Security to Resilience tab for better logical grouping +- π§ **Prompt Cache in AI tab** β Relocated CacheStatsCard from Advanced to AI tab alongside Thinking Budget and System Prompt + +### Changed + +- β»οΈ **Settings page restructure** β Reorganized all settings tabs for better UX: + - **Security**: Simplified to Login/Password and IP Access Control only + - **Routing**: Expanded strategy grid with all 6 routing strategies + - **Resilience**: Reordered cards (Provider Profiles β Rate Limiting β Circuit Breakers β Policies & Locked Identifiers) + - **AI**: Now includes Thinking Budget, System Prompt, and Prompt Cache + - **Advanced**: Simplified to only Global Proxy configuration +- π **Backend routing strategies** β Implemented `random` (Fisher-Yates shuffle), `least-used` (sorted by `lastUsedAt`), `cost-optimized` (sorted by priority ascending), and fixed `p2c` (power-of-two-choices with health scoring) in `auth.ts` +- π **Resilience API updates** β GET endpoint now merges saved rate limit defaults with constants; PATCH endpoint accepts both `profiles` and `defaults` +- π **Usage page split** β Refactored Usage page into "Request Logs" (with updated icon) and a new dedicated "Limits & Quotas" page + +### Fixed + +- π **Provider add error** β Improved error handling for API responses when adding new provider connections, with clear validation feedback + +--- + ## [0.8.5] β 2026-02-17 ### Added @@ -191,6 +219,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +[0.8.8]: https://github.com/diegosouzapw/OmniRoute/compare/v0.8.5...v0.8.8 [0.8.5]: https://github.com/diegosouzapw/OmniRoute/compare/v0.8.0...v0.8.5 [0.8.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.7.0...v0.8.0 [0.7.0]: https://github.com/diegosouzapw/OmniRoute/compare/v0.6.0...v0.7.0 diff --git a/README.md b/README.md index 5969487cef..f619fc9be6 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ docker compose --profile cli up -d | Image | Tag | Size | Description | | ------------------------ | -------- | ------ | --------------------- | | `diegosouzapw/omniroute` | `latest` | ~250MB | Latest stable release | -| `diegosouzapw/omniroute` | `0.8.5` | ~250MB | Current version | +| `diegosouzapw/omniroute` | `0.8.8` | ~250MB | Current version | --- @@ -166,19 +166,19 @@ docker compose --profile cli up -d ### π§ Core Routing & Intelligence -| Feature | What It Does | -| ------------------------------- | --------------------------------------------------------------------------------- | -| π― **Smart 4-Tier Fallback** | Auto-route: Subscription β API Key β Cheap β Free | -| π **Real-Time Quota Tracking** | Live token count + reset countdown per provider | -| π **Format Translation** | OpenAI β Claude β Gemini β Cursor β Kiro seamless | -| π₯ **Multi-Account Support** | Multiple accounts per provider with P2C selection | -| π **Auto Token Refresh** | OAuth tokens refresh automatically with retry | -| π¨ **Custom Combos** | 6 strategies: priority, weighted, round-robin, random, least-used, cost-optimized | -| π§© **Custom Models** | Add any model ID to any provider | -| π **Wildcard Router** | Route `provider/*` patterns to any provider dynamically | -| π§ **Thinking Budget** | Passthrough, auto, custom, and adaptive modes for reasoning models | -| π¬ **System Prompt Injection** | Global system prompt applied across all requests | -| π **Responses API** | Full OpenAI Responses API (`/v1/responses`) support for Codex | +| Feature | What It Does | +| ------------------------------- | ------------------------------------------------------------------------------ | +| π― **Smart 4-Tier Fallback** | Auto-route: Subscription β API Key β Cheap β Free | +| π **Real-Time Quota Tracking** | Live token count + reset countdown per provider | +| π **Format Translation** | OpenAI β Claude β Gemini β Cursor β Kiro seamless | +| π₯ **Multi-Account Support** | Multiple accounts per provider with intelligent selection | +| π **Auto Token Refresh** | OAuth tokens refresh automatically with retry | +| π¨ **Custom Combos** | 6 strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized | +| π§© **Custom Models** | Add any model ID to any provider | +| π **Wildcard Router** | Route `provider/*` patterns to any provider dynamically | +| π§ **Thinking Budget** | Passthrough, auto, custom, and adaptive modes for reasoning models | +| π¬ **System Prompt Injection** | Global system prompt applied across all requests | +| π **Responses API** | Full OpenAI Responses API (`/v1/responses`) support for Codex | ### π΅ Multi-Modal APIs @@ -193,26 +193,28 @@ docker compose --profile cli up -d ### π‘οΈ Resilience & Security -| Feature | What It Does | -| ------------------------------- | ------------------------------------------------------------ | -| π **Circuit Breaker** | Auto-open/close per-provider with configurable thresholds | -| π‘οΈ **Anti-Thundering Herd** | Mutex + semaphore rate-limit for API key providers | -| π§ **Semantic Cache** | Two-tier cache (signature + semantic) reduces cost & latency | -| β‘ **Request Idempotency** | 5s dedup window for duplicate requests | -| π **TLS Fingerprint Spoofing** | Bypass TLS-based bot detection via wreq-js | -| π **IP Filtering** | Allowlist/blocklist for API access control | -| π **Compliance Audit Log** | Tamper-proof request logs with opt-out per API key | +| Feature | What It Does | +| ------------------------------- | ------------------------------------------------------------- | +| π **Circuit Breaker** | Auto-open/close per-provider with configurable thresholds | +| π‘οΈ **Anti-Thundering Herd** | Mutex + semaphore rate-limit for API key providers | +| π§ **Semantic Cache** | Two-tier cache (signature + semantic) reduces cost & latency | +| β‘ **Request Idempotency** | 5s dedup window for duplicate requests | +| π **TLS Fingerprint Spoofing** | Bypass TLS-based bot detection via wreq-js | +| π **IP Filtering** | Allowlist/blocklist for API access control | +| π **Compliance Audit Log** | Tamper-proof request logs with opt-out per API key | +| π **Editable Rate Limits** | Configurable RPM, min gap, and max concurrent at system level | ### π Observability & Analytics -| Feature | What It Does | -| ------------------------ | ------------------------------------------------------ | -| π **Request Logging** | Debug mode with full request/response logs | -| πΎ **SQLite Proxy Logs** | Persistent proxy logs survive server restarts | -| π **Usage Analytics** | Track tokens, cost, trends over time | -| π **Progress Tracking** | Opt-in SSE progress events for streaming | -| π§ͺ **LLM Evaluations** | Golden set testing with 4 match strategies | -| π **Request Telemetry** | p50/p95/p99 latency aggregation + X-Request-Id tracing | +| Feature | What It Does | +| ---------------------------- | --------------------------------------------------------------- | +| π **Request Logging** | Debug mode with full request/response logs | +| πΎ **SQLite Proxy Logs** | Persistent proxy logs survive server restarts | +| π **Analytics Dashboard** | Recharts-powered: stat cards, model usage chart, provider table | +| π **Progress Tracking** | Opt-in SSE progress events for streaming | +| π§ͺ **LLM Evaluations** | Golden set testing with 4 match strategies | +| π **Request Telemetry** | p50/p95/p99 latency aggregation + X-Request-Id tracing | +| π **Request Logs + Quotas** | Dedicated pages for log browsing and limits/quotas tracking | ### βοΈ Deployment & Sync @@ -292,7 +294,7 @@ registerSuite({ ## π οΈ Tech Stack - **Runtime**: Node.js 20+ -- **Language**: TypeScript 5.9 β **100% TypeScript** across `src/` and `open-sse/` (v0.8.5) +- **Language**: TypeScript 5.9 β **100% TypeScript** across `src/` and `open-sse/` (v0.8.8) - **Framework**: Next.js 16 + React 19 + Tailwind CSS 4 - **Database**: LowDB (JSON) + SQLite (domain state + proxy logs) - **Streaming**: Server-Sent Events (SSE) @@ -347,7 +349,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. ```bash # Create a release β npm publish happens automatically -gh release create v0.8.5 --title "v0.8.5" --generate-notes +gh release create v0.8.8 --title "v0.8.8" --generate-notes ``` --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 29f4c3dcfa..ca157e9956 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -408,6 +408,9 @@ erDiagram number stickyRoundRobinLimit boolean requireLogin string password_hash + string fallbackStrategy + json rateLimitDefaults + json providerProfiles } PROVIDER_CONNECTION { @@ -750,8 +753,9 @@ Environment variables actively used by code: 3. Request logger writes full headers/body when enabled; treat log directory as sensitive. 4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability. 5. The `open-sse/` directory is published as the `@omniroute/open-sse` **npm workspace package**. Source code imports it via `@omniroute/open-sse/...` (resolved by Next.js `transpilePackages`). File paths in this document still use the directory name `open-sse/` for consistency. -6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive visualizations (bar charts, donut charts). +6. Charts in the dashboard use **Recharts** (SVG-based) for accessible, interactive analytics visualizations (model usage bar charts, provider breakdown tables with success rates). 7. E2E tests use **Playwright** (`tests/e2e/`), run via `npm run test:e2e`. Unit tests use **Node.js test runner** (`tests/unit/`), run via `npm run test:plan3`. Source code under `src/` is **TypeScript** (`.ts`/`.tsx`); the `open-sse/` workspace remains JavaScript (`.js`). +8. Settings page is organized into 5 tabs: Security, Routing (6 global strategies: fill-first, round-robin, p2c, random, least-used, cost-optimized), Resilience (editable rate limits, circuit breaker, policies), AI (thinking budget, system prompt, prompt cache), Advanced (proxy). ## Operational Verification Checklist diff --git a/docs/TASKS.md b/docs/TASKS.md index fff6dd4ead..3ea8b535a5 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -8,24 +8,24 @@ ### Backend Core -- [ ] `constants.js` β Substituir `COOLDOWN_MS.transient` por `transientInitial` (5s) + `transientMax` (60s) -- [ ] `constants.js` β Adicionar `PROVIDER_PROFILES` (oauth / apikey) com cooldowns diferenciados -- [ ] `constants.js` β Adicionar `DEFAULT_API_LIMITS` (100 RPM, 200ms minTime) -- [ ] `providerRegistry.js` β Criar helper `getProviderCategory(providerId)` β `"oauth"` | `"apikey"` -- [ ] `accountFallback.js` β Aceitar `provider` como parΓ’metro em `checkFallbackError` -- [ ] `accountFallback.js` β Implementar backoff exponencial para 502/503/504 transientes -- [ ] `accountFallback.js` β Calcular cooldown baseado no perfil do provedor -- [ ] `accountFallback.js` β Adicionar helper `getProviderProfile(provider)` +- [x] `constants.ts` β Substituir `COOLDOWN_MS.transient` por `transientInitial` (5s) + `transientMax` (60s) +- [x] `constants.ts` β Adicionar `PROVIDER_PROFILES` (oauth / apikey) com cooldowns diferenciados +- [x] `constants.ts` β Adicionar `DEFAULT_API_LIMITS` (100 RPM, 200ms minTime) +- [x] `providerRegistry.ts` β Criar helper `getProviderCategory(providerId)` β `"oauth"` | `"apikey"` +- [x] `accountFallback.ts` β Aceitar `provider` como parΓ’metro em `checkFallbackError` +- [x] `accountFallback.ts` β Implementar backoff exponencial para 502/503/504 transientes +- [x] `accountFallback.ts` β Calcular cooldown baseado no perfil do provedor +- [x] `accountFallback.ts` β Adicionar helper `getProviderProfile(provider)` ### Callers (propagar `provider`) -- [ ] `auth.js` β `markAccountUnavailable` β Passar `provider` para `checkFallbackError` -- [ ] `combo.js` β `handleComboChat` / `handleRoundRobinCombo` β Passar `provider` nos erros +- [x] `auth.ts` β `markAccountUnavailable` β Passar `provider` para `checkFallbackError` +- [x] `combo.ts` β `handleComboChat` / `handleRoundRobinCombo` β Passar `provider` nos erros ### Testes -- [ ] Atualizar `rate-limit-enhanced.test.mjs` β Teste "transient errors don't increase backoff" β `newBackoffLevel = 1` -- [ ] Criar `error-classification.test.mjs` β Cooldown exponencial 502, perfis OAuth/API, helper `getProviderCategory` +- [x] Atualizar `rate-limit-enhanced.test.mjs` β Teste "transient errors don't increase backoff" β `newBackoffLevel = 1` +- [x] Criar `error-classification.test.mjs` β Cooldown exponencial 502, perfis OAuth/API, helper `getProviderCategory` --- @@ -33,15 +33,15 @@ ### Backend -- [ ] `combo.js` β Importar `getCircuitBreaker` e `CircuitBreakerOpenError` -- [ ] `combo.js` β `handleComboChat` β Verificar `breaker.canExecute()` antes de cada modelo -- [ ] `combo.js` β `handleRoundRobinCombo` β Integrar breaker per-model -- [ ] `combo.js` β Marcar `semaphore.markRateLimited` para 502/503/504 (nΓ£o sΓ³ 429) -- [ ] `combo.js` β Implementar early exit quando todos os modelos tΓͺm breaker OPEN +- [x] `combo.ts` β Importar `getCircuitBreaker` e `CircuitBreakerOpenError` +- [x] `combo.ts` β `handleComboChat` β Verificar `breaker.canExecute()` antes de cada modelo +- [x] `combo.ts` β `handleRoundRobinCombo` β Integrar breaker per-model +- [x] `combo.ts` β Marcar `semaphore.markRateLimited` para 502/503/504 (nΓ£o sΓ³ 429) +- [x] `combo.ts` β Implementar early exit quando todos os modelos tΓͺm breaker OPEN ### Testes -- [ ] Criar `combo-circuit-breaker.test.mjs` β Combo skip breaker OPEN, early exit, semΓ‘foro 502 +- [x] Criar `combo-circuit-breaker.test.mjs` β Combo skip breaker OPEN, early exit, semΓ‘foro 502 --- @@ -49,13 +49,13 @@ ### Backend -- [ ] `rateLimitManager.js` β Auto-enable para `apikey` providers com limites elevados -- [ ] `rateLimitManager.js` β Criar limiter com defaults (100 RPM) quando nΓ£o configurado -- [ ] `auth.js` β Adicionar mutex na `markAccountUnavailable` para evitar marcaΓ§Γ£o paralela +- [x] `rateLimitManager.ts` β Auto-enable para `apikey` providers com limites elevados +- [x] `rateLimitManager.ts` β Criar limiter com defaults (100 RPM) quando nΓ£o configurado +- [x] `auth.ts` β Adicionar mutex na `markAccountUnavailable` para evitar marcaΓ§Γ£o paralela ### Testes -- [ ] Criar `thundering-herd.test.mjs` β Mutex, auto-enable, limites nΓ£o restritivos +- [x] Criar `thundering-herd.test.mjs` β Mutex, auto-enable, limites nΓ£o restritivos --- @@ -63,30 +63,51 @@ ### Settings Page -- [ ] `settings/page.tsx` β Adicionar tab "Resilience" (icon: `health_and_safety`) entre Routing e Pricing +- [x] `settings/page.tsx` β Adicionar tab "Resilience" (icon: `health_and_safety`) entre Routing e Pricing ### Novos Componentes -- [ ] Criar `ResilienceTab.tsx` β Layout com 3 cards -- [ ] Criar `ProviderProfilesCard.tsx` β Toggle OAuth/API Key, inputs para cooldowns -- [ ] Criar `CircuitBreakerCard.tsx` β Status real-time per-provider, auto-refresh 5s, botΓ£o reset -- [ ] Criar `RateLimitOverviewCard.tsx` β Tabela providers Γ accounts Γ cooldown +- [x] Criar `ResilienceTab.tsx` β Layout com 4 cards (Provider Profiles β Rate Limiting β Circuit Breakers β Policies) +- [x] Criar `ProviderProfilesCard.tsx` β Toggle OAuth/API Key, inputs para cooldowns +- [x] Criar `CircuitBreakerCard.tsx` β Status real-time per-provider, auto-refresh 5s, botΓ£o reset +- [x] Criar `RateLimitOverviewCard.tsx` β Tabela providers Γ accounts Γ cooldown β **agora editΓ‘vel com RPM, Min Gap, Max Concurrent** ### API Routes -- [ ] Criar `api/resilience/route.ts` β GET (estado completo) + PATCH (salvar perfis) -- [ ] Criar `api/resilience/reset/route.ts` β POST (resetar breakers + cooldowns) +- [x] Criar `api/resilience/route.ts` β GET (estado completo + defaults mesclados) + PATCH (salvar perfis + defaults) +- [x] Criar `api/resilience/reset/route.ts` β POST (resetar breakers + cooldowns) ### MigraΓ§Γ£o -- [ ] Avaliar se `PoliciesPanel.tsx` pode ser removido ou simplificado apΓ³s nova aba +- [x] `PoliciesPanel.tsx` movido de Security para Resilience tab + +--- + +## Fase 5 β Settings Page Restructure (v0.9.0) + +### Tab Reorganization + +- [x] **Security** β Simplificado para Login/Password + IP Access Control +- [x] **Routing** β Expandido para 6 estratΓ©gias globais com descriΓ§Γ΅es +- [x] **Resilience** β Reordenado: Provider Profiles β Rate Limiting (editΓ‘vel) β Circuit Breakers β Policies +- [x] **AI** β Thinking Budget + System Prompt + Prompt Cache (movido do Advanced) +- [x] **Advanced** β Simplificado para apenas Global Proxy + +### Backend Routing Strategies + +- [x] `auth.ts` β Implementar `random` (Fisher-Yates shuffle) +- [x] `auth.ts` β Implementar `least-used` (sorted by lastUsedAt) +- [x] `auth.ts` β Implementar `cost-optimized` (sorted by priority) +- [x] `auth.ts` β Corrigir `p2c` (power-of-two-choices com health scoring) +- [x] `settings.ts` β Expandir tipo `fallbackStrategy` para 6 valores --- ## VerificaΓ§Γ£o Final -- [ ] Rodar todos os testes unitΓ‘rios: `node --test tests/unit/*.test.mjs` -- [ ] Build do Next.js: `npm run build` -- [ ] Verificar aba Resilience no browser -- [ ] Testar persistΓͺncia dos perfis (salvar β reload) -- [ ] Testar Reset All Breakers +- [x] Rodar todos os testes unitΓ‘rios: `node --test tests/unit/*.test.mjs` +- [x] Build do Next.js: `npm run build` +- [x] Verificar aba Resilience no browser +- [x] Testar persistΓͺncia dos perfis (salvar β reload) +- [x] Testar Reset All Breakers +- [x] Verificar todas as 5 tabs reestruturadas diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 492e644f67..1ec4be1afd 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -513,6 +513,9 @@ Configure via **Dashboard β Settings β Routing**. | **Fill First** | Uses accounts in priority order β primary account handles all requests until unavailable | | **Round Robin** | Cycles through all accounts with a configurable sticky limit (default: 3 calls per account) | | **P2C (Power of Two Choices)** | Picks 2 random accounts and routes to the healthier one β balances load with awareness of health | +| **Random** | Randomly selects an account for each request using Fisher-Yates shuffle | +| **Least Used** | Routes to the account with the oldest `lastUsedAt` timestamp, distributing traffic evenly | +| **Cost Optimized** | Routes to the account with the lowest priority value, optimizing for lowest-cost providers | #### Wildcard Model Aliases @@ -542,25 +545,47 @@ Chain: production-fallback Configure via **Dashboard β Settings β Resilience**. -OmniRoute implements provider-level resilience with three components: +OmniRoute implements provider-level resilience with four components: -1. **Circuit Breaker** β Tracks failures per provider and automatically opens the circuit when a threshold is reached: - - **CLOSED** (Healthy) β Requests flow normally - - **OPEN** β Provider is temporarily blocked after repeated failures - - **HALF_OPEN** β Testing if provider has recovered - -2. **Provider Profiles** β Per-provider configuration for: +1. **Provider Profiles** β Per-provider configuration for: - Failure threshold (how many failures before opening) - Cooldown duration - Rate limit detection sensitivity - Exponential backoff parameters -3. **Rate Limit Auto-Detection** β Monitors `429` and `Retry-After` headers to proactively avoid hitting provider rate limits. +2. **Editable Rate Limits** β System-level defaults configurable in the dashboard: + - **Requests Per Minute (RPM)** β Maximum requests per minute per account + - **Min Time Between Requests** β Minimum gap in milliseconds between requests + - **Max Concurrent Requests** β Maximum simultaneous requests per account + - Click **Edit** to modify, then **Save** or **Cancel**. Values persist via the resilience API. + +3. **Circuit Breaker** β Tracks failures per provider and automatically opens the circuit when a threshold is reached: + - **CLOSED** (Healthy) β Requests flow normally + - **OPEN** β Provider is temporarily blocked after repeated failures + - **HALF_OPEN** β Testing if provider has recovered + +4. **Policies & Locked Identifiers** β Shows circuit breaker status and locked identifiers with force-unlock capability. + +5. **Rate Limit Auto-Detection** β Monitors `429` and `Retry-After` headers to proactively avoid hitting provider rate limits. **Pro Tip:** Use **Reset All** button to clear all circuit breakers and cooldowns when a provider recovers from an outage. --- +### Settings Dashboard + +The settings page is organized into 5 tabs for easy navigation: + +| Tab | Contents | +| -------------- | ---------------------------------------------------------------------------------------------- | +| **Security** | Login/Password settings and IP Access Control (allowlist/blocklist) | +| **Routing** | Global routing strategy (6 options), wildcard model aliases, fallback chains, combo defaults | +| **Resilience** | Provider profiles, editable rate limits, circuit breaker status, policies & locked identifiers | +| **AI** | Thinking budget configuration, global system prompt injection, prompt cache stats | +| **Advanced** | Global proxy configuration (HTTP/SOCKS5) | + +--- + ### Costs & Budget Management Access via **Dashboard β Costs**. diff --git a/package.json b/package.json index 9ea1bf53a4..a35f0d8111 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "0.8.5", + "version": "0.8.8", "description": "Smart AI Router with auto fallback β route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { diff --git a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx index 79d55dc552..89ba789269 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.tsx @@ -29,6 +29,7 @@ export default function CLIToolsPageClient({ machineId }) { const [cloudEnabled, setCloudEnabled] = useState(false); const [apiKeys, setApiKeys] = useState([]); const [toolStatuses, setToolStatuses] = useState({}); + const [statusesLoaded, setStatusesLoaded] = useState(false); useEffect(() => { fetchConnections(); @@ -70,6 +71,8 @@ export default function CLIToolsPageClient({ machineId }) { } } catch (error) { console.log("Error fetching CLI tool statuses:", error); + } finally { + setStatusesLoaded(true); } }; @@ -144,7 +147,7 @@ export default function CLIToolsPageClient({ machineId }) { return "http://localhost:20128"; }; - if (loading) { + if (loading || !statusesLoaded) { return (
{tool.description}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CliStatusBadge.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/CliStatusBadge.tsx index b1e3701779..4048868b3e 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CliStatusBadge.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CliStatusBadge.tsx @@ -4,13 +4,41 @@ * Shared status badge for CLI tool cards. * Shows the effective config/installation status using batch data, * so badges are visible even when cards are collapsed. + * Optionally shows last-configured relative timestamp. */ -export default function CliStatusBadge({ effectiveConfigStatus, batchStatus }) { + +function formatRelativeTime(isoDate: string): string { + const now = Date.now(); + const then = new Date(isoDate).getTime(); + const diffMs = now - then; + if (diffMs < 0) return "just now"; + + const seconds = Math.floor(diffMs / 1000); + if (seconds < 60) return "just now"; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + + const months = Math.floor(days / 30); + if (months < 12) return `${months}mo ago`; + + return `${Math.floor(months / 12)}y ago`; +} + +export default function CliStatusBadge({ + effectiveConfigStatus, + batchStatus, + lastConfiguredAt = null, +}) { // Determine badge from effectiveConfigStatus or batchStatus const status = effectiveConfigStatus || batchStatus?.configStatus || null; - if (!status) return null; - const badges = { configured: { dotClass: "bg-green-500", @@ -39,14 +67,32 @@ export default function CliStatusBadge({ effectiveConfigStatus, batchStatus }) { }, }; - const badge = badges[status] || badges.unknown; + const badge = status ? badges[status] || badges.unknown : null; return ( - - - {badge.text} - + <> + {badge && ( + + + {badge.text} + + )} + {lastConfiguredAt ? ( + + schedule + {formatRelativeTime(lastConfiguredAt)} + + ) : status && status !== "not_installed" ? ( + + schedule + Never + + ) : null} + > ); } diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.tsx index 08538370b5..9f24dda46b 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClineToolCard.tsx @@ -17,6 +17,7 @@ export default function ClineToolCard({ activeProviders, cloudEnabled, batchStatus, + lastConfiguredAt, }) { const [clineStatus, setClineStatus] = useState(null); const [checkingCline, setCheckingCline] = useState(false); @@ -236,6 +237,7 @@ export default function ClineToolCard({{tool.description}
@@ -463,11 +465,14 @@ export default function ClineToolCard({ isOpen={showManualConfigModal} onClose={() => setShowManualConfigModal(false)} title="Cline Manual Configuration" - {...{onApply: handleManualConfig, currentConfig: { - model: selectedModel, - apiKey: selectedApiKey, - baseUrl: customBaseUrl || baseUrl, - }} as any} + {...({ + onApply: handleManualConfig, + currentConfig: { + model: selectedModel, + apiKey: selectedApiKey, + baseUrl: customBaseUrl || baseUrl, + }, + } as any)} /> )} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.tsx index bd4881c425..e443bb6698 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.tsx @@ -14,6 +14,7 @@ export default function CodexToolCard({ activeProviders, cloudEnabled, batchStatus, + lastConfiguredAt, }) { const [codexStatus, setCodexStatus] = useState(null); const [checkingCodex, setCheckingCodex] = useState(false); @@ -337,6 +338,7 @@ wire_api = "responses"{tool.description}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.tsx index 2b408544c8..5d2f1caa5e 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DroidToolCard.tsx @@ -17,6 +17,7 @@ export default function DroidToolCard({ activeProviders, cloudEnabled, batchStatus, + lastConfiguredAt, }) { const [droidStatus, setDroidStatus] = useState(null); const [checkingDroid, setCheckingDroid] = useState(false); @@ -270,6 +271,7 @@ export default function DroidToolCard({{tool.description}
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.tsx index be7e2621d6..48536dadce 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/KiloToolCard.tsx @@ -17,6 +17,7 @@ export default function KiloToolCard({ activeProviders, cloudEnabled, batchStatus, + lastConfiguredAt, }) { const [kiloStatus, setKiloStatus] = useState(null); const [checkingKilo, setCheckingKilo] = useState(false); @@ -222,6 +223,7 @@ export default function KiloToolCard({{tool.description}
@@ -448,11 +450,14 @@ export default function KiloToolCard({ isOpen={showManualConfigModal} onClose={() => setShowManualConfigModal(false)} title="Kilo Code Manual Configuration" - {...{onApply: handleManualConfig, currentConfig: { - model: selectedModel, - apiKey: selectedApiKey, - baseUrl: customBaseUrl || baseUrl, - }} as any} + {...({ + onApply: handleManualConfig, + currentConfig: { + model: selectedModel, + apiKey: selectedApiKey, + baseUrl: customBaseUrl || baseUrl, + }, + } as any)} /> )} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.tsx b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.tsx index 1909b98a8c..7fa4f72e34 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.tsx +++ b/src/app/(dashboard)/dashboard/cli-tools/components/OpenClawToolCard.tsx @@ -17,6 +17,7 @@ export default function OpenClawToolCard({ activeProviders, cloudEnabled, batchStatus, + lastConfiguredAt, }) { const [openclawStatus, setOpenclawStatus] = useState(null); const [checkingOpenclaw, setCheckingOpenclaw] = useState(false); @@ -274,6 +275,7 @@ export default function OpenClawToolCard({{tool.description}
diff --git a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx index 2ac3081126..1b37ac2308 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from "react"; import { Card, Button } from "@/shared/components"; +import { useNotificationStore } from "@/store/notificationStore"; // βββ State colors and labels ββββββββββββββββββββββββββββββββββββββββββββββ const STATE_STYLES = { @@ -28,6 +29,12 @@ const STATE_STYLES = { }, }; +const CB_STATUS = { + closed: { icon: "check_circle", color: "#22c55e", label: "Closed" }, + "half-open": { icon: "pending", color: "#f59e0b", label: "Half-Open" }, + open: { icon: "error", color: "#ef4444", label: "Open" }, +}; + function formatMs(ms) { if (!ms || ms <= 0) return "β"; if (ms < 1000) return `${ms}ms`; @@ -35,86 +42,6 @@ function formatMs(ms) { return `${(ms / 60000).toFixed(1)}m`; } -// βββ Circuit Breaker Card ββββββββββββββββββββββββββββββββββββββββββββββββ -function CircuitBreakerCard({ breakers, onReset, loading }) { - const activeBreakers = breakers.filter((b) => b.state !== "CLOSED"); - const totalBreakers = breakers.length; - - return ( -- No circuit breakers active yet. They are created automatically when requests flow - through the combo pipeline. -
- ) : ( -@@ -235,22 +199,34 @@ function RateLimitOverviewCard({ rateLimitStatus, defaults }) {
+ No circuit breakers active yet. They are created automatically when requests flow + through the combo pipeline. +
+ ) : ( ++ All systems operational β no lockouts or tripped breakers +
+Circuit Breakers
+Locked Identifiers
+| toggleSort("provider")}
+ >
+ Provider |
+ toggleSort("requests")}
+ >
+ Requests |
+ toggleSort("promptTokens")}
+ >
+ Input |
+ toggleSort("completionTokens")}
+ >
+ Output{" "}
+ |
+ toggleSort("totalTokens")}
+ >
+ Total |
+ toggleSort("cost")}
+ >
+ Cost |
+ Share | +
|---|---|---|---|---|---|---|
|
+
+
+ {p.provider}
+
+ |
+ + {fmtFull(p.requests)} + | ++ {fmt(p.promptTokens)} + | ++ {fmt(p.completionTokens)} + | ++ {fmt(p.totalTokens)} + | ++ {fmtCost(p.cost)} + | +
+
+
+
+
+
+
+ {pct}%
+
+ |
+