---
name: swl-dashboard
description: >
  Lanza el dashboard web interactivo de uso de Claude Code (claude-usage).
  Muestra métricas históricas de tokens, costos por modelo, sesiones y proyectos
  con gráficos interactivos. Requiere Python 3.8+. Subcomandos: dashboard (web),
  today (resumen de hoy en terminal), stats (estadísticas históricas en terminal).
  Activar cuando el usuario pide ver el dashboard, análisis histórico de costos,
  métricas de sesiones anteriores o comparativa de modelos.
user-invocable: false
version: "1.0.0"
herramientasPermitidas: [Read, Bash, Glob]
exclusiones:
  - "No cargar para ver métricas de la sesión actual en tiempo real — usar `/swl:status metricas`; este skill requiere datos históricos de múltiples sesiones en `~/.claude/projects/**/*.jsonl`."
  - "No cargar si `claude-usage` no está disponible y el objetivo es solo verificar el estado del sistema SWL — usar `/swl:status salud` que no depende de la instalación de la herramienta vendor."
  - "No cargar para diagnosticar por qué un agente falló — este skill muestra métricas de tokens y costos, no trazas de error de agentes; para eso usar `tracing-processor` o revisar `.planning/traces/`."
  - "No cargar en entornos CI/headless donde no hay browser para abrir el dashboard web — usar el subcomando `stats` o `today` que salen en terminal sin servidor HTTP."
evolvable: true  # default para skill estandar
---
# Skill: swl-dashboard

Lanza el análisis de uso histórico de Claude Code mediante claude-usage
(scripts/vendor/claude-usage/), una herramienta Python zero-dependencies
que parsea los JSONL de Claude Code y genera un dashboard web interactivo.

## Subcomandos

| Subcomando | Comportamiento |
|------------|---------------|
| (vacío) o `dashboard` | Escanea JSONL → abre dashboard web en http://localhost:8080 |
| `today` | Imprime en terminal el resumen de consumo del día actual |
| `stats` | Imprime estadísticas históricas completas (all-time) |
| `scan` | Solo sincroniza JSONL → SQLite sin abrir dashboard |

## Paso 0 — Verificar Python disponible

```bash
python3 --version 2>/dev/null || python --version 2>/dev/null
```

Si Python < 3.8 o no está disponible:
```
⚠ swl-dashboard requiere Python 3.8+. No se encontró intérprete compatible.
   Instala Python desde https://python.org o usa: winget install Python.Python.3
   Alternativa: usa /swl:status metricas para métricas de la sesión actual.
```
Detener y mostrar el mensaje anterior.

## Paso 1 — Detectar intérprete y localizar cli.py

Localizar `cli.py` con búsqueda en cascada (proyecto → global ~/.claude → npm → caché npx):

```bash
# 1. Root del proyecto actual
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
CLI_PY="$PROJECT_ROOT/scripts/vendor/claude-usage/cli.py"

# 2. Fallback: instalación global de swl-ses en cualquier runtime compatible
#    Copiado automáticamente por `swl-ses install --global --target <runtime>`
#    Cubre: claude, openclaude, opencode, gemini, copilot, codex
if [ ! -f "$CLI_PY" ]; then
  for RUNTIME_DIR in "$HOME/.claude" "$HOME/.openclaude" "$HOME/.opencode" "$HOME/.gemini" "$HOME/.github" "$HOME/.codex"; do
    CANDIDATE="$RUNTIME_DIR/scripts/vendor/claude-usage/cli.py"
    if [ -f "$CANDIDATE" ]; then
      CLI_PY="$CANDIDATE"
      break
    fi
  done
fi

# 3. Fallback: instalación global de npm (paquete canónico o mirror)
if [ ! -f "$CLI_PY" ]; then
  NPM_GLOBAL=$(npm root -g 2>/dev/null)
  for SCOPE in "@saulwade/swl-ses" "@saul-wade/swl-ses"; do
    CANDIDATE="$NPM_GLOBAL/$SCOPE/scripts/vendor/claude-usage/cli.py"
    if [ -f "$CANDIDATE" ]; then
      CLI_PY="$CANDIDATE"
      break
    fi
  done
fi

# 4. Fallback: caché de npx (npx swl-ses sin instalar globalmente)
if [ ! -f "$CLI_PY" ]; then
  NPX_CACHE=$(npm config get cache 2>/dev/null)/_npx
  FOUND=$(find "$NPX_CACHE" -name "cli.py" -path "*/claude-usage/cli.py" 2>/dev/null | sort -r | head -1)
  [ -n "$FOUND" ] && CLI_PY="$FOUND"
fi

echo "CLI_PY=$CLI_PY"
[ -f "$CLI_PY" ] && echo "OK" || echo "MISSING"
```

