Saltar al contenido
# 🖥️ Dashboard — klaude-proxy ## 🤔 ¿Qué hago? ¿Cómo lo hago? ¿Y para qué lo hago? **¿Qué hago?** Soy la interfaz web de observabilidad de klaude-proxy. Muestro en tiempo real los logs del proxy, estadísticas de caché, costes inferidos de inferencia, llamadas al proveedor y el contenido de cada petición enviada a Anthropic. **¿Cómo lo hago?** Con una página HTML servida por FastAPI en `GET /dashboard`. El canal de datos en tiempo real es Server-Sent Events (SSE) desde `GET /logs/stream`. Las estadísticas de costes y llamadas se obtienen polleando `GET /analytics/stats` cada 5 segundos. Los contadores de sesión (hits, misses, vectores) se obtienen polleando `GET /cache/stats` cada 5 segundos. **¿Y para qué lo hago?** Para que en una sola vista se pueda ver el estado del sistema, el coste en tiempo real, si la caché está funcionando, y el contenido exacto de lo que se envía a Anthropic — sin necesidad de entrar al contenedor ni parsear logs manualmente. --- ## 🗺️ Layout del dashboard ``` ┌─────────────────────────────────────────┬──────────────────────┐ │ LIVE LOGS │ SESSION COUNTERS │ │ ───────────────────────────────────── │ Hits / Misses │ │ [log entry…] │ Vectors (Qdrant) │ │ [log entry…] │ │ │ [log entry…] │ COSTES 💰 │ │ │ Real / Ahorrado │ │ REQUEST CONTENT │ Tokens In / Out │ │ ───────────────────────────────────── │ │ │ [preview petición al proveedor…] │ LLAMADAS PROVEEDOR │ │ [preview petición al proveedor…] │ Salientes / Entrantes│ └─────────────────────────────────────────┴──────────────────────┘ ``` **Columna izquierda:** `Live Logs` (flex:1, scroll infinito) + `Request Content` (altura fija 220px, scroll) **Columna derecha (sidebar 260px):** `Session Counters` + `Costes` + `Llamadas Proveedor` --- ## 📡 Fuentes de datos | Panel | Fuente | Frecuencia | | --- | --- | --- | | Live Logs | `GET /logs/stream` (SSE push) | Tiempo real | | Request Content | Evento `FORWARD` parseado del SSE stream | Tiempo real | | Session Counters (hits/misses/vectors/KB/Sources) | `GET /cache/stats` + `GET /knowledge/stats` + `GET /sources/stats` | Poll 5s | | Costes y llamadas proveedor | `GET /analytics/stats` | Poll 5s | --- ## 🔢 Sección: Session Counters | Contador | Descripción | | --- | --- | | **Cache Hits** | Peticiones servidas directamente desde caché (`X-Cache: HIT`) | | **Cache Misses** | Peticiones reenviadas al LLM sin ningún contexto de caché | | **Vectors** | Puntos indexados en `klaude_cache` (= entradas cacheadas manualmente) | | **KB HIT** | Peticiones enriquecidas con contexto de `klaude_knowledge` (`X-Cache: KNOWLEDGE-RAG`) | | **Sources HIT** | Peticiones enriquecidas con contexto de `klaude_sources` (`X-Cache: SOURCES-RAG`) | > ⚠️ **Nota técnica:** `vectors_count` fue deprecado en versiones modernas de `qdrant-client` y puede retornar `null`. El dashboard hace fallback a `points_count` cuando `vectors_count` es nulo o cero — en esta colección ambos valores son equivalentes (1 punto = 1 vector). --- ## 💰 Sección: Costes | Campo | Descripción | | --- | --- | | **Coste real** | USD gastados en llamadas a Anthropic en esta sesión | | **Ahorro caché** | USD que se habrían gastado sin caché (estimado) | | **Tokens In** | Total de tokens de entrada procesados | | **Tokens Out** | Total de tokens de salida generados | Los costes se calculan en base a la tabla de precios Anthropic mid-2026 definida en `analytics.py`. Ver [analytics.md](analytics.md) para la tabla de precios completa. --- ## 📞 Sección: Llamadas Proveedor | Campo | Descripción | | --- | --- | | **Salientes** | Número de peticiones reenviadas a Anthropic (cache MISSes) | | **Entrantes** | Número de respuestas recibidas de Anthropic | En condiciones normales, `Salientes == Entrantes`. Una discrepancia indicaría peticiones en vuelo o errores de red. --- ## 📋 Panel: Request Content Muestra el texto plano de cada petición enviada al proveedor, insertadas newest-first. Máximo 50 entradas en pantalla. **Cómo funciona:** el proxy emite un log con formato `FORWARD hash=<8chars> model=<model> text=<preview>` justo antes de reenviar a Anthropic. El dashboard parsea ese evento del SSE stream y lo inserta en el panel sin necesidad de un endpoint adicional. Cada entrada muestra: - 🕐 Timestamp local - Modelo usado - Preview del texto de la petición (hasta ~300 chars) --- ## 🔄 Flujo de datos completo ```mermaid flowchart LR subgraph Proxy A[log_buffer\nring buffer] -->|SSE push| B[GET /logs/stream] C[GET /cache/stats] --> D[Qdrant stats] E[GET /analytics/stats] --> F[_SessionStats\nin-memory] end subgraph Dashboard Browser B -->|EventSource| G[Live Logs panel] B -->|parse FORWARD event| H[Request Content panel] C -->|fetch poll 5s| I[Session Counters] E -->|fetch poll 5s| J[Costes + Llamadas] end ``` --- ## 🌐 Acceso ```bash # En local open http://localhost:8080/dashboard # En servidor remoto open http://192.168.1.50:8080/dashboard ``` No requiere autenticación — asumiendo que el puerto 8080 no está expuesto a internet (red local / VPN). --- ## ⚠️ Gotcha de desarrollo — Python triple-quoted strings y JS escapes > **Lección aprendida — Issue #168 / commit `36be2c0`** El HTML completo del dashboard vive en `proxy/main.py` como constante Python: ```python _DASHBOARD_HTML = """<!DOCTYPE html> ... const lines = buf.split('\n'); # ← PELIGRO ... """ ``` Dentro de una triple-quoted Python string, `'\n'` **no es un escape JS** — es un **salto de línea real** interpretado por Python al cargar el módulo. El browser recibe: ```text buf.split(' ') // ← salto de línea literal → SyntaxError: '' string literal contains an unescaped line break ``` Esto bloquea **toda la ejecución del `<script>`**: el badge queda en `connecting…`, ningún poll arranca, el SSE no se conecta. ### ✅ Regla para añadir JS a `_DASHBOARD_HTML` Cualquier secuencia de escape JavaScript dentro de la triple-quoted string **debe duplicar la barra**: | En JS real | En `_DASHBOARD_HTML` Python | Lo que llega al browser | | --- | --- | --- | | `'\n'` | `'\\n'` | `'\n'` ✅ | | `'\t'` | `'\\t'` | `'\t'` ✅ | | `'\r\n'` | `'\\r\\n'` | `'\r\n'` ✅ | | `'\\'` | `'\\\\'` | `'\\'` ✅ | Afectan a **strings JS con comillas simples o dobles**. Los template literals (backtick) con `${...}` no tienen este problema para `\n`, pero sí para `\\` si se quiere una barra literal. ### 🔍 Cómo detectar el bug Si el badge queda en `connecting…` y en Firefox DevTools → Console aparece: ```text Uncaught SyntaxError: '' string literal contains an unescaped line break ``` 1. Abrir DevTools → Debugger → buscar la línea indicada en el dashboard HTML 2. La cadena rota aparece dividida en dos líneas físicas 3. Buscar en `main.py` la cadena JS equivalente y añadir `\\` donde corresponda --- ## 🔗 Documentos relacionados - [analytics.md](analytics.md) — Módulo de analytics, SQLite y cálculo de costes - [proxy.md](proxy.md) — Todos los endpoints del proxy - [architecture.md](architecture.md) — Diagrama general del sistema