# PRD — Hermes Copilot Dashboard

**Auteur :** Aramis Bégnène
**Date :** 4 septembre 2026
**Statut :** V1 – spécification complète, prête à implémenter
**Type :** Application personnelle mono-utilisateur, en lecture seule

---

## 1. Objectif

Aramis pilote toute son activité via Hermes Agent : sessions interactives (TUI/Telegram), webhooks, crons (sourcing MyFlip, planification, supervision n8n). Chaque tâche consomme des tokens de son abonnement Ollama Cloud Pro (20 €/mois). Aujourd'hui, pour savoir où il en est, il doit ouvrir Safari, se connecter au portail Ollama et lire les jauges à la main — et il n'a **aucune vue sur ce que Hermes a réellement fait** : quelles tâches ont tourné, avec quel modèle, si ça a réussi, ce que ça a coûté.

Le problème n'est pas l'absence de données : Hermes les enregistre toutes en local (`state.db`, `usage_audit.jsonl`). Le problème est qu'elles sont invisibles. **Ce dashboard garantit qu'Aramis voit, sur une seule page, l'état de son quota Ollama Cloud et l'activité de ses agents Hermes — sans jamais avoir à ouvrir le portail Ollama ni fouiller les logs.**

### Ce que ça fait
- Affiche les deux jauges de quota Ollama Cloud — session (~4h) et hebdomadaire — reconstituées depuis les données Hermes, calibrées sur la réalité Ollama
- Liste tous les crons programmés : nom, horaire de départ, prochain déclenchement, dernier statut, canal de livraison
- Liste l'historique des tâches/sessions : titre, source, modèle, tokens, coût, succès/échec, type de problème rencontré
- Agrège les dépenses par modèle et par période

## 2. Scope

**Dans la V1 :**
- Interface web unique, en lecture seule, générée en local sur le VPS
- Bloc « Quota Ollama Cloud » : deux jauges (session glissante 4h, semaine lundi→dimanche), répartition des tokens par modèle, compteur de requêtes par modèle
- Bloc « Crons » : tous les crons Hermes actifs/paussés, avec horaires et statuts
- Bloc « Sessions / Tâches » : historique paginé ou filtrable par source (tui / telegram / webhook / cron), avec modèle, tokens, coût estimé, statut, signaux de problème
- Bloc « Dépenses » : total par période (jour/semaine/mois), par modèle ; coût = 0 € pour Ollama Cloud (abonnement) mais tokens et équivalents affichés
- Données rafraîchies à chaque chargement de page (extraction à la volée depuis les sources locales)