Si el resultado es `MISSING` después de los cuatro intentos:
```
⚠ claude-usage no encontrado en ninguna ubicación conocida.

   La herramienta requiere que swl-ses esté instalado globalmente.
   Ejecuta desde el directorio del repo swl-ses:

     swl-ses install --global --profile completo

   Esto copia claude-usage a ~/.claude/scripts/vendor/claude-usage/ y lo hace
   disponible en todos los proyectos automáticamente.

   Alternativa inmediata: /swl:status metricas para métricas de la sesión actual.
```

> **Nota**: guardar `CLI_PY` y el directorio padre `VENDOR_DIR=$(dirname "$CLI_PY")` para usarlos en el Paso 2.

## Paso 2 — Ejecutar según subcomando

> Usar `CLI_PY` y `VENDOR_DIR` detectados en el Paso 1.
> Todos los comandos necesitan `cd "$VENDOR_DIR"` previo para que los imports relativos (scanner.py, dashboard.py) funcionen.

### Subcomando: `today`

```bash
cd "$VENDOR_DIR"
python "$CLI_PY" scan 2>/dev/null
python "$CLI_PY" today
```

Muestra el consumo del día en terminal. Ejemplo de salida esperada:
```
------------------------------------------------------------
  Today's Usage  (2026-04-08)
------------------------------------------------------------
  claude-sonnet-4-6   turns=47  in=125.4K  out=42.9K  cost=$0.58
------------------------------------------------------------
  TOTAL               turns=47  in=125.4K  out=42.9K  cost=$0.58
  Sessions today: 3
------------------------------------------------------------
```

### Subcomando: `stats`

```bash
cd "$VENDOR_DIR"
python "$CLI_PY" scan 2>/dev/null
python "$CLI_PY" stats
```

Muestra estadísticas históricas all-time con desglose por modelo y top proyectos.

### Subcomando: `scan`

```bash
cd "$VENDOR_DIR"
python "$CLI_PY" scan
echo "✓ Base de datos actualizada: ~/.claude/usage.db"
```

### Subcomando: `dashboard` (o vacío)

`cli.py` detecta automáticamente el primer puerto libre (8888 → 9090 → 9191 …).
**Nota:** 8080 está excluido intencionalmente — suele estar ocupado por Postgres, IIS u otros servicios.
Si el usuario especificó un puerto con `/swl:status dashboard --port 9090`, pasarlo explícitamente.

**IMPORTANTE:** No verificar disponibilidad con `curl http://localhost:8080` — ese puerto puede estar
ocupado por otro servicio y devolvería un falso positivo. Usar el flujo siguiente:

```bash
cd "$VENDOR_DIR"
# Scan previo para asegurar datos actualizados
python "$CLI_PY" scan 2>/dev/null

# Detectar qué puerto está libre ANTES de iniciar (para saber cuál reportar al usuario)
DASHBOARD_PORT=$(python3 -c "
import socket
candidates = [8888, 9090, 9191, 7777, 7878, 8181, 8282, 8383]
for p in candidates:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            s.bind(('localhost', p))
            print(p)
            break
        except OSError:
            pass
" 2>/dev/null)

# Si el usuario especificó un puerto explícito, usarlo en su lugar
# DASHBOARD_PORT="${PUERTO_EXPLICITO:-$DASHBOARD_PORT}"

# Iniciar dashboard en background con el puerto detectado
python "$CLI_PY" dashboard --port "$DASHBOARD_PORT" &
sleep 3
```

Después de ejecutar el último comando, mostrar al usuario:
```
Dashboard iniciando en http://localhost:<DASHBOARD_PORT>

Filtros disponibles:
  • Rangos de tiempo: 7d, 30d, 90d, all
  • Selección de modelos: checkboxes interactivos
  • URLs bookmarkeables: ?range=7d&models=claude-sonnet-4-6

Presiona Ctrl+C en el terminal para detener el servidor.
```

> El dashboard se auto-refresca cada 30 segundos. Para sesiones activas,
> los datos se sincronizan al detener la sesión (Stop hook).

## Paso 3 — Si falla la ejecución

Si Python devuelve error al correr cli.py:

```bash
cd "$VENDOR_DIR"
python "$CLI_PY" scan 2>&1
```

Mostrar el error exacto y sugerir según el tipo:

| Error | Causa | Solución |
|-------|-------|----------|
| `No module named X` | cli.py es zero-dependencies; no debería ocurrir | Verifica integridad: `head -5 "$CLI_PY"` |
| `No such file ~/.claude/projects/` | Claude Code no ha creado sesiones aún | Inicia una sesión de trabajo normal primero |
| `PermissionError: [WinError 10013]` | Puerto reservado por Windows (Hyper-V, Docker, IIS) | El auto-detect de puertos debería evitarlo; si persiste: `python "$CLI_PY" dashboard --port 9999` |
| `No se encontró ningún puerto disponible` | Todos los puertos candidatos bloqueados | Especificar uno manualmente: `--port 9999` |

## Datos que visualiza el dashboard

| Métrica | Descripción |
|---------|-------------|
| Daily token usage | Gráfico de barras apiladas: input / output / cache read / cache creation por día |
| By model | Doughnut: distribución de tokens por modelo (sonnet vs opus vs haiku) |
| Top projects | Barras horizontales: top 10 proyectos por consumo |
| Sessions table | Tabla con: sesión, proyecto, duración, modelo, turns, tokens, costo estimado |
| Model cost table | Desglose de costo total por modelo |

## Dónde se almacenan los datos

- **Base de datos**: `~/.claude/usage.db` (SQLite, ~cero dependencias)
- **Fuente de datos**: `~/.claude/projects/**/*.jsonl` (logs de Claude Code)
- **Actualización**: al ejecutar `scan` o al inicio de cada subcomando

## Subcomando: `guardrails`

Muestra en terminal las métricas de activación y bloqueo de los guardrails (hooks bloqueantes)
de la sesión actual. Los datos provienen de `hooks/lib/guardrail-metrics.js`.

```bash
# Leer métricas de guardrails de la sesión actual desde /tmp/
SESSION_ID="${CLAUDE_SESSION_ID:-default}"
BRIDGE_FILE="/tmp/swl-guardrail-metrics-${SESSION_ID}.json"

if [ -f "$BRIDGE_FILE" ]; then
  node -e "
    const m = require('$BRIDGE_FILE');
    const hooks = m.hooks || {};
    const entries = Object.entries(hooks).sort(([,a],[,b]) => b.bloqueos - a.bloqueos);
    if (entries.length === 0) {
      console.log('Ningún guardrail se activó en esta sesión.');
    } else {
      console.log('\\nGuardrails — Activaciones de la sesión\\n');
      console.log('Hook'.padEnd(30) + 'Activaciones'.padStart(13) + 'Bloqueos'.padStart(9) + 'Warnings'.padStart(9) + 'Tasa'.padStart(7));
      console.log('-'.repeat(68));
      for (const [nombre, datos] of entries) {
        const tasa = datos.activaciones > 0 ? Math.round(datos.bloqueos / datos.activaciones * 100) + '%' : '0%';
        console.log(nombre.replace('.js','').padEnd(30) + String(datos.activaciones).padStart(13) + String(datos.bloqueos).padStart(9) + String(datos.warnings).padStart(9) + tasa.padStart(7));
      }
    }
  "
else
  echo "Sin datos de guardrails para esta sesión. (bridge: $BRIDGE_FILE)"
  echo "Los guardrails registran métricas cuando se activan durante la sesión."
fi
```

Después de mostrar la tabla, ejecutar diagnóstico automático:

```bash
node -e "
  try {
    const { getMetrics, diagnosticar } = require('./hooks/lib/guardrail-metrics');
    const SESSION_ID = process.env.CLAUDE_SESSION_ID || 'default';
    const metricas = getMetrics(SESSION_ID);
    const advertencias = diagnosticar(metricas);
    if (advertencias.length > 0) {
      console.log('\\n⚠ Diagnóstico:');
      advertencias.forEach(a => console.log('  - ' + a));
    } else {
      console.log('\\n✓ Guardrails sin anomalías detectadas.');
    }
  } catch(e) { console.log('(sin datos disponibles)'); }
"
```

> El resumen de guardrails también aparece automáticamente al final de cada sesión
> (hook `resumen-sesion.js`) cuando hay advertencias de tasa anómala.

## Estructura por widgets (v2, inspirado en mission-control)

El módulo `scripts/lib/dashboard-widgets.js` define un catálogo estructurado de 19 widgets
distribuidos en 7 categorías. Permite organizar la salida del dashboard por bloques
categorizados en lugar del bloque monolítico actual.

### Categorías disponibles

| Categoría  | Widgets incluidos                                               | Modo     |
|------------|-----------------------------------------------------------------|----------|
| health     | health-score, health-row, health-componentes                    | offline  |
| agentes    | agentes-activos, agentes-top-uso, cadenas-delegacion           | mixed    |
| metricas   | tokens-hoy, costo-hoy, costo-por-agente, costo-por-modelo      | offline  |
| evolucion  | nudges-recientes, evoluciones-aplicadas, alertas-persistentes  | offline  |
| sesiones   | sesiones-activas, sesiones-historial                            | offline  |
| costos     | costos-tendencia, costos-proyectos                              | offline  |
| seguridad  | security-posture, auditoria-reciente                            | online   |

Modos: `offline` = funciona leyendo archivos locales; `online` = requiere conexión activa.

### Uso básico

