mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-09 22:14:43 +02:00
Les pages détail du portail HUD affichent désormais un aperçu live des données du service, pas seulement santé + lien : - Ghostfolio → portefeuille complet : valeur, performance globale, positions (allocation % + P/L par ligne, coloré). Nouvel endpoint serveur POST /v1/portfolio/details (jeton client, source unique réutilisée). - Alertmanager → TOUTES les alertes actives du homelab (total + critiques + liste avec sévérité), rendu côté HUD depuis `all_alerts` poussé par le StatusPoller (auto-rafraîchi, aucun appel supplémentaire). Mécanisme extensible (`DETAIL_PROVIDERS` : `server` = requête backend | `alerts` = liste locale). Cartes de stats + lignes de données génériques. Client 0.18.0. Vérifié via Playwright (rendu, tons, normalisation des %). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
4.8 KiB
Python
133 lines
4.8 KiB
Python
"""Client HTTP vers le STT-server (in-cluster).
|
|
|
|
Le client n'a plus de cerveau : il envoie le texte transcrit au serveur, qui
|
|
orchestre l'inférence (LiteLLM → Qwen3/Claude) et renvoie la réponse. Le client
|
|
choisit le modèle (alias LiteLLM) et le passe au serveur par requête.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
from typing import Any
|
|
|
|
|
|
class ServerClient:
|
|
def __init__(self, cfg: dict[str, Any]):
|
|
self.url = cfg["url"].rstrip("/")
|
|
self.timeout = cfg.get("timeout_sec", 90)
|
|
self.model = cfg.get("model", "hermes-default")
|
|
# contexte présélectionné (None = défaut serveur). Piloté par le HUD.
|
|
self.context = cfg.get("context")
|
|
# dernier contexte assemblé renvoyé par le serveur (pour le visualiseur)
|
|
self.last_context: dict | None = None
|
|
self.last_context_id: str | None = None
|
|
# jetons à transmettre au serveur (ex. {"ghostfolio_token": …}) — source UNIQUE
|
|
# du jeton, côté client ; le serveur les consomme (contexte + /v1/portfolio).
|
|
self.secrets: dict = {}
|
|
# une session par run → mémoire court-terme côté serveur
|
|
self.session_id = uuid.uuid4().hex
|
|
|
|
def ask(self, text: str, model: str | None = None) -> str:
|
|
import requests
|
|
|
|
payload = {
|
|
"text": text,
|
|
"model": model or self.model,
|
|
"session_id": self.session_id,
|
|
}
|
|
if self.context:
|
|
payload["context"] = self.context
|
|
if self.secrets:
|
|
payload["secrets"] = self.secrets
|
|
r = requests.post(f"{self.url}/v1/ask", json=payload, timeout=self.timeout)
|
|
if r.status_code == 400:
|
|
raise RuntimeError(r.json().get("detail", "requête invalide"))
|
|
r.raise_for_status()
|
|
data = r.json()
|
|
# contexte assemblé (système + sources live + RAG + mémoire) → visualiseur HUD
|
|
self.last_context = data.get("context")
|
|
self.last_context_id = data.get("context_id")
|
|
return data["reply"].strip()
|
|
|
|
def portfolio(self) -> str:
|
|
"""Valeur du portefeuille Ghostfolio via le serveur (jeton client transmis).
|
|
|
|
Unifie le jeton : c'est le serveur qui parle à Ghostfolio. Dégrade en message
|
|
clair si le serveur est antérieur (pas d'endpoint) ou injoignable.
|
|
"""
|
|
import requests
|
|
|
|
try:
|
|
r = requests.post(
|
|
f"{self.url}/v1/portfolio",
|
|
json={"token": self.secrets.get("ghostfolio_token")},
|
|
timeout=self.timeout,
|
|
)
|
|
r.raise_for_status()
|
|
return (r.json().get("summary") or "").strip() or "Pas de réponse de Ghostfolio."
|
|
except requests.RequestException:
|
|
return "Je n'arrive pas à joindre le serveur pour Ghostfolio."
|
|
|
|
def portfolio_details(self) -> dict:
|
|
"""Aperçu structuré du portefeuille (page détail du portail HUD).
|
|
|
|
{value, currency, performance_pct, holdings:[…]} ou {error: …}. Dégrade en
|
|
clair si le serveur est antérieur (pas d'endpoint) ou injoignable.
|
|
"""
|
|
import requests
|
|
|
|
try:
|
|
r = requests.post(
|
|
f"{self.url}/v1/portfolio/details",
|
|
json={"token": self.secrets.get("ghostfolio_token")},
|
|
timeout=self.timeout,
|
|
)
|
|
if r.status_code == 404:
|
|
return {"error": "Aperçu indisponible (STT-server antérieur)."}
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except requests.RequestException:
|
|
return {"error": "Je n'arrive pas à joindre le serveur pour Ghostfolio."}
|
|
|
|
def contexts(self) -> dict | None:
|
|
"""{'default':…, 'contexts':[{id,label,icon,description}]} ; None si indisponible.
|
|
|
|
Dégrade en None sur un serveur antérieur (endpoint absent) → pas de sélecteur.
|
|
"""
|
|
import requests
|
|
|
|
try:
|
|
r = requests.get(f"{self.url}/v1/contexts", timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except requests.RequestException:
|
|
return None
|
|
|
|
def reset(self) -> None:
|
|
import requests
|
|
|
|
try:
|
|
requests.post(
|
|
f"{self.url}/v1/reset",
|
|
json={"text": "", "session_id": self.session_id},
|
|
timeout=10,
|
|
)
|
|
except requests.RequestException:
|
|
pass
|
|
|
|
def models(self) -> dict:
|
|
"""{'default': ..., 'available': [...]} depuis le serveur."""
|
|
import requests
|
|
|
|
r = requests.get(f"{self.url}/v1/models", timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
|
|
def healthy(self) -> bool:
|
|
import requests
|
|
|
|
try:
|
|
r = requests.get(f"{self.url}/healthz", timeout=5)
|
|
return r.ok
|
|
except requests.RequestException:
|
|
return False
|