# 🖥️ 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