---
name: emma-dashboard
description: Gestione e personalizzazione della dashboard di Emma (emma-dashboard.html). Modificare il file HTML, aggiornare sezioni, riavviare il server HTTP.
model_tier: 2
---

# Skill: emma-dashboard

Skill per gestire, aggiornare e migliorare la dashboard personale di Emma per Kuki.

## POLITICA DATI — solo dati reali, niente stime

**Kuki vuole solo dati veri.** Niente:
- Stime inventate (es. "45 tools disponibili" senza fonte)
- Placeholder (es. "ultimo riavvio: 14.06.2026 13:35" hardcoded)
- Agenti fittizi (es. leggere sessioni Telegram come "agenti attivi")
- Conteggi senza fonte reale

Se non hai una fonte dati affidabile per un widget, **non metterlo**. Mostra "dati non disponibili" piuttosto che un numero inventato.

## ❌ Widget rimossi in v2.1 — perché non c'erano dati reali

Questi widget sono stati rimossi perché le fonti dati davano risultati falsi:

| Widget | Fonte tentata | Perché rimosso |
|--------|--------------|----------------|
| **Agenti** | `sessions.json` | Mostrava "Roberta CCavadini" (l'utente Telegram) come se fosse un agente attivo. Non ci sono agenti Emma indipendenti da mostrare. |
| **Tools** | Stima hardcoded "45" | Era un numero inventato senza API per contare i tools Hermes. |
| **Lista provider** | Hardcoded | Non c'è API per verificare lo stato live dei provider. |

**Regola:** Se l'unica fonte dati dà risultati fuorvianti (es. sessioni Telegram spacciate per agenti), **non mettere il widget**. Meglio niente che dati sbagliati.

## ⚠️ Pitfall — DeepSeek via OpenRouter NON è gratuito

**Attenzione:** DeepSeek V4 Flash su `api.deepseek.com` (provider diretto) è gratuito.
Ma **DeepSeek V4 Flash via OpenRouter** (come provider) **NON è gratuito** — OpenRouter applica un mark-up sui token.

Se il config di Hermes usa OpenRouter come provider (anche per DeepSeek), ogni chiamata DeepSeek costa. Il `token_costs.json` deve distinguere:
- `provider: "DeepSeek"` + `note: "gratuito (api.deepseek.com)"` = gratuito
- `provider: "OpenRouter"` = a pagamento, anche se il modello è deepseek-chat

**Cosa controllare quando Kuki chiede costi:**
1. In che sessione siamo? (`hermes config get provider` / `hermes config get model`)
2. Se provider = OpenRouter → **tutto è a pagamento**, anche DeepSeek
3. Se provider = deepseek (api.deepseek.com) → DeepSeek è gratuito, Sonnet comunque a pagamento

**Caso reale (14/06/2026):**
- Sessione Sonnet via OpenRouter: $1.70 reali
- Sessioni DeepSeek via OpenRouter (prima dello switch a api.deepseek.com): costo reale (qualche centesimo)
- Sessioni DeepSeek via api.deepseek.com (dopo switch): gratuite

## ⚠️ Pitfall — Costi Sonnet persi da token_costs.json

Se il file `token_costs.json` viene resettato (es. durante fix della dashboard), i costi reali delle sessioni Sonnet passate vanno **recuperati manualmente**.

`dashboard_refresh.py` fa tracking incrementale via delta tokens (`last_lpt.txt`), quindi **non recupera automaticamente** sessioni passate non più tracciate.

**Recupero manuale:**
1. Cerca nello storico sessioni (`session_search(query="Sonnet 4 cost")`) il costo reale
2. Dalla sessione Sonnet: `estimated_cost_usd` dà il costo reale da OpenRouter
3. Scrivi manualmente in `token_costs.json` la voce mancante:
   ```json
   "2026-06-14": {
     "cost": 1.70,
     "calls": 8,
     "note": "7 DeepSeek (gratuite) + 1 Sonnet 4 ($1.70 da OpenRouter)"
   }
   ```
4. Rilancia `dashboard_refresh.py` — ora settimana/mese calcoleranno correttamente

**Esempio storico reale (14/06/2026):**
- Sessione Sonnet 4 (08:41-10:30): $1.70 da OpenRouter usage
- 7 chiamate DeepSeek V4 Flash (pomeriggio): gratuite
- Totale giorno: $1.70, 8 chiamate

## 🐞 Troubleshooting — Server HTTP non risponde

**Sintomo:** La dashboard non si apre (errore connessione, "pagina non raggiungibile").

**Diagnosi rapida:**
```bash
ss -tlnp | grep 8080                    # server in ascolto?
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/emma-dashboard.html
```

Se `ss` non mostra nulla o `curl` dà 000/FAIL → server crashed.

**Riavvio:**
1. `kill $(lsof -ti:8080) 2>/dev/null` (solo se il vecchio processo è rimasto attaccato)
2. Avviare via Hermes (background=true) — importante: **non usare nohup/&**, Hermes deve tracciare il processo
3. Dopo 2 secondi, verificare con `ss -tlnp | grep 8080` che sia in ascolto
4. Controllare il JSON dati: `curl -s http://localhost:8080/dashboard_data.json | python3 -m json.tool | head -5`

**IP update:** Se questa macchina cambia IP (es. riavvio router, cambio rete), aggiornare gli URL in questa skill. I ping via Tailscale (100.x.x.x) sono stabili anche se la LAN cambia.

## 📍 File Dashboard

```
~/.hermes/emma-dashboard.html         # HTML della dashboard
~/.hermes/scripts/dashboard_refresh.py  # Script Python che genera i dati JSON
~/.hermes/dashboard_data.json           # Dati JSON generati ogni 5 min
```

## 🔄 Ciclo di Aggiornamento

1. **Cron job** `dashboard-refresh` esegue `dashboard_refresh.py` ogni 5 min (no_agent)
2. Lo script genera `dashboard_data.json` in `~/.hermes/`
3. L'HTML lato browser fa fetch di `/dashboard_data.json` ogni 30 secondi
4. Il server HTTP serve entrambi dalla directory `~/.hermes/` sulla porta 8080

## 🌐 Accesso

**Server su questa macchina Linux (agent host).**

- **Tailscale (da PC aziendale Kuki):** http://100.78.71.82:8080/emma-dashboard.html — stabile anche con firewall aziendale se la porta 8080 è aperta
- **LAN locale:** http://10.205.63.252:8080/emma-dashboard.html (solo da questa rete)
- **Cloudflare Tunnel (HTTPS pubblico):** Usare `cloudflared tunnel --url http://localhost:8080` per generare un URL `https://*.trycloudflare.com/emma-dashboard.html`. Utile quando il firewall aziendale blocca la porta 8080 via Tailscale. L'URL cambia a ogni riavvio del tunnel.
- ~~**LAN (Acer):** http://192.168.1.102:8080/ — NON VALIDO, l'Acer non ospita il server. L'Acer è un PC Windows aziendale di Kuki (100.109.20.72 via Tailscale). Questa macchina (agent Hermes) ha IP LAN `10.205.63.252`.~~
- **HTTP Server:** `python3 -m http.server 8080 --directory ~/.hermes/ --bind 0.0.0.0` (da lanciare in background con Hermes: `terminal(background=true)`)

## ☁️ Cloudflare Tunnel — Accesso HTTPS pubblico

Vedi `references/cloudflare-tunnel-setup.md` per procedura completa di setup, attivazione e debug.

**TL;DR:** Avviare cloudflared in background, leggere URL dal log, dare a Kuki.

**Limiti NOTI (da comunicare a Kuki):**
- URL CAMBIA a ogni riavvio del tunnel — non è un link fisso
- Cloudflared in `/tmp/` — non installato permanentemente
- Per URL fisso serve dominio Cloudflare + named tunnel

## 🎯 Widget (v2.1)
| Widget | Descrizione |
|--------|------------|
| 📡 Stato | Provider + modello hardcoded nello script |
| 💰 Costo LLM | `token_costs.json` + sessione corrente → solo costi reali (DeepSeek=gratuito) |
| 🖥️ Salute Acer | `os.getloadavg()`, `free -m`, `df -h /` |
| 🎫 Ticket | `~/.hermes/tickets/*.json` (TUTTI i file JSON) |
| 🚢 FAK Confronto | `~/.hermes/fak-comparison/comparison_data.json` → campo `fak_comparison` |
| ⏰ Cron Jobs | `hermes cron list` (parsing output testuale con regex) |
| 📚 Skills | Conteggio cartelle `~/.hermes/skills/` |

## 🎫 Ticket — Link Cliccabili (v2.1+)

## 🛠️ Costi — Logica Corretta

Vedi `references/dashboard-refresh-script.md` per dettagli.

Principio:
- **DeepSeek V4 Flash** su `api.deepseek.com` = **gratuito** (nessun costo reale, solo conteggio chiamate)
- **Claude Sonnet 4** via OpenRouter = costo reale da `estimated_cost_usd` nella sessione
- `token_costs.json` tiene traccia dei costi giorno per giorno + delta tracking via `last_lpt.txt`

Non usare `PRICING` per stimare costi DeepSeek — è gratuito. Non forzare `KNOWN_COSTS` hardcoded.

## ⚠️ Pitfall — Cron Jobs Parsing

`hermes cron list` NON supporta `--json`. L'output è tabellare ANSI. Per parsarlo:

```python
import re
m = re.match(r'\s{2}([a-f0-9]+)\s+\[(\w+)\]', line.rstrip())   # job_id + stato
m2 = re.match(r'\s{4}(\w[\w\s-]*?):\s+(.*)', line.rstrip())     # campi (Name, Schedule, ecc.)
```

**Attenzione:** usare `line.strip()` rompe la regex perché toglie gli spazi iniziali necessari al match. Usare `line.rstrip()`.

## ⚠️ Pitfall — PATH in no_agent Scripts

Gli script eseguiti dal cron in modalità no_agent NON hanno `~/.local/bin` nel PATH. Aggiungere esplicitamente:

```python
env = os.environ.copy()
env['PATH'] = f"{os.path.expanduser('~/.local/bin')}:{env.get('PATH', '')}"
subprocess.check_output('hermes cron list', env=env, ...)
```

## ⚠️ Pitfall — Skills Counting

`hermes skills list --json` NON esiste. Contare direttamente dal filesystem:

```python
entries = [d for d in os.listdir(skills_dir) if os.path.isdir(os.path.join(skills_dir, d)) and not d.startswith('.')]
```

## ⚠️ Pitfall — DeepSeek via OpenRouter NON è gratuito

**Attenzione:** DeepSeek V4 Flash su `api.deepseek.com` (provider diretto) è gratuito.
Ma **DeepSeek V4 Flash via OpenRouter** (come provider) **NON è gratuito** — OpenRouter applica un mark-up sui token.

Se il config di Hermes usa OpenRouter come provider (anche per DeepSeek), ogni chiamata DeepSeek costa. Il `token_costs.json` deve distinguere:
- `provider: "DeepSeek"` + `note: "gratuito (api.deepseek.com)"` = gratuito
- `provider: "OpenRouter"` = a pagamento, anche se il modello è deepseek-chat

**Cosa controllare quando Kuki chiede costi:**
1. In che sessione siamo? (`hermes config get provider` / `hermes config get model`)
2. Se provider = OpenRouter → **tutto è a pagamento**, anche DeepSeek
3. Se provider = deepseek (api.deepseek.com) → DeepSeek è gratuito, Sonnet comunque a pagamento

**Caso reale (14/06/2026):**
- Sessione Sonnet via OpenRouter: $1.70 reali
- Sessioni DeepSeek via OpenRouter (prima dello switch a api.deepseek.com): costo reale (qualche centesimo)
- Sessioni DeepSeek via api.deepseek.com (dopo switch): gratuite

## ⚠️ Pitfall — Costi Sonnet persi da token_costs.json

Se il file `token_costs.json` viene resettato (es. durante fix della dashboard), i costi reali delle sessioni Sonnet passate vanno **recuperati manualmente**.

`dashboard_refresh.py` fa tracking incrementale via delta tokens (`last_lpt.txt`), quindi **non recupera automaticamente** sessioni passate non più tracciate.

**Recupero manuale:**
1. Cerca nello storico sessioni (`session_search(query="Sonnet 4 cost")`) il costo reale
2. Dalla sessione Sonnet: `estimated_cost_usd` dà il costo reale da OpenRouter
3. Scrivi manualmente in `token_costs.json` la voce mancante:
   ```json
   "2026-06-14": {
     "cost": 1.70,
     "calls": 8,
     "note": "7 DeepSeek (gratuite) + 1 Sonnet 4 ($1.70 da OpenRouter)"
   }
   ```
4. Rilancia `dashboard_refresh.py` — ora settimana/mese calcoleranno correttamente

**Esempio storico reale (14/06/2026):**
- Sessione Sonnet 4 (08:41-10:30): $1.70 da OpenRouter usage
- 7 chiamate DeepSeek V4 Flash (pomeriggio): gratuite
- Totale giorno: $1.70, 8 chiamate

Se nella cartella `tickets/` ci sono file residui di vecchi formati (es. `current_status.json` + `talent_risk_2026_06_14.json`), il conteggio è sbagliato. Lo script prende il file `.json` più recente e conta TUTTI i file `.json` come ticket separati. Pulire i file vecchi.

## 🛠️ Come Modificare — Dashboard

### Aggiungere una sezione

1. Aggiungere contenitore HTML in `emma-dashboard.html` con id univoco
2. Aggiungere logica `render()` nello `<script>` che legge dal JSON e popola il contenitore
3. Aggiungere chiave nel dict `data = {...}` in `dashboard_refresh.py` con una funzione loader
4. Se la sezione punta a una pagina dettaglio (es. `/fak_dashboard.html`), aggiungere symlink:
   ```bash
   ln -sf ~/.hermes/<cartella>/<file>.html ~/.hermes/<file>.html
   ```
5. Rigenerare e testare:
   ```bash
   python3 ~/.hermes/scripts/dashboard_refresh.py
   curl -s http://localhost:8080/dashboard_data.json | python3 -m json.tool | grep -A5 "fak_comparison"
   ```

### Verificare

```bash
ss -tlnp | grep 8080                          # server attivo?
python3 ~/.hermes/scripts/dashboard_refresh.py # genera JSON
curl -s http://localhost:8080/dashboard_data.json | python3 -m json.tool | head -20
```

### Server IP — Quale macchina?

**Questa skill gira sul server Linux che ospita gli agenti Hermes, NON sull'Acer di Kuki.**

| Macchina | IP | Ruolo |
|----------|-----|-------|
| **Agent Host (Linux)** | `10.205.63.252` (LAN) / `100.78.71.82` (Tailscale) | Ospita il server HTTP 8080 + dashboard |
| **Acer di Kuki** | `192.168.1.102` (LAN) / `100.109.20.72` (Tailscale) | PC Windows aziendale di Kuki, firewall bloccato, niente installazioni |

**Non provare mai a partire il server sull'Acer** — firewall bloccato, no installazioni. Il server sta SEMPRE su questa macchina Linux.

### Riavviare server HTTP (metodo corretto)

```python
# Da execute_code o terminal — MAI usare nohup/&
cd ~/.hermes && python3 -m http.server 8080
```

Oppure via Hermes:
```
terminal(background=true, command="cd ~/.hermes && python3 -m http.server 8080")
```

## 🎫 Ticket — Pagina Dettaglio Leggibile

Il JSON crudo del ticket (`talent_risk_2026_06_14.json`) non è leggibile da browser (viene aperto inline o scaricato come blob illeggibile).

La soluzione è **`ticket-view.html`** — pagina HTML dedicata, stesso stile dashboard, che:
1. Elenca la directory `/tickets/` per trovare il file `.json` più recente
2. Lo carica e renderizza in sezioni: Informazioni, Attività, Deliverable, Note
3. Offre link "Scarica JSON" per il download raw (via `<a download>`)
4. Link "Torna alla dashboard"

File: `~/.hermes/ticket-view.html`
Accesso: `http://100.78.71.82:8080/ticket-view.html` (via Tailscale) o tramite Cloudflare Tunnel

Nella dashboard, il widget Ticket ha due link sotto la card:
- `🔍 Vedi dettaglio ticket` → apre `ticket-view.html` in nuova tab
- `⬇️ Scarica JSON` → funzione JavaScript `downloadTicket()` che:
  1. Fetch `/tickets/` per trovare il file `.json`
  2. Fetch il file come `blob`
  3. Crea `<a download>` con `URL.createObjectURL(blob)`
  4. Triggera click, poi pulisce

**IMPORTANTE:** Non usare `Content-Disposition: attachment` — Python http.server non lo supporta senza modifiche. Usa sempre il blob + download attribute lato client.

## ✅ Verifica

- Ricarica la pagina nel browser
- Controlla che console non mostri errori 404 sul JSON
- Il timestamp dei dati (`dati: 14/06/2026 HH:MM`) deve essere recente (max 5 min)
- I costi DeepSeek devono mostrare "gratuito" non un numero stimato

## ⚠️ Limiti

- **NON modificare** config.yaml, .env o SOUL.md con questa skill
- **NON cancellare** il file dashboard senza autorizzazione di Kuki
- **NON inventare dati** — se manca una fonte, mostra "N/D" o "nessun dato"