```javascript
const { listarPorCategoria, listarPorModo, obtenerWidget, obtenerLayoutDefault } =
  require('./scripts/lib/dashboard-widgets');

// Todos los widgets de salud disponibles sin conexión
const widgetsHealth = listarPorCategoria('health');

// Layout sugerido para sesión sin gateway
const layout = obtenerLayoutDefault('offline');

// Obtener un widget por ID
const w = obtenerWidget('health-score');
// → { id: 'health-score', label: 'Score de Salud', categoria: 'health', modos: ['offline'], tamanoDefault: 'sm', ... }
```

### Integración futura en la salida del comando

Al generar la salida de `/swl:status dashboard`, iterar el layout default del modo detectado
y renderizar cada widget con su categoría como sección:

```javascript
const layout = obtenerLayoutDefault('offline');
for (const id of layout) {
  const widget = obtenerWidget(id);
  // Renderizar sección: `## ${widget.categoria} / ${widget.label}`
}
```

La integración real de datos por widget es responsabilidad del código de ejecución del
comando. El catálogo solo define la estructura — no accede a fuentes de datos.

## Relación con otros componentes SWL

| Componente SWL | Scope | Complemento |
|---------------|-------|-------------|
| `tracking-costos.js` (hook) | Sesión actual, tiempo real | swl-dashboard añade histórico multi-sesión |
| `/swl:status metricas` (comando) | Sesión actual | swl-dashboard añade visualización gráfica |
| `.planning/METRICAS.md` | Persistencia por proyecto | swl-dashboard agrega consulta SQL cross-project |
| `resumen-sesion.js` (hook) | Resumen inline al Stop | swl-dashboard añade análisis post-hoc detallado |
| `guardrail-metrics.js` (lib) | Sesión actual | swl-dashboard `guardrails` muestra activaciones/bloqueos por hook |

## Cuándo NO cargar

- El usuario quiere ver métricas de la sesión activa actual — usar `/swl:status metricas`; este skill procesa el historial persisitido en `~/.claude/projects/`, no el stream en tiempo real de la sesión.
- `claude-usage` no está instalado y el objetivo es diagnosticar la salud del sistema SWL — usar `/swl:status salud`; no tiene sentido cargar este skill si la única acción posible sería indicar que hay que instalar la herramienta vendor.
- Se ejecuta en un entorno CI/headless o en un servidor remoto sin browser — los subcomandos `today` y `stats` funcionan en terminal, pero si el objetivo era el dashboard web interactivo, ese entorno no lo soporta.
- Se busca trazar el motivo de fallo de un agente específico — este skill visualiza consumo de tokens y costos; los detalles de error de agente están en `.planning/traces/` y se analizan con `tracing-processor`.

## Gotchas / Errores comunes no obvios

- **`curl http://localhost:8080` devuelve 200 pero no es el dashboard**: el skill advierte explícitamente que el puerto 8080 suele estar ocupado por Postgres, IIS u otros servicios y puede devolver falso positivo. Causa: verificar disponibilidad en lugar de dejar que `cli.py` detecte el primer puerto libre. Solución: no verificar con `curl` — el flujo correcto es detectar el puerto libre con el script Python de socket binding del Paso 2 y reportar ese puerto al usuario; si el usuario abre ese puerto, verá el dashboard correcto.
- **`cd "$VENDOR_DIR"` omitido antes de `python "$CLI_PY"`**: el script falla con `ModuleNotFoundError` porque `scanner.py` y `dashboard.py` se importan con rutas relativas. Causa: ejecutar `python $CLI_PY` desde el directorio de trabajo del proyecto en lugar de desde el directorio del vendor. Solución: siempre `cd "$VENDOR_DIR"` antes de cualquier invocación de `cli.py` — es el requisito documentado en el Paso 2 del skill.
- **Base de datos vacía después de ejecutar el dashboard**: el dashboard muestra gráficos vacíos aunque hay sesiones en `~/.claude/projects/`. Causa: `scan` no se ejecutó antes del subcomando principal. Solución: ejecutar `python "$CLI_PY" scan 2>/dev/null` como paso previo a cualquier subcomando — `scan` es quien parsea los JSONL y puebla `~/.claude/usage.db`.
- **Fallback a `~/.claude` cuando el repo tiene su propio vendor**: el script busca `CLI_PY` en el directorio del proyecto primero; si no lo encuentra, busca en instalaciones globales. Si el usuario tiene una versión global más antigua que la del repo, el skill usa la versión incorrecta. Causa: el Paso 1 resuelve los 4 fallbacks en orden pero no verifica versiones. Solución: si el repo tiene `scripts/vendor/claude-usage/cli.py`, esa siempre tiene precedencia — verificar con `head -1 "$CLI_PY"` que la versión encontrada es la del repo.
