Consolidation de Domo91

This commit is contained in:
2025-12-14 15:55:15 +01:00
parent 0c3457f30a
commit a0b6d22727
5 changed files with 773 additions and 695 deletions

17
.env
View File

@@ -6,21 +6,12 @@ DB_NAME=Sondes
AUTH_USERS=[{"user":"Michel","pass":"210462"}] AUTH_USERS=[{"user":"Michel","pass":"210462"}]
# MQTT Saclay # MQTT
MQTT_HOST=54.36.188.119 MQTT_HOST=162.19.78.131
MQTT_USER=Bwps MQTT_USER=sondes
MQTT_PASS=scJ5ACj2keRfI^ MQTT_PASS=3J@bjYP0
# --- MQTT Meudon ---
MQTT_HOST_MEUDON=162.19.78.131
MQTT_USER_MEUDON=sondes
MQTT_PASS_MEUDON=3J@bjYP0
MQTT_PORT_MEUDON=1883 MQTT_PORT_MEUDON=1883
# Topic gyrophare Meudon
GYRO_MQTT_TOPIC_MEUDON=Meudon/gyrophare
# Boucle rapide du gyro # Boucle rapide du gyro
GYRO_MODE=mqtt GYRO_MODE=mqtt
GYRO_CHECK_SEC=20 GYRO_CHECK_SEC=20

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,11 @@ DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS") DB_PASS = os.getenv("DB_PASS")
DB_NAME = os.getenv("DB_NAME") DB_NAME = os.getenv("DB_NAME")
# --- MQTT Meudon --- # --- MQTT ---
MQTT_HOST = os.getenv("MQTT_HOST_MEUDON") MQTT_HOST = os.getenv("MQTT_HOST")
MQTT_USER = os.getenv("MQTT_USER_MEUDON") MQTT_USER = os.getenv("MQTT_USER")
MQTT_PASS = os.getenv("MQTT_PASS_MEUDON") MQTT_PASS = os.getenv("MQTT_PASS")
MQTT_PORT = int(os.getenv("MQTT_PORT_MEUDON", "1883")) MQTT_PORT = int(os.getenv("MQTT_PORT", "1883"))
# Client ID (configurable, sinon suffixé avec le hostname) # Client ID (configurable, sinon suffixé avec le hostname)
MQTT_CLIENT_ID = os.getenv( MQTT_CLIENT_ID = os.getenv(

View File

@@ -26,9 +26,9 @@ DB_USER = os.getenv("DB_USER")
DB_PASS = os.getenv("DB_PASS") DB_PASS = os.getenv("DB_PASS")
DB_NAME = os.getenv("DB_NAME") DB_NAME = os.getenv("DB_NAME")
MQTT_HOST = os.getenv("MQTT_HOST") MQTT_HOST = "54.36.188.119"
MQTT_USER = os.getenv("MQTT_USER") MQTT_USER = "Bwps"
MQTT_PASS = os.getenv("MQTT_PASS") MQTT_PASS = "scJ5ACj2keRfI^"
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883)) MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
GYRO_TOPIC_SACLAY = os.getenv("GYRO_MQTT_TOPIC_SACLAY", "Saclay/gyrophare") GYRO_TOPIC_SACLAY = os.getenv("GYRO_MQTT_TOPIC_SACLAY", "Saclay/gyrophare")

View File

@@ -3,19 +3,16 @@ import os
import random import random
import traceback import traceback
from datetime import datetime, date, time from datetime import datetime, date, time
from contextlib import closing
import bcrypt import bcrypt
import matplotlib.dates as mdates import matplotlib.dates as mdates
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import mysql.connector import mysql.connector
import pandas as pd import pandas as pd
pd.set_option('future.no_silent_downcasting', True) pd.set_option("future.no_silent_downcasting", True)
import streamlit as st import streamlit as st
from contextlib import closing
from dotenv import find_dotenv, load_dotenv from dotenv import find_dotenv, load_dotenv
env_file = find_dotenv(usecwd=True)
if env_file:
load_dotenv(env_file)
from fpdf import FPDF from fpdf import FPDF
# ========================================================= # =========================================================
@@ -28,7 +25,10 @@ st.write("Bienvenue sur lapplication de supervision.")
# ========================================================= # =========================================================
# ENV & DB # ENV & DB
# ========================================================= # =========================================================
load_dotenv() env_file = find_dotenv(usecwd=True)
if env_file:
load_dotenv(env_file)
db_config = { db_config = {
"host": os.getenv("DB_HOST"), "host": os.getenv("DB_HOST"),
"user": os.getenv("DB_USER"), "user": os.getenv("DB_USER"),
@@ -37,15 +37,51 @@ db_config = {
"autocommit": False, "autocommit": False,
} }
SITES_AUTORISES = {"Saclay", "Meudon", "Roissy"} # anti-injection sur noms de tables # Roissy n'existe pas actuellement => on garde Saclay / Meudon
SITES_AUTORISES = {"Saclay", "Meudon"} # anti-injection sur noms de tables
SITES_LISTE = sorted(SITES_AUTORISES)
def get_connection(): def get_connection():
return mysql.connector.connect(**db_config) return mysql.connector.connect(**db_config)
# --- Gyro: lecture + badge (auto) --- def assert_site_ok(site: str):
if site not in SITES_AUTORISES:
raise ValueError(f"Site invalide: {site}")
# =========================================================
# Session state
# =========================================================
for key, default in {
"authenticated": False,
"role": None,
"site_autorise": None,
"onglet_actif": "Accueil",
"selected_date": date.today(),
"selected_site": "Saclay",
"selected_periode": "Toute la journée",
}.items():
st.session_state.setdefault(key, default)
# =========================================================
# Sécurité mots de passe
# =========================================================
def hash_password(plain_password: str) -> str:
return bcrypt.hashpw(plain_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verifier_password(input_password: str, hash_en_base: str) -> bool:
return bcrypt.checkpw(input_password.encode("utf-8"), hash_en_base.encode("utf-8"))
# =========================================================
# Gyro: lecture + badge
# =========================================================
def fetch_gyro(site: str): def fetch_gyro(site: str):
"""Retourne (etat, ts) depuis la vue v_gyro_last pour le site donné.""" """Retourne (etat, ts) depuis la vue v_gyro_last pour le site donné."""
assert_site_ok(site)
q = """ q = """
SELECT Etat, `Date` SELECT Etat, `Date`
FROM Sondes.v_gyro_last FROM Sondes.v_gyro_last
@@ -53,9 +89,7 @@ def fetch_gyro(site: str):
ORDER BY `Date` DESC ORDER BY `Date` DESC
LIMIT 1 LIMIT 1
""" """
cnx = get_connection() with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
try:
cur = cnx.cursor(dictionary=True)
cur.execute(q, (site,)) cur.execute(q, (site,))
row = cur.fetchone() row = cur.fetchone()
if not row: if not row:
@@ -63,36 +97,25 @@ def fetch_gyro(site: str):
etat = (row.get("Etat") or "").strip().upper() etat = (row.get("Etat") or "").strip().upper()
ts = row.get("Date") ts = row.get("Date")
return etat, ts return etat, ts
finally:
try:
cur.close()
except Exception:
pass
try:
cnx.close()
except Exception:
pass
def render_gyro_badge(site: str, stale_after_min: int = 10): def render_gyro_badge(site: str, stale_after_min: int = 10):
"""Affiche un voyant Gyro (vert/rouge/orange) + fraîcheur des données.""" """Affiche un voyant Gyro (vert/rouge/orange) + fraîcheur des données."""
etat, ts = fetch_gyro(site) etat, ts = fetch_gyro(site)
# Etat → couleur/label
if etat in ("ON", "1"): if etat in ("ON", "1"):
color, label = "#ef4444", "GYRO ON" # Rouge = gyro actif color, label = "#ef4444", "GYRO ON"
elif etat in ("OFF", "0"): elif etat in ("OFF", "0"):
color, label = "#22c55e", "GYRO OFF" # Vert = gyro arrêté color, label = "#22c55e", "GYRO OFF"
elif etat in ("ALERTE", "ALARM", "ALARMED"): elif etat in ("ALERTE", "ALARM", "ALARMED"):
color, label = "#f59e0b", "GYRO ALERTE" # Orange = alerte color, label = "#f59e0b", "GYRO ALERTE"
else: else:
color, label = "#9E9E9E", "GYRO INCONNU" color, label = "#9E9E9E", "GYRO INCONNU"
# Fraîcheur
stale = True stale = True
age_txt = "" age_txt = ""
if ts is not None: if ts is not None:
try: try:
# ts provient normalement de MySQL déjà en datetime
from datetime import datetime as _dt from datetime import datetime as _dt
now = _dt.now(ts.tzinfo) if hasattr(ts, "tzinfo") and ts.tzinfo else _dt.now() now = _dt.now(ts.tzinfo) if hasattr(ts, "tzinfo") and ts.tzinfo else _dt.now()
mins = int((now - ts).total_seconds() // 60) mins = int((now - ts).total_seconds() // 60)
@@ -117,37 +140,10 @@ def render_gyro_badge(site: str, stale_after_min: int = 10):
</div> </div>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
def get_conn():
return mysql.connector.connect(**db_config)
# ========================================================= # =========================================================
# Session state # Bootstrap schéma : Journal_Erreurs
# =========================================================
for key, default in {
"authenticated": False,
"role": None,
"site_autorise": None,
"onglet_actif": "Accueil",
"selected_date": date.today(),
"selected_site": "Saclay",
"selected_periode": "Toute la journée",
}.items():
st.session_state.setdefault(key, default)
# =========================================================
# Sécurité mots de passe
# =========================================================
def hash_password(plain_password):
return bcrypt.hashpw(plain_password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verifier_password(input_password, hash_en_base):
return bcrypt.checkpw(input_password.encode("utf-8"), hash_en_base.encode("utf-8"))
# =========================================================
# Bootstrap schéma : crée Journal_Erreurs si absent
# Gère 2 cas :
# - VERSION A : colonne générée (recommandée)
# - VERSION B (fallback) : colonne simple + triggers pour normaliser Source_Id_norm
# ========================================================= # =========================================================
def ensure_schema(): def ensure_schema():
ddl_generated = """ ddl_generated = """
@@ -198,7 +194,7 @@ def ensure_schema():
] ]
triggers_fallback = [ triggers_fallback = [
""" """
CREATE TRIGGER IF NOT EXISTS trg_je_bi CREATE TRIGGER trg_je_bi
BEFORE INSERT ON Sondes.Journal_Erreurs BEFORE INSERT ON Sondes.Journal_Erreurs
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
@@ -206,7 +202,7 @@ def ensure_schema():
END END
""", """,
""" """
CREATE TRIGGER IF NOT EXISTS trg_je_bu CREATE TRIGGER trg_je_bu
BEFORE UPDATE ON Sondes.Journal_Erreurs BEFORE UPDATE ON Sondes.Journal_Erreurs
FOR EACH ROW FOR EACH ROW
BEGIN BEGIN
@@ -216,19 +212,17 @@ def ensure_schema():
] ]
try: try:
with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur: with closing(get_connection()) as cnx, closing(cnx.cursor()) as cur:
cur.execute(ddl_generated) cur.execute(ddl_generated)
cnx.commit() cnx.commit()
except Exception: except Exception:
# fallback si la colonne générée n'est pas supportée with closing(get_connection()) as cnx, closing(cnx.cursor()) as cur:
with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur:
cur.execute(ddl_fallback) cur.execute(ddl_fallback)
for q in idxs_fallback: for q in idxs_fallback:
try: try:
cur.execute(q) cur.execute(q)
except Exception: except Exception:
pass pass
# MariaDB/MySQL n'ont pas tous IF NOT EXISTS sur triggers → on tente drop/try
for name in ("trg_je_bi", "trg_je_bu"): for name in ("trg_je_bi", "trg_je_bu"):
try: try:
cur.execute(f"DROP TRIGGER IF EXISTS {name}") cur.execute(f"DROP TRIGGER IF EXISTS {name}")
@@ -241,12 +235,13 @@ def ensure_schema():
pass pass
cnx.commit() cnx.commit()
# Exécution à limport
try: try:
ensure_schema() ensure_schema()
except Exception as e: except Exception as e:
st.warning(f"Init schéma Journal_Erreurs : {e}") st.warning(f"Init schéma Journal_Erreurs : {e}")
# ========================================================= # =========================================================
# Connexion utilisateur # Connexion utilisateur
# ========================================================= # =========================================================
@@ -256,8 +251,7 @@ if not st.session_state.get("authenticated", False):
if st.sidebar.button("Se connecter"): if st.sidebar.button("Se connecter"):
try: try:
conn = get_connection() with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
cursor = conn.cursor(dictionary=True)
cursor.execute( cursor.execute(
""" """
SELECT NomUtilisateur, role, MotDePasseHash, Site, DateExpiration SELECT NomUtilisateur, role, MotDePasseHash, Site, DateExpiration
@@ -280,8 +274,9 @@ if not st.session_state.get("authenticated", False):
"authenticated": True, "authenticated": True,
"role": result["role"], "role": result["role"],
"site_autorise": result["Site"], "site_autorise": result["Site"],
"onglet_actif": "Accueil", # 👈 reset "onglet_actif": "Accueil",
}) })
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cursor.execute( cursor.execute(
""" """
@@ -293,33 +288,40 @@ if not st.session_state.get("authenticated", False):
conn.commit() conn.commit()
st.rerun() st.rerun()
cursor.close()
conn.close()
except Exception as e: except Exception as e:
st.sidebar.error(f"Erreur connexion : {e}") st.sidebar.error(f"Erreur connexion : {e}")
st.sidebar.text(traceback.format_exc())
else: else:
st.sidebar.success(f"Connecté ({st.session_state.get('role')})") st.sidebar.success(f"Connecté ({st.session_state.get('role')})")
if st.sidebar.button("🔓 Déconnexion"): if st.sidebar.button("🔓 Déconnexion"):
for key in ["authenticated", "role", "site_autorise"]: for key in ["authenticated", "role", "site_autorise"]:
st.session_state[key] = False if key == "authenticated" else None st.session_state[key] = False if key == "authenticated" else None
st.session_state["onglet_actif"] = "Accueil" # 👈 reset st.session_state["onglet_actif"] = "Accueil"
st.rerun() st.rerun()
# ========================================================= # =========================================================
# PDF # PDF
# ========================================================= # =========================================================
def generer_pdf(site, date_str, periode): def generer_pdf(site: str, date_str: str, periode: str):
assert_site_ok(site)
st.info(f"Génération du rapport PDF pour {site} à la date {date_str} ({periode})") st.info(f"Génération du rapport PDF pour {site} à la date {date_str} ({periode})")
try:
conn = get_connection()
pdf_cursor = conn.cursor(dictionary=True)
# Relevés plages = {
pdf_cursor.execute( "Toute la journée": (time(0, 0), time(23, 59)),
"Matin (6h-12h)": (time(6, 0), time(12, 0)),
"Après-midi (12h-18h)": (time(12, 0), time(18, 0)),
"Nuit (18h-6h)": (time(18, 0), time(6, 0)),
}
try:
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cur:
cur.execute(
f"SELECT Sonde, Date, Temperature FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", f"SELECT Sonde, Date, Temperature FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date",
(date_str,), (date_str,),
) )
rows = pdf_cursor.fetchall() rows = cur.fetchall()
df = pd.DataFrame(rows) df = pd.DataFrame(rows)
if df.empty: if df.empty:
st.warning("Aucune donnée ce jour.") st.warning("Aucune donnée ce jour.")
@@ -328,13 +330,6 @@ def generer_pdf(site, date_str, periode):
df["Heure"] = pd.to_datetime(df["Date"]).dt.strftime("%H:%M") df["Heure"] = pd.to_datetime(df["Date"]).dt.strftime("%H:%M")
df["Heure_obj"] = pd.to_datetime(df["Date"]).dt.time df["Heure_obj"] = pd.to_datetime(df["Date"]).dt.time
# Périodes
plages = {
"Toute la journée": (time(0, 0), time(23, 59)),
"Matin (6h-12h)": (time(6, 0), time(12, 0)),
"Après-midi (12h-18h)": (time(12, 0), time(18, 0)),
"Nuit (18h-6h)": (time(18, 0), time(6, 0)),
}
heure_debut, heure_fin = plages.get(periode, (time(0, 0), time(23, 59))) heure_debut, heure_fin = plages.get(periode, (time(0, 0), time(23, 59)))
if heure_debut < heure_fin: if heure_debut < heure_fin:
df = df[(df["Heure_obj"] >= heure_debut) & (df["Heure_obj"] <= heure_fin)] df = df[(df["Heure_obj"] >= heure_debut) & (df["Heure_obj"] <= heure_fin)]
@@ -346,16 +341,12 @@ def generer_pdf(site, date_str, periode):
df_sonde = df[df["Sonde"] == sonde] df_sonde = df[df["Sonde"] == sonde]
releves[sonde] = list(zip(df_sonde["Heure"], df_sonde["Temperature"])) releves[sonde] = list(zip(df_sonde["Heure"], df_sonde["Temperature"]))
# Alertes
table_alertes = f"Alertes_{site}" table_alertes = f"Alertes_{site}"
pdf_cursor.execute( cur.execute(
f"SELECT Sonde, Debut_defaut, Etat FROM `{table_alertes}` WHERE DATE(Debut_defaut) = %s", f"SELECT Sonde, Debut_defaut, Etat FROM `{table_alertes}` WHERE DATE(Debut_defaut) = %s",
(date_str,), (date_str,),
) )
alertes = pdf_cursor.fetchall() alertes = cur.fetchall()
pdf_cursor.close()
conn.close()
class RapportPDF(FPDF): class RapportPDF(FPDF):
def header(self): def header(self):
@@ -397,7 +388,9 @@ def generer_pdf(site, date_str, periode):
self.cell(30, 6, f"{t1:.2f}", border=1) self.cell(30, 6, f"{t1:.2f}", border=1)
else: else:
self.cell(70, 6, "", border=0) self.cell(70, 6, "", border=0)
self.cell(20, 6, "", border=0) self.cell(20, 6, "", border=0)
if i < len(col2): if i < len(col2):
h2, t2 = col2[i] h2, t2 = col2[i]
self.cell(40, 6, h2, border=1) self.cell(40, 6, h2, border=1)
@@ -436,11 +429,13 @@ def generer_pdf(site, date_str, periode):
mime="application/pdf", mime="application/pdf",
) )
except Exception as err1: except Exception as err:
st.error(f"Erreur lors de la génération du PDF : {err1}") st.error(f"Erreur lors de la génération du PDF : {err}")
st.text(traceback.format_exc())
# ========================================================= # =========================================================
# Fonctions Journal erreurs (SQL) # Journal erreurs (SQL)
# ========================================================= # =========================================================
def _get_site_courant(): def _get_site_courant():
role = st.session_state.get("role") role = st.session_state.get("role")
@@ -448,7 +443,9 @@ def _get_site_courant():
return st.session_state.get("site_autorise") return st.session_state.get("site_autorise")
return st.session_state.get("selected_site", "Saclay") return st.session_state.get("selected_site", "Saclay")
def load_alertes(site: str, jour: date): def load_alertes(site: str, jour: date):
assert_site_ok(site)
table_alertes = f"Alertes_{site}" table_alertes = f"Alertes_{site}"
q = f""" q = f"""
SELECT SELECT
@@ -463,22 +460,18 @@ def load_alertes(site: str, jour: date):
WHERE DATE(a.Debut_defaut) = %s WHERE DATE(a.Debut_defaut) = %s
OR (a.Etat <> 'Acquitté' AND DATE(a.Debut_defaut) <= %s); OR (a.Etat <> 'Acquitté' AND DATE(a.Debut_defaut) <= %s);
""" """
with closing(get_conn()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
cur.execute(q, (site, jour, jour)) cur.execute(q, (site, jour, jour))
rows = cur.fetchall() rows = cur.fetchall()
cols = ["Source_Id","Site","Sonde","DateJour","Type","Resume","Etat"] cols = ["Source_Id", "Site", "Sonde", "DateJour", "Type", "Resume", "Etat"]
return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols)
def load_anomalies_auto(site: str, jour: date): def load_anomalies_auto(site: str, jour: date):
""" assert_site_ok(site)
Détection d'anomalies "Auto" sur la date choisie :
- GAPS : vrais trous = diff entre 2 mesures consécutives >= gap_threshold_min
- JUMPS : saut de température > jump_deg entre 2 mesures consécutives
- BOUNDS : valeurs hors bornes physiques
"""
table_mesures = site table_mesures = site
gap_threshold_min = 20 # seuil "trou" (ex. mesures toutes 5 min → 20 min = 4 créneaux manqués) gap_threshold_min = 20
jump_deg = 10 # saut suspect jump_deg = 10
min_phys, max_phys = -60, 120 min_phys, max_phys = -60, 120
q = f""" q = f"""
@@ -538,26 +531,29 @@ def load_anomalies_auto(site: str, jour: date):
ORDER BY Sonde; ORDER BY Sonde;
""" """
params = (jour, site, jour, site, jour, site, jour) params = (jour, site, jour, site, jour, site, jour)
with closing(get_conn()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
cur.execute(q, params) cur.execute(q, params)
rows = cur.fetchall() rows = cur.fetchall()
cols = ["Source_Id","Site","Sonde","DateJour","Type","Resume"] cols = ["Source_Id", "Site", "Sonde", "DateJour", "Type", "Resume"]
return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols)
def load_journal_existants(site: str, jour: date): def load_journal_existants(site: str, jour: date):
assert_site_ok(site)
q = """ q = """
SELECT Id, Site, Sonde, DateJour, Type, Source_Id, Resume, SELECT Id, Site, Sonde, DateJour, Type, Source_Id, Resume,
Statut, Priorite, Assignation, Commentaire, Tag Statut, Priorite, Assignation, Commentaire, Tag
FROM Sondes.Journal_Erreurs FROM Sondes.Journal_Erreurs
WHERE Site=%s AND DateJour=%s; WHERE Site=%s AND DateJour=%s;
""" """
with closing(get_conn()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
cur.execute(q, (site, jour)) cur.execute(q, (site, jour))
rows = cur.fetchall() rows = cur.fetchall()
cols = ["Id","Site","Sonde","DateJour","Type","Source_Id","Resume","Statut","Priorite","Assignation","Commentaire","Tag"] cols = ["Id", "Site", "Sonde", "DateJour", "Type", "Source_Id", "Resume",
"Statut", "Priorite", "Assignation", "Commentaire", "Tag"]
return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols)
def upsert_journal(rows: list[dict]): def upsert_journal(rows: list[dict]):
if not rows: if not rows:
return return
@@ -575,15 +571,17 @@ def upsert_journal(rows: list[dict]):
Tag=VALUES(Tag), Tag=VALUES(Tag),
UpdatedAt=CURRENT_TIMESTAMP; UpdatedAt=CURRENT_TIMESTAMP;
""" """
with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur: with closing(get_connection()) as cnx, closing(cnx.cursor()) as cur:
cur.executemany(q_insert, rows) cur.executemany(q_insert, rows)
cnx.commit() cnx.commit()
# ========================================================= # =========================================================
# Page Journal erreurs # Page Journal erreurs
# ========================================================= # =========================================================
def page_journal_erreurs(): def page_journal_erreurs():
st.header("📝 Journal des erreurs") st.header("📝 Journal des erreurs")
site = _get_site_courant() site = _get_site_courant()
if not site: if not site:
st.warning("Veuillez dabord choisir un site sur la page daccueil.") st.warning("Veuillez dabord choisir un site sur la page daccueil.")
@@ -595,7 +593,6 @@ def page_journal_erreurs():
jour = st.date_input("Date de vision", value=st.session_state.get("selected_date", date.today())) jour = st.date_input("Date de vision", value=st.session_state.get("selected_date", date.today()))
st.caption(f"Site : {site} — Date : {jour.strftime('%d/%m/%Y')}") st.caption(f"Site : {site} — Date : {jour.strftime('%d/%m/%Y')}")
# --- Chargement des sources
df_alertes = load_alertes(site, jour) df_alertes = load_alertes(site, jour)
df_auto = load_anomalies_auto(site, jour) df_auto = load_anomalies_auto(site, jour)
df_saved = load_journal_existants(site, jour) df_saved = load_journal_existants(site, jour)
@@ -605,54 +602,69 @@ def page_journal_erreurs():
st.info("Aucune anomalie détectée ni alerte pour cette date.") st.info("Aucune anomalie détectée ni alerte pour cette date.")
return return
# Colonnes attendues def _source_norm(x):
for col in ["Statut","Priorite","Assignation","Commentaire","Tag","Id","Source_Id"]: return 0 if pd.isna(x) else int(x)
if col not in base.columns:
base[col] = pd.NA
key_join = ["Site","Sonde","DateJour","Type","Source_Id","Resume"] # base : garantir Source_Id + Source_Id_norm
df = base.merge(df_saved, on=key_join, how="left", suffixes=("","_saved")) if "Source_Id" not in base.columns:
base["Source_Id"] = pd.NA
base["Source_Id_norm"] = base["Source_Id"].apply(_source_norm)
# Garantir l'existence des *_saved si df_saved est vide # df_saved : garantir colonnes et Key, même si vide
for c in ["Statut_saved","Priorite_saved","Assignation_saved","Commentaire_saved", if df_saved is None or df_saved.empty:
"Tag_saved","Id_saved","Source_Id_saved"]: df_saved = pd.DataFrame(columns=["Key", "Statut", "Priorite", "Assignation", "Commentaire", "Tag"])
if c not in df.columns:
df[c] = pd.NA
# Valeurs par défaut depuis sauvegarde
df["Statut"] = df["Statut"].fillna(df["Statut_saved"]).fillna("Nouveau")
df["Priorite"] = df["Priorite"].fillna(df["Priorite_saved"]).fillna(3)
df["Assignation"] = df["Assignation"].fillna(df["Assignation_saved"])
df["Commentaire"] = df["Commentaire"].fillna(df["Commentaire_saved"])
df["Tag"] = df["Tag"].fillna(df["Tag_saved"])
df["Id"] = df["Id"].fillna(df["Id_saved"])
df["Source_Id"] = df["Source_Id"].fillna(df["Source_Id_saved"])
# --- Types compatibles pour l'éditeur ---
text_cols = ["Sonde", "Type", "Resume", "Statut", "Assignation", "Tag", "Commentaire"]
for c in text_cols:
if c not in df.columns:
df[c] = pd.Series(dtype="string") # colonne vide typée texte
else: else:
df[c] = df[c].astype("string").fillna("") # tout en string, pas de NaN if "Source_Id" not in df_saved.columns:
df_saved["Source_Id"] = pd.NA
df_saved["Source_Id_norm"] = df_saved["Source_Id"].apply(_source_norm)
# Priorité = entier nullable (évite 3.0) df_saved["Key"] = (
df_saved["Site"].astype(str) + "|" +
df_saved["Sonde"].astype(str) + "|" +
df_saved["DateJour"].astype(str) + "|" +
df_saved["Type"].astype(str) + "|" +
df_saved["Source_Id_norm"].astype(str)
)
for c in ["Statut", "Priorite", "Assignation", "Commentaire", "Tag"]:
if c not in df_saved.columns:
df_saved[c] = pd.NA
# base Key (toujours)
base["Key"] = (
base["Site"].astype(str) + "|" +
base["Sonde"].astype(str) + "|" +
base["DateJour"].astype(str) + "|" +
base["Type"].astype(str) + "|" +
base["Source_Id_norm"].astype(str)
)
df = base.merge(
df_saved[["Key", "Statut", "Priorite", "Assignation", "Commentaire", "Tag"]],
on="Key",
how="left",
)
# Defaults & types
df["Statut"] = df["Statut"].fillna("Nouveau").astype("string")
df["Priorite"] = pd.to_numeric(df["Priorite"], errors="coerce").fillna(3).astype("Int64") df["Priorite"] = pd.to_numeric(df["Priorite"], errors="coerce").fillna(3).astype("Int64")
# --- Éditeur for c in ["Assignation", "Commentaire", "Tag"]:
st.subheader("Synthèse (éditable)") df[c] = df[c].astype("string").fillna("")
edit_cols = ["Sonde","Type","Resume","Statut","Priorite","Assignation","Tag","Commentaire"]
disabled_cols = ["Sonde","Type","Resume"] for c in ["Sonde", "Type", "Resume"]:
df[c] = df[c].astype("string").fillna("")
st.subheader("Synthèse (éditable)") st.subheader("Synthèse (éditable)")
edit_cols = ["Sonde", "Type", "Resume", "Statut", "Priorite", "Assignation", "Tag", "Commentaire"]
disabled_cols = ["Sonde", "Type", "Resume"] view_cols = ["Key", "Sonde", "Type", "Resume", "Statut", "Priorite", "Assignation", "Tag", "Commentaire"]
disabled_cols = ["Key", "Sonde", "Type", "Resume"]
editable = st.data_editor( editable = st.data_editor(
df[edit_cols].copy(), # <<< important df[view_cols].copy(),
disabled=disabled_cols, disabled=disabled_cols,
use_container_width=True, use_container_width=True,
hide_index=True, hide_index=True,
column_config={ column_config={
"Key": st.column_config.TextColumn("Key", disabled=True),
"Statut": st.column_config.SelectboxColumn( "Statut": st.column_config.SelectboxColumn(
"Statut", options=["Nouveau", "En cours", "Planifié", "Clos"], help="État de la tâche" "Statut", options=["Nouveau", "En cours", "Planifié", "Clos"], help="État de la tâche"
), ),
@@ -668,18 +680,19 @@ def page_journal_erreurs():
} }
) )
# --- Sauvegarde
def _none_if_empty(x): def _none_if_empty(x):
if x is None: return None if x is None:
if isinstance(x, float) and pd.isna(x): return None return None
if isinstance(x, str) and x.strip() == "": return None if isinstance(x, float) and pd.isna(x):
return None
if isinstance(x, str) and x.strip() == "":
return None
return x return x
if st.session_state.get("role") == "superviseur": if st.session_state.get("role") == "superviseur":
if st.button("💾 Enregistrer les modifications"): if st.button("💾 Enregistrer les modifications"):
# On rattache les clés (Site/Sonde/DateJour/Type/Source_Id/Resume) aux lignes éditées df_keys = df[["Key", "Site", "Sonde", "DateJour", "Type", "Source_Id", "Resume"]].copy()
df_keys = df[["Site","Sonde","DateJour","Type","Source_Id","Resume"]] df_to_save = editable.merge(df_keys, on="Key", how="left")
df_to_save = editable.merge(df_keys, on=["Sonde","Type","Resume"], how="left")
payload = [] payload = []
for _, r in df_to_save.iterrows(): for _, r in df_to_save.iterrows():
@@ -713,14 +726,10 @@ if st.session_state.get("authenticated"):
if role != "superviseur" if role != "superviseur"
else st.session_state.get("selected_site", "Saclay") else st.session_state.get("selected_site", "Saclay")
) )
if not site_selectionne: if site_selectionne:
st.info("Connectez-vous et choisissez un site pour afficher les alertes.") assert_site_ok(site_selectionne)
else:
if site_selectionne not in SITES_AUTORISES:
raise ValueError(f"Site invalide: {site_selectionne}")
conn = get_connection()
cursor = conn.cursor(dictionary=True)
table_alertes = f"Alertes_{site_selectionne}" table_alertes = f"Alertes_{site_selectionne}"
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
cursor.execute( cursor.execute(
f"SELECT Sonde, Debut_defaut, Etat " f"SELECT Sonde, Debut_defaut, Etat "
f"FROM `{table_alertes}` " f"FROM `{table_alertes}` "
@@ -728,45 +737,47 @@ if st.session_state.get("authenticated"):
f"ORDER BY Debut_defaut DESC" f"ORDER BY Debut_defaut DESC"
) )
alertes = cursor.fetchall() alertes = cursor.fetchall()
if alertes: if alertes:
df_alertes = pd.DataFrame(alertes)
st.subheader("🚨 Alertes non acquittées") st.subheader("🚨 Alertes non acquittées")
st.dataframe(df_alertes, use_container_width=True) st.dataframe(pd.DataFrame(alertes), use_container_width=True)
else: else:
st.success("✅ Aucune alerte en cours.") st.success("✅ Aucune alerte en cours.")
cursor.close() else:
conn.close() st.info("Connectez-vous et choisissez un site pour afficher les alertes.")
except Exception as e: except Exception as e:
st.error(f"Erreur lors de la récupération des alertes : {e}") st.error(f"Erreur lors de la récupération des alertes : {e}")
else: st.text(traceback.format_exc())
st.info("Connectez-vous pour voir les alertes en cours.") else:
pass
# ========================================================= # =========================================================
# Navigation # Navigation + Pages (CORRIGÉ : pour superviseur ET utilisateur)
# ========================================================= # =========================================================
if st.session_state["authenticated"]: if st.session_state.get("authenticated"):
onglets = (
["Accueil", "Entretien"]
if st.session_state["role"] != "superviseur"
else ["Accueil", "Statistiques", "Entretien", "Traffic", "Journal erreurs"]
)
onglets = (
["Accueil", "Entretien"]
if st.session_state["role"] != "superviseur"
else ["Accueil", "Statistiques", "Entretien", "Traffic", "Journal erreurs"]
)
# 🔒 Normaliser l'onglet actif si absent de la liste (ex. compte non-admin) # Onglets selon rôle
if st.session_state.get("role") != "superviseur":
onglets = ["Accueil", "Statistiques"]
else:
onglets = ["Accueil", "Statistiques", "Traffic", "Journal erreurs"]
# Normaliser l'onglet actif
if st.session_state.get("onglet_actif") not in onglets: if st.session_state.get("onglet_actif") not in onglets:
st.session_state["onglet_actif"] = onglets[0] st.session_state["onglet_actif"] = onglets[0]
# Menu (créé pour tous)
onglet_selectionne = st.sidebar.radio( onglet_selectionne = st.sidebar.radio(
"📁 Navigation", onglets, index=onglets.index(st.session_state["onglet_actif"]) "📁 Navigation",
onglets,
index=onglets.index(st.session_state["onglet_actif"]),
) )
st.session_state["onglet_actif"] = onglet_selectionne
# Contexte commun
site_actuel = ( site_actuel = (
st.session_state.get("site_autorise") st.session_state.get("site_autorise")
if st.session_state["role"] != "superviseur" if st.session_state.get("role") != "superviseur"
else st.session_state.get("selected_site", "Saclay") else st.session_state.get("selected_site", "Saclay")
) )
date_selectionnee = st.session_state.get("selected_date", date.today()) date_selectionnee = st.session_state.get("selected_date", date.today())
@@ -774,28 +785,40 @@ if st.session_state["authenticated"]:
# ------------------ Accueil ------------------ # ------------------ Accueil ------------------
if onglet_selectionne == "Accueil": if onglet_selectionne == "Accueil":
try: try:
# Site imposé ou sélection admin
if st.session_state.get("role") == "superviseur":
# sélection possible
if site_actuel not in SITES_LISTE:
site_actuel = SITES_LISTE[0]
site_actuel = st.selectbox(
"📍 Choisissez un site :",
SITES_LISTE,
index=SITES_LISTE.index(site_actuel) if site_actuel in SITES_LISTE else 0
)
st.session_state["selected_site"] = site_actuel
else:
st.info(f"Site imposé : {site_actuel}")
# --- Voyant Gyro pour le site courant --- assert_site_ok(site_actuel)
# Voyant Gyro
st.subheader(f"🚨 Statut Gyro — {site_actuel}") st.subheader(f"🚨 Statut Gyro — {site_actuel}")
try: try:
st.autorefresh(interval=30000, key="gyro_autorefresh") st.autorefresh(interval=30000, key="gyro_autorefresh")
except Exception: except Exception:
pass pass
render_gyro_badge(site_actuel) render_gyro_badge(site_actuel)
# ----------------------------------------
conn = get_connection()
cursor = conn.cursor(dictionary=True)
if st.session_state["role"] == "superviseur":
site_actuel = st.selectbox("📍 Choisissez un site :", ["Saclay", "Meudon"], index=0)
st.session_state["selected_site"] = site_actuel
else:
st.info(f"Site imposé : {site_actuel}")
# Date
date_selectionnee = st.date_input("📅 Date du relevé", value=date_selectionnee) date_selectionnee = st.date_input("📅 Date du relevé", value=date_selectionnee)
st.session_state["selected_date"] = date_selectionnee st.session_state["selected_date"] = date_selectionnee
rows = []
df_sonde = pd.DataFrame()
seuil_temp = 10.0
sonde_choisie = None
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
cursor.execute( cursor.execute(
f"SELECT * FROM `{site_actuel}` WHERE DATE(Date) = %s ORDER BY Sonde, Date DESC", f"SELECT * FROM `{site_actuel}` WHERE DATE(Date) = %s ORDER BY Sonde, Date DESC",
(date_selectionnee.strftime("%Y-%m-%d"),), (date_selectionnee.strftime("%Y-%m-%d"),),
@@ -807,6 +830,7 @@ if st.session_state["authenticated"]:
df["Date"] = pd.to_datetime(df["Date"]) df["Date"] = pd.to_datetime(df["Date"])
sondes = sorted(df["Sonde"].unique()) sondes = sorted(df["Sonde"].unique())
sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes) sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes)
df_sonde = df[df["Sonde"] == sonde_choisie].copy() df_sonde = df[df["Sonde"] == sonde_choisie].copy()
df_sonde["Heure"] = df_sonde["Date"].dt.hour df_sonde["Heure"] = df_sonde["Date"].dt.hour
@@ -816,7 +840,6 @@ if st.session_state["authenticated"]:
) )
st.session_state["selected_periode"] = tranche st.session_state["selected_periode"] = tranche
# Génération PDF
if st.button("🧾 Générer le PDF du jour"): if st.button("🧾 Générer le PDF du jour"):
generer_pdf( generer_pdf(
site_actuel, site_actuel,
@@ -824,6 +847,7 @@ if st.session_state["authenticated"]:
st.session_state.get("selected_periode", "Toute la journée"), st.session_state.get("selected_periode", "Toute la journée"),
) )
# Filtre tranche
if tranche == "Matin (6h-12h)": if tranche == "Matin (6h-12h)":
df_sonde = df_sonde[(df_sonde["Heure"] >= 6) & (df_sonde["Heure"] < 12)] df_sonde = df_sonde[(df_sonde["Heure"] >= 6) & (df_sonde["Heure"] < 12)]
elif tranche == "Après-midi (12h-18h)": elif tranche == "Après-midi (12h-18h)":
@@ -831,15 +855,16 @@ if st.session_state["authenticated"]:
elif tranche == "Nuit (18h-6h)": elif tranche == "Nuit (18h-6h)":
df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)] df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)]
seuil_temp = 10 # Seuil
cursor.execute( cursor.execute(
"SELECT Temp_Max FROM Sondes.Chambres_froides WHERE Lieu = %s AND Sonde = %s", "SELECT Temp_Max FROM Sondes.Chambres_froides WHERE Lieu = %s AND Sonde = %s",
(site_actuel, sonde_choisie), (site_actuel, sonde_choisie),
) )
seuil = cursor.fetchone() seuil = cursor.fetchone()
if seuil: if seuil and seuil.get("Temp_Max") is not None:
seuil_temp = seuil["Temp_Max"] seuil_temp = float(seuil["Temp_Max"])
if rows and not df_sonde.empty:
st.subheader("📊 Tableau des relevés") st.subheader("📊 Tableau des relevés")
def surlignage_temp(val): def surlignage_temp(val):
@@ -852,6 +877,7 @@ if st.session_state["authenticated"]:
styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"]) styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"])
st.dataframe(styled_df, use_container_width=True) st.dataframe(styled_df, use_container_width=True)
st.subheader("📈 Évolution de la température") st.subheader("📈 Évolution de la température")
fig, ax = plt.subplots(figsize=(10, 4)) fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o")
@@ -862,9 +888,9 @@ if st.session_state["authenticated"]:
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
ax.legend() ax.legend()
st.pyplot(fig) st.pyplot(fig)
elif not rows:
st.info("Aucun relevé pour cette date.")
cursor.close()
conn.close()
except Exception as e: except Exception as e:
st.error(f"Erreur : {e}") st.error(f"Erreur : {e}")
st.text(traceback.format_exc()) st.text(traceback.format_exc())
@@ -872,21 +898,24 @@ if st.session_state["authenticated"]:
# ------------------ Statistiques ------------------ # ------------------ Statistiques ------------------
elif onglet_selectionne == "Statistiques": elif onglet_selectionne == "Statistiques":
st.markdown("## 📈 Statistiques de température") st.markdown("## 📈 Statistiques de température")
site = ( site = (
st.session_state["site_autorise"] st.session_state.get("site_autorise")
if st.session_state["role"] != "superviseur" if st.session_state.get("role") != "superviseur"
else st.session_state.get("selected_site", "Saclay") else st.session_state.get("selected_site", "Saclay")
) )
assert_site_ok(site)
date_val = st.session_state.get("selected_date", date.today()) date_val = st.session_state.get("selected_date", date.today())
try: try:
conn = get_connection() with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
cursor = conn.cursor(dictionary=True)
cursor.execute( cursor.execute(
f"SELECT * FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", f"SELECT * FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date",
(date_val.strftime("%Y-%m-%d"),), (date_val.strftime("%Y-%m-%d"),),
) )
rows = cursor.fetchall() rows = cursor.fetchall()
df = pd.DataFrame(rows) df = pd.DataFrame(rows)
if df.empty: if df.empty:
st.info("Aucune donnée pour cette date.") st.info("Aucune donnée pour cette date.")
@@ -895,6 +924,7 @@ if st.session_state["authenticated"]:
sondes = sorted(df["Sonde"].unique()) sondes = sorted(df["Sonde"].unique())
sonde = st.selectbox("Choisir une sonde :", sondes, key="selectbox_stats") sonde = st.selectbox("Choisir une sonde :", sondes, key="selectbox_stats")
df_sonde = df[df["Sonde"] == sonde] df_sonde = df[df["Sonde"] == sonde]
st.subheader("Évolution journalière") st.subheader("Évolution journalière")
fig, ax = plt.subplots(figsize=(10, 4)) fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o")
@@ -903,21 +933,22 @@ if st.session_state["authenticated"]:
ax.set_ylabel("Température (°C)") ax.set_ylabel("Température (°C)")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
st.pyplot(fig) st.pyplot(fig)
cursor.close()
conn.close()
except Exception as e: except Exception as e:
st.error(f"Erreur chargement statistiques : {e}") st.error(f"Erreur chargement statistiques : {e}")
st.text(traceback.format_exc())
# Admin Chambres froides # Admin Chambres froides
if st.session_state["role"] == "superviseur": if st.session_state.get("role") == "superviseur":
with st.expander("🛠️ Gestion des chambres froides (administrateur)", expanded=True): with st.expander("🛠️ Gestion des chambres froides (administrateur)", expanded=True):
if st.button("🔄 Actualiser la liste"): if st.button("🔄 Actualiser la liste"):
st.session_state["refresh_admin"] = random.randint(0, 9999) st.session_state["refresh_admin"] = random.randint(0, 9999)
try: try:
conn_admin = get_connection() with closing(get_connection()) as conn_admin, closing(conn_admin.cursor(dictionary=True)) as cursor_admin:
cursor_admin = conn_admin.cursor(dictionary=True)
cursor_admin.execute("SELECT * FROM Sondes.Chambres_froides WHERE Lieu = %s", (site,)) cursor_admin.execute("SELECT * FROM Sondes.Chambres_froides WHERE Lieu = %s", (site,))
chambres = cursor_admin.fetchall() chambres = cursor_admin.fetchall()
if not chambres: if not chambres:
st.warning("Aucune chambre froide pour ce site.") st.warning("Aucune chambre froide pour ce site.")
else: else:
@@ -925,6 +956,7 @@ if st.session_state["authenticated"]:
col1, col2, col3 = st.columns([3, 1, 2]) col1, col2, col3 = st.columns([3, 1, 2])
with col1: with col1:
st.markdown(f"**{chambre['Sonde']}**") st.markdown(f"**{chambre['Sonde']}**")
with col2: with col2:
etat = st.checkbox( etat = st.checkbox(
"ON", "ON",
@@ -932,8 +964,9 @@ if st.session_state["authenticated"]:
key=f"etat_{chambre['Id']}_{st.session_state.get('refresh_admin', 0)}", key=f"etat_{chambre['Id']}_{st.session_state.get('refresh_admin', 0)}",
) )
new_etat = "ON" if etat else "OFF" new_etat = "ON" if etat else "OFF"
with col3: with col3:
temp_max = chambre["Temp_Max"] temp_max = int(chambre["Temp_Max"])
moins, temp_display, plus = st.columns([1, 2, 1]) moins, temp_display, plus = st.columns([1, 2, 1])
with moins: with moins:
if st.button("", key=f"moins_{chambre['Id']}"): if st.button("", key=f"moins_{chambre['Id']}"):
@@ -946,6 +979,7 @@ if st.session_state["authenticated"]:
with plus: with plus:
if st.button("", key=f"plus_{chambre['Id']}"): if st.button("", key=f"plus_{chambre['Id']}"):
temp_max += 1 temp_max += 1
if new_etat != chambre["Etat"] or temp_max != chambre["Temp_Max"]: if new_etat != chambre["Etat"] or temp_max != chambre["Temp_Max"]:
cursor_admin.execute( cursor_admin.execute(
"UPDATE Sondes.Chambres_froides SET Etat = %s, Temp_Max = %s WHERE Id = %s", "UPDATE Sondes.Chambres_froides SET Etat = %s, Temp_Max = %s WHERE Id = %s",
@@ -953,54 +987,36 @@ if st.session_state["authenticated"]:
) )
conn_admin.commit() conn_admin.commit()
st.success(f"{chambre['Sonde']} mise à jour") st.success(f"{chambre['Sonde']} mise à jour")
cursor_admin.close()
conn_admin.close()
except Exception as e: except Exception as e:
st.error(f"Erreur SQL (admin) : {e}") st.error(f"Erreur SQL (admin) : {e}")
# ------------------ Entretien ------------------
elif onglet_selectionne == "Entretien":
st.header("🧰 Gestion Entretien")
try:
conn = get_connection()
cursor = conn.cursor(dictionary=True)
cursor.execute(
"SELECT Id, Sonde, En_entretien FROM Sondes.Chambres_froides WHERE Lieu = %s",
(site_actuel,),
)
sondes = cursor.fetchall()
for sonde in sondes:
checked = st.checkbox(f"{sonde['Sonde']}", value=sonde["En_entretien"])
if checked != sonde["En_entretien"]:
cursor.execute(
"UPDATE Sondes.Chambres_froides SET En_entretien = %s WHERE Id = %s",
(checked, sonde["Id"]),
)
conn.commit()
st.success(f"{sonde['Sonde']} {'mise' if checked else 'retirée'} en entretien.")
cursor.close()
conn.close()
except Exception as e:
st.error(f"Erreur : {e}")
st.text(traceback.format_exc()) st.text(traceback.format_exc())
# ------------------ Traffic ------------------ # ------------------ Traffic ------------------
elif onglet_selectionne == "Traffic": elif onglet_selectionne == "Traffic":
st.header("🚦 Connexions récentes") st.header("🚦 Connexions récentes")
try: try:
conn = get_connection() with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
cursor = conn.cursor(dictionary=True)
cursor.execute( cursor.execute(
"SELECT Utilisateur, Lieu, Date_Connexion FROM Sondes.Connexion_Log ORDER BY Date_Connexion DESC LIMIT 100" "SELECT Utilisateur, Lieu, Date_Connexion "
"FROM Sondes.Connexion_Log "
"WHERE Utilisateur NOT LIKE %s "
"ORDER BY Date_Connexion DESC "
"LIMIT 100",
("Michel%",),
) )
logs = cursor.fetchall() logs = cursor.fetchall()
df_logs = pd.DataFrame(logs) df_logs = pd.DataFrame(logs)
st.dataframe(df_logs, use_container_width=True) st.dataframe(df_logs, use_container_width=True)
cursor.close()
conn.close()
except Exception as e: except Exception as e:
st.error(f"Erreur : {e}") st.error(f"Erreur : {e}")
st.text(traceback.format_exc())
# ------------------ Journal erreurs ------------------ # ------------------ Journal erreurs ------------------
elif onglet_selectionne == "Journal erreurs": elif onglet_selectionne == "Journal erreurs":
page_journal_erreurs() page_journal_erreurs()
else:
st.info("Connectez-vous pour accéder à lapplication.")