**Explicitement hors scope (pour plus tard) :**
- ~~Scraping exact du portail Ollama~~ — **fait en V1.1 (04/09/2026)** : découverte de l'endpoint officiel `https://ollama.com/api/usage` (auth Bearer `OLLAMA_API_KEY`, lue depuis `/opt/data/.env`) qui expose les vraies jauges session/semaine et les requêtes par modèle. Intégré dans `extract.py` comme source primaire ; le compteur maison calibré reste en fallback si l'API est injoignable
- Résumés IA des problèmes rencontrés sur une tâche (via un modèle peu coûteux type DeepSeek) — la V1 montre les signaux techniques, la synthèse qualitative viendra ensuite
- Historique OpenRouter / Gemini (arrêtés par Aramis, hors de l'abonnement actif) — les données existent dans la base mais ne sont pas affichées
- Actions depuis le dashboard (pause/reprise de cron, etc.) : V1 = lecture seule, pas de bouton qui modifie
- Authentification : URL devserve du VPS (HTTPS, domaine privé) fait office de contrôle d'accès pour V1

## 3. Utilisateurs & flux principaux

Un seul utilisateur : Aramis, depuis Safari sur son MacBook.

**Parcours principal :**
1. Aramis ouvre l'URL du dashboard (exposée via devserve sur le VPS, même mécanisme que le dashboard CDL)
2. Il lit en haut de page : les deux jauges de quota Ollama (suis-je à l'aise pour lancer une grosse tâche ?)
3. Il parcourt les crons : tout part bien demain ? un cron est-il en erreur (comme le fut « Planification semaine », silencieusement mort pendant des jours) ?
4. Il parcourt les dernières sessions : la tâche de suivi colis d'hier a-t-elle réussi ? sur quel modèle ? combien de tokens ?
5. Il referme. La page entière se comprend en moins d'une minute.

**Flux secondaire — diagnostic :** en cas de souci signalé par un cron (ex : erreur de livraison Telegram), Aramis retrouve la session correspondante dans l'historique et identifie le type d'échec.

## 4. Fonctionnalités & règles métier détaillées

### 4.1 Bloc « Quota Ollama Cloud »

Le portail Ollama n'expose pas ces valeurs par API — elles sont **reconstituées depuis les données locales Hermes** (`session_model_usage`, filtré sur `billing_provider = 'ollama-cloud'`).

**Jauge session (fenêtre glissante 4h) :**
- Somme des tokens (input + output + cache_read) consommés sur les 4 dernières heures, divisée par le quota de session
- Quota de session calibré : **5 240 000 tokens** (= quota hebdo × 4h / 168h). Le calibrage repose sur la capture du 04/09 (48,2% hebdo ≈ 106M tokens ⇒ quota hebdo ≈ 220M) ; le quota de session est la part hebdo ramenée à une fenêtre de 4h — formule et valeur doivent rester cohérentes, la formule fait foi
- Affichage : pourcentage + barre de progression + « consommé sur les 4 dernières heures » (fenêtre glissante — pas de « reset dans Xh » affiché : le compteur glisse en continu, un compte à rebours serait mensonger sans scraping du portail)

**Jauge hebdomadaire (lundi → dimanche, heure de Paris) :**
- Somme des tokens consommés depuis lundi 00:00 heure de Paris, divisée par le quota hebdo calibré (220M)
- Affichage : pourcentage + barre segmentée par modèle + « se réinitialise [jour] »

**Seuils de couleur (les deux jauges) :**
- Vert : < 60% · Orange : 60–85% · Rouge : > 85%
- Au rouge : message d'alerte discret sous la jauge (« quota critique — envisager glm-5.3-flash pour les tâches lourdes »)

**Répartition par modèle :** liste des modèles consommés sur la période affichée avec tokens et nombre de requêtes, triée par consommation décroissante (miroir de la section « Models used this week » du portail Ollama).

**Calibrage :** un fichier de configuration JSON centralise les constantes de calibrage (quotas session/semaine, fenêtres, seuils) pour les retoucher sans toucher le code.

### 4.2 Bloc « Crons »

Source : `cron/jobs.json` (complété par `cron/list` en exécution).

Pour chaque cron, afficher :
- **Nom** et job_id (court)
- **Horaire de départ** : expression cron traduite en langage clair (« tous les jours à 10:00 ») + **prochain déclenchement** en heure de Paris
- **Modèle** : modèle et provider utilisés (ou « script seul, sans LLM » pour les crons no_agent)
- **Dernier statut** : ok / erreur / silencieux (badge vert/rouge/orange)
- **Canal de livraison** : Telegram / origine / silencieux
- **Historique compact** : pour les crons LLM, nombre de runs depuis 7 jours, taux de succès, tokens moyens par run (source : `usage_audit.jsonl`)

Badge orange « silencieux » si un cron actif n'a pas tourné depuis plus de 1,5× son intervalle attendu (règle du cron « Planification semaine » mort pendant 5 jours : le dashboard doit faire ce que ni Telegram ni les logs n'ont fait — **le rendre visible immédiatement**).

### 4.3 Bloc « Sessions / Tâches »

Source : `state.db`, tables `sessions` (join `session_model_usage` agrégée), filtre `billing_provider = 'ollama-cloud'` (V1 ; les autres providers existent en base mais sont filtrés).

Chaque ligne :
- **Titre** (généré par Hermes) + date/heure (Paris)
- **Source** : tui / telegram / webhook / cron — badge coloré
- **Modèle(s)** : modèle principal (celui de la session)
- **Consommation** : tokens (in/out/cache séparés en tooltip, total visible) + nombre d'appels API
- **Coût** : `estimated_cost_usd` (≈ 0 pour Ollama Cloud) ; l'info utile est le volume en tokens — le coût en € n'est pas facturé à l'usage
- **Statut** : réussi (vert) / terminé avec avertissements (orange) / échoué (rouge)
- **Problèmes rencontrés** : liste de signaux techniques dérivés de l'historique de la session (V1, pas d'analyse IA) :
  - `finish_reason` ≠ `stop` et ≠ `tool_calls` sur les messages assistant (table `messages`)
  - appels d'outil en erreur (`messages.effect_disposition = 'error'`)
  - erreurs enregistrées sur la session : `compression_failure_error`, `handoff_error`, `end_reason` anormal (`error`, `crash`)
  - dérive de modèle (modèle observé ≠ modèle configuré — le bug du cron planification de fin août)
  - délai d'exécution anormal : > 5 min pour une session interactive, > 15 min pour un cron
  
  (Écart PRD initial : `delivery_obligations` et `request_dump_*.json` retirés — colonnes/fichiers non stabilisés dans le schéma vérifié ; réintégrables en V1.1 si besoin.)
- Filtres : par source, par statut, par période (7/30 jours). Pagination ou scroll virtuel au-delà de 50 lignes.

### 4.4 Bloc « Dépenses »

- Abonnement Ollama Cloud Pro : **20 €/mois, facturé fixe** — affiché comme coût de revient forfaitaire du mois (pas de pay-per-token)
- Volume consommé par période : jour / semaine / mois, en tokens et en équivalent « % de quota hebdo »
- Répartition par modèle (barres) : qui consomme quoi — glm-5.3 est-il le plus gourmand ?
- Tendance simple : barres quotidiennes des 14 derniers jours (tokens/jour)
- Pas d'extra usage pour l'instant : balance Ollama = 0 $, auto-reload désactivé (visible sur le portail ; le dashboard peut l'afficher en note fixe en V1)

## 4.5 Architecture d'exécution et fraîcheur des données

**Contrainte de départ : devserve ne sert que des fichiers statiques** (http.server simple, aucun mécanisme d'exécution par requête — ni CGI, ni Flask, ni watcher). L'architecture s'y plie explicitement :

- **Deux artefacts servis :** `index.html` (front autonome) et `data.json` (extraction du moment), générés côte à côte dans le dossier servi
- **`index.html` charge `data.json` via fetch** ; si le fetch échoue (extraction pas encore lancée, base momentanément verrouillée), il réessaie automatiquement et affiche un bandeau « données du [date/heure de generated_at] » en attendant — **le fallback est le précédent `data.json` resté sur disque**, qui sert de dernier snapshot réussi sans aucun process persistant
- **Régénération de `data.json` : cron Hermes dédié** — job `no_agent` (script seul, pas de LLM) qui exécute `extract.py` toutes les 15 minutes. C'est la boucle de fraîcheur : extraction régénérée périodiquement, page statique toujours servie instantanément par devserve, données au maximum 15 minutes de retard — acceptable pour un dashboard de pilotage, et le bouton « Actualiser » du navigateur recharge le dernier snapshot
- **L'extraction n'est jamais dans le chemin de la requête HTTP** : une requête ne déclenche aucun code Python, elle ne lit que des fichiers. Ainsi un lock SQLite au moment d'un hit HTTP n'a aucun effet sur la page

### 4.6 Lecture stricte et sécurité

- `state.db` ouvert **uniquement en mode URI read-only** : `sqlite3.connect("file:/opt/data/state.db?mode=ro", uri=True)` — pas un `PRAGMA query_only` (qui requiert une connexion en écriture), pas une simple absence de requêtes d'écriture : `mode=ro` garantit au niveau du VFS qu'aucune écriture ni création de fichier n'est possible, même en cas de bug du script
- Le dossier d'extraction est jetable : le supprimer intégralement n'entraîne aucune perte de données
- Aucun secret (token, clé) dans la page ou dans `data.json` ; l'URL devserve est le seul contrôle d'accès en V1

## 5. Contraintes techniques

- **Hébergement** : VPS srv1899780 (186.241.16.37), exposé via **devserve** (`scripts/devserve.py`) — réflexe standard d'Aramis : URL HTTPS du type `<nom>.dev.srv1899780.hstgr.cloud`, jamais un chemin de fichier ni une IP:port
- **Stack** : `index.html` (front, HTML + CSS + JS vanilla, aucun build, aucune dépendance npm) + `extract.py` (script Python **stdlib uniquement** — sqlite3, json, time ; pas de croniter ni de lib externe : la traduction cron→humain et le prochain déclenchement sont de la responsabilité du script, en parsant les expressions simples utilisées par les crons réels)
- **Horaires de cron** : le prochain déclenchement n'est **pas recalculé** — `cron/jobs.json` expose déjà `next_run_at` et `last_run_at` en ISO UTC ; le script se borne à les convertir en heure de Paris (fuseau fixe Europe/Paris, CEST +2h en septembre, CET +1h en hiver — la bascule est un point d'attention mineur, pas un bloqueur)
- **Extraction** : `extract.py` lit `state.db` en mode read-only (cf. 4.6), `cron/jobs.json`, `cron/usage_audit.jsonl`, et écrit `data.json` dans le dossier servi. Aucune écriture ailleurs que dans le dossier du dashboard
- **Aucune écriture dans Hermes** : le dashboard ne modifie jamais `state.db`, `jobs.json`, ni aucun réglage Hermes
- **Fuseau** : toutes les heures affichées en Europe/Paris
- **Sécurité** : pas de secret dans la page ni dans data.json (pas de token API) ; l'URL devserve est le seul contrôle d'accès
- **Performance** : chargement < 2s avec ~150 sessions et ~16k messages ; extraction SQLite en requêtes agrégées, pas de scan message par message
- **Navigation** : fonctionne bien sur Safari desktop ; pas de responsive mobile exigé en V1

## 6. Modèle de données

Pas de nouveau modèle : le dashboard lit les sources existantes de Hermes.

| Donnée | Source | Champs utilisés |
|---|---|---|
| Sessions | `state.db` · `sessions` | id, title, source, model, started_at, ended_at, end_reason, message_count, api_call_count, input_tokens, output_tokens, cache_read_tokens, billing_provider, estimated_cost_usd, compression_failure_error, handoff_error, last_activity_at |
| Usage par modèle | `state.db` · `session_model_usage` | session_id, model, tokens (input/output/cache_read), api_call_count, estimated_cost_usd, first_seen/last_seen |
| Signaux d'erreur | `state.db` · `messages.finish_reason`, `effect_disposition` | finish_reason anormal (≠ `stop` et ≠ `tool_calls`), tool_results en erreur (`effect_disposition = 'error'`) |
| Crons | `cron/jobs.json` | name, schedule, next_run_at, last_run_at, last_status, deliver, model, no_agent, script, enabled |
| Historique crons | `cron/usage_audit.jsonl` | ts, job_id, total_tokens, model, duration_ms, error |
| Calibrage quota | `config/dashboard-quotas.json` (nouveau, lu seulement) | weekly_allowance_tokens, session_allowance_tokens, session_window_hours, seuils |

**Colonne `billing_provider` : présente sur les DEUX tables** (`sessions` et `session_model_usage` — schéma réel en annexe A). Le filtrage Ollama se fait sur `session_model_usage.billing_provider` pour toutes les agrégations de tokens (source de vérité de la consommation), et `sessions.billing_provider` sert uniquement au tri/affichage par ligne de session. Les quotas (4.1) et les dépenses (4.4) s'appuient exclusivement sur `session_model_usage` — pas de JOIN `sessions` nécessaire pour les jauges.

**Modèle affiché par session :** la colonne `sessions.model` est le modèle **principal** (configuré en début de session) — c'est lui qu'on affiche. Si `session_model_usage` révèle des modèles additionnels (bascule en cours de route), le ou les modèles secondaires s'affichent en secondaire dans la ligne (badge grisé), sans remplacer le principal. Priorité d'affichage : modèle principal, puis secondaires triés par tokens décroissants.

## Annexe A — Schéma réel (dump du 04/09/2026, tables utilisées)

```
sessions:
  id TEXT, source TEXT, user_id TEXT, session_key TEXT, chat_id TEXT, chat_type TEXT,
  thread_id TEXT, display_name TEXT, origin_json TEXT, expiry_finalized INTEGER,
  model TEXT, model_config TEXT, system_prompt TEXT, system_prompt_hash TEXT,
  parent_session_id TEXT, started_at REAL, ended_at REAL, end_reason TEXT,
  message_count INTEGER, tool_call_count INTEGER, input_tokens INTEGER,
  output_tokens INTEGER, cache_read_tokens INTEGER, cache_write_tokens INTEGER,
  reasoning_tokens INTEGER, cwd TEXT, git_branch TEXT, git_repo_root TEXT,
  billing_provider TEXT, billing_base_url TEXT, billing_mode TEXT,
  estimated_cost_usd REAL, actual_cost_usd REAL, cost_status TEXT, cost_source TEXT,
  pricing_version TEXT, title TEXT, last_activity_at REAL,
  last_activity_description TEXT, last_activity_provenance TEXT,
  api_call_count INTEGER, handoff_state TEXT, handoff_platform TEXT,
  handoff_error TEXT, compression_failure_cooldown_until REAL,
  compression_failure_error TEXT, compression_fallback_streak INTEGER,
  compression_ineffective_count INTEGER, profile_name TEXT, rewind_count INTEGER,
  archived INTEGER, pinned INTEGER, title_source TEXT, last_read_at REAL

session_model_usage:
  session_id TEXT, model TEXT, billing_provider TEXT, billing_base_url TEXT,
  billing_mode TEXT, task TEXT, api_call_count INTEGER, input_tokens INTEGER,
  output_tokens INTEGER, cache_read_tokens INTEGER, cache_write_tokens INTEGER,
  reasoning_tokens INTEGER, estimated_cost_usd REAL, actual_cost_usd REAL,
  cost_status TEXT, cost_source TEXT, first_seen REAL, last_seen REAL

messages (colonnes utilisées):
  id INTEGER, session_id TEXT, role TEXT, effect_disposition TEXT, timestamp REAL,
  token_count INTEGER, finish_reason TEXT, observed INTEGER, active INTEGER, compacted INTEGER
```

Le script d'implémentation doit écrire son SQL strictement contre ce schéma — si une colonne manque, **corriger le PRD ou le schéma, jamais inventer un nom de colonne**.

## 7. Critères d'acceptation

1. **Jauges calibrées** : la jauge hebdo affiche 48% (±3 points) en comparaison de la capture Ollama du 04/09 au soir (48,2%) ; la jauge session est cohérente : sa valeur = tokens 4h glissantes ÷ 5,24M, et le calcul est reproductible en relançant `extract.py` (la formule, pas une valeur codée en dur, fait foi)
2. **Tous les crons visibles** : les 4 crons actifs (rappel architecture, supervision n8n, planification semaine, suivi colis) apparaissent avec leur horaire exact, leur prochain déclenchement en heure de Paris et leur dernier statut réel
3. **Un cron silencieux se voit** : en simulant un cron sans run récent, le badge orange apparaît (règle 1,5× intervalle)
4. **Historique exploitable** : les 30 dernières sessions s'affichent en < 2s, avec pour chacune modèle, tokens, statut et signaux de problème détectés
5. **Détail d'échec** : le cron « Planification semaine » (en erreur au 30/08, réparé) apparaît dans l'historique avec son statut d'échec et le signal « erreur » visible
6. **Lecture seule prouvée** : `state.db` ouvert via `sqlite3.connect("file:...?mode=ro", uri=True)` — le test est : exécuter `extract.py` avec la base rendue non inscriptible au niveau fichier (chmod) ; il doit réussir intégralement, ce qui prouve le mode URI read-only (un `PRAGMA query_only` ou une simple absence de writes échoueraient ou n'auraient aucune garantie)
7. **Lisible en un coup d'œil** : page unique, sans scroll horizontal, hiérarchie claire — Aramis valide visuellement (il ajuste en direct, il préfère voir un exemple avant d'intégrer)
8. **Aucune dépendance** : aucun `npm install`, aucun build — l'URL devserve sert la page telle quelle
9. **Zéro secret** : grep sur les fichiers générés → aucun token, aucune clé, aucun secret
10. **URL livrée** : dashboard accessible sur `https://hermes-dashboard.dev.srv1899780.hstgr.cloud` (ou équivalent devserve) depuis Safari

---

## Historique du calibrage (annexe)

- Capture Ollama du 04/09 23:38 Paris : session 1,5% (reset 3h), semaine 48,2% (reset 2 jours), 1 772 requêtes, extra balance 0$, auto-reload off, plan Pro
- Compteur Hermes depuis lundi 01/09 : 106,0M tokens, 1 634 appels — **calibrage validé** (écart 8% avec Ollama, expliqué par le web search natif Ollama et appels hors Hermes)
- Quota hebdo calibré : **~220M tokens** ; à re-rétalonner après scraping (évolution future)