diff --git a/admin/ia/stt.md b/admin/ia/stt.md index 3dffa00..aa26edc 100644 --- a/admin/ia/stt.md +++ b/admin/ia/stt.md @@ -214,6 +214,7 @@ au prompt système (`STT_DISABLE_THINKING`, défaut `true` ; inoffensif hors Qwe | — UI/HUD | `stt/client/stt/ui/` + `hud/` | HTTP statique + websocket bidirectionnel (états → HUD ; réglages **et messages texte** → backend) ; HUD embarqué dans le package | | — portail | `stt/client/stt/portal/` | `registry.py` (services config-driven `[[services]]`) + `health.py` (StatusPoller). Panneau « Services » du HUD : tuiles + **pages détail** (description, **santé live**, composants, alertes, **aperçu enrichi**, bouton Ouvrir). Santé = probe HTTP + **Prometheus** `up{}`/kube-state-metrics + **Alertmanager**. Ouvre l'URL dans le navigateur normal | | — aperçu enrichi (≥ 0.18.0) | `hud/index.html` (`renderServiceData`) + `ui/app.py` (`service-detail`) | Aperçu live spécifique au service dans sa page détail : **Ghostfolio** → valeur + performance + positions (alloc % et P/L par ligne, via `POST /v1/portfolio/details` du serveur, jeton client) ; **Alertmanager** → **toutes** les alertes actives (rendu HUD à partir de `all_alerts` poussé par le poller, auto-rafraîchi). Extensible : `DETAIL_PROVIDERS` (`server` = requête backend \| `alerts` = liste locale) | +| — page détail pleine page + métriques (≥ 0.19.0) | `hud/index.html` (`renderMetrics`, `is-detail`) + `registry.Metric` + `health.py` | La page détail passe en **pleine page** (le drawer prend tout l'écran, 2 colonnes sur grand écran : données ⟂ état). Section **Métriques** par service (config `[[services.metrics]]` → `{label, prom, unit, hide_if_empty}`) évaluée par le `StatusPoller` : redémarrages, uptime (kube-state-metrics) + mémoire, CPU (cAdvisor, masqués si absents). Défauts via `config._pod_metrics(ns, pod_re)`. Formatage HUD par unité (`duration`/`bytes`/`cpu`/`count`) | | — intentions vocales | `stt/client/stt/portal/intents.py` + `ghostfolio.py` | lecture seule, court-circuitent le LLM (interceptées dans `engine._respond`) : « ouvre <service> », « état du cluster/services » (résumé santé parlé), « combien sur mon ghostfolio » (API Ghostfolio, `[ghostfolio] access_token`) | | — contexte HUD | `stt/client/stt/ui/app.py` + `hud/` | sélecteur de contexte (Réglages) envoyé par requête + **visualiseur** (drawer) affichant le contexte assemblé renvoyé par le serveur ; affichage de la version installée | | STT-server | `stt/server/` (conteneur) | FastAPI : `/healthz`, `/v1/ask` ; `brain.py` → LiteLLM | diff --git a/stt/client/pyproject.toml b/stt/client/pyproject.toml index 830ea2a..72dbe84 100644 --- a/stt/client/pyproject.toml +++ b/stt/client/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "stt" -version = "0.18.0" +version = "0.19.0" description = "STT — client vocal Jarvis du homelab Funk (voix + HUD, parle au STT-server)" requires-python = ">=3.11" readme = "README.md" diff --git a/stt/client/stt/config.py b/stt/client/stt/config.py index 586624b..3584377 100644 --- a/stt/client/stt/config.py +++ b/stt/client/stt/config.py @@ -29,6 +29,29 @@ def resolve_model(name: str) -> str: """Nom court → alias LiteLLM (ou renvoie tel quel si déjà un alias).""" return MODEL_PRESETS.get(name, name) + +def _pod_metrics(ns: str, pod_re: str) -> list[dict]: + """Jeu de métriques standard pour les pods d'un service (page détail HUD). + + Redémarrages + uptime : kube-state-metrics (toujours scrappé). Mémoire + CPU : + cAdvisor/kubelet (`hide_if_empty` → la carte disparaît si non disponible). Les + requêtes s'agrègent à une seule valeur (sum/max) → le poller prend la 1re. + """ + sel = f'namespace="{ns}",pod=~"{pod_re}"' + csel = f'namespace="{ns}",pod=~"{pod_re}",container!="",container!="POD"' + return [ + {"label": "Redémarrages", + "prom": f"sum(kube_pod_container_status_restarts_total{{{sel}}})", "unit": "count"}, + {"label": "Démarré depuis", + "prom": f"time()-max(kube_pod_start_time{{{sel}}})", "unit": "duration"}, + {"label": "Mémoire", + "prom": f"sum(container_memory_working_set_bytes{{{csel}}})", + "unit": "bytes", "hide_if_empty": True}, + {"label": "CPU", + "prom": f"sum(rate(container_cpu_usage_seconds_total{{{csel}}}[5m]))", + "unit": "cpu", "hide_if_empty": True}, + ] + DEFAULT_CONFIG: dict[str, Any] = { "server": { # STT-server (in-cluster) — joignable via Traefik / wildcard *.lab.local @@ -109,6 +132,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Suivi de portefeuille et d'investissements (actions, crypto). " "Base PostgreSQL sur storage-01 + cache Redis in-cluster.", "health_url": "http://ghostfolio.lab.local/api/v1/health", + "metrics": _pod_metrics("ai", "ghostfolio-.+"), "components": [ {"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"ghostfolio-.+",pod!~"ghostfolio-redis-.+",condition="true"}'}, {"name": "Redis (cache)", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"ghostfolio-redis-.+",condition="true"}'}, @@ -123,6 +147,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Tableaux de bord et visualisation des métriques du homelab " "(Prometheus comme source de données).", "health_url": "http://grafana.lab.local/api/health", + "metrics": _pod_metrics("monitoring", ".*grafana.*"), "components": [ {"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~".*grafana.*",condition="true"}'}, ], @@ -135,6 +160,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Déploiement continu GitOps : synchronise les workloads k8s " "depuis le dépôt Git (pattern App-of-Apps).", "health_url": "http://argocd.lab.local/healthz", + "metrics": _pod_metrics("argocd", ".+"), "components": [ {"name": "Pods (server, repo, redis…)", "prom": 'kube_pod_status_ready{namespace="argocd",condition="true"}'}, ], @@ -147,6 +173,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Automatisation et workflows (alertes, rapport horaire, worker " "doc Hermès). Base PostgreSQL sur storage-01.", "health_url": "http://n8n.lab.local/healthz", + "metrics": _pod_metrics("ai", "n8n-.+"), "components": [ {"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"n8n-.+",condition="true"}'}, {"name": "PostgreSQL — storage-01", "prom": 'up{job="storage-01"}'}, @@ -160,6 +187,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Interface de chat web vers les LLM du homelab " "(backend LiteLLM sur storage-01).", "health_url": "http://openwebui.lab.local/health", + "metrics": _pod_metrics("ai", "open-webui-.+"), "components": [ {"name": "Application", "prom": 'kube_pod_status_ready{namespace="ai",pod=~"open-webui-.+",condition="true"}'}, {"name": "LiteLLM — storage-01", "prom": 'up{job="storage-01"}'}, @@ -173,6 +201,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Collecte et stockage des métriques (TSDB) — scrute hôtes, " "nœuds k8s et services.", "health_url": "http://prometheus.lab.local/-/healthy", + "metrics": _pod_metrics("monitoring", "prometheus-.+"), "components": [ {"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~"prometheus-.+",condition="true"}'}, ], @@ -185,6 +214,7 @@ DEFAULT_CONFIG: dict[str, Any] = { "description": "Routage et notification des alertes Prometheus " "(→ webhook Hermès, n8n, e-mail).", "health_url": "http://alertmanager.lab.local/-/healthy", + "metrics": _pod_metrics("monitoring", "alertmanager-.+"), "components": [ {"name": "Pod", "prom": 'kube_pod_status_ready{namespace="monitoring",pod=~"alertmanager-.+",condition="true"}'}, ], diff --git a/stt/client/stt/hud/index.html b/stt/client/stt/hud/index.html index f3f2128..58211d6 100644 --- a/stt/client/stt/hud/index.html +++ b/stt/client/stt/hud/index.html @@ -780,6 +780,27 @@ .svc-data-row .svc-data-val[data-tone="bad"] { color: #d23a52; } .svc-data-row .svc-data-val[data-tone="warn"] { color: #c5841f; } .svc-data-empty { font-size: 13px; color: var(--ink-soft); margin: 4px 0 0; } + .svc-stat .svc-stat-sub { display: block; font-size: 11px; color: var(--ink-soft); margin-top: 2px; font-weight: 500; } + + /* ============================================================ + PORTAIL — page détail PLEINE PAGE (le drawer prend tout l'écran) + ============================================================ */ + #servicesDrawer.is-detail { width: 100vw; max-width: 100vw; } + #servicesDrawer.is-detail .body { padding: 0; } + /* contenu centré et large, façon « vraie page » */ + #svcDetail { max-width: 1040px; margin: 0 auto; padding: 18px 28px 40px; } + /* 2 colonnes sur grand écran : données à gauche, état/actions à droite */ + .svc-detail-grid { display: block; } + @media (min-width: 820px) { + .svc-detail-grid { + display: grid; grid-template-columns: 1.4fr 1fr; + gap: 0 40px; align-items: start; + } + .svc-col-side .svc-section:first-child { padding-top: 0; } + .svc-col-side .svc-section:first-child .flabel { margin-top: 0; } + } + /* en pleine page, les cartes de stats peuvent s'étaler davantage */ + #servicesDrawer.is-detail .svc-stat { flex-basis: 150px; } /* Badge alertes global (en tête de la liste) */ .svc-alert-badge { @@ -1120,31 +1141,44 @@
- -