# -*- coding: utf-8 -*- import os import random import traceback from datetime import datetime, date, time import bcrypt import matplotlib.dates as mdates import matplotlib.pyplot as plt import mysql.connector import pandas as pd import streamlit as st from contextlib import closing from dotenv import load_dotenv from fpdf import FPDF # ========================================================= # Config de page # ========================================================= st.set_page_config(page_title="Domo91 - Surveillance", layout="wide") st.title("📊 Domo91 - Surveillance des sondes") st.write("Bienvenue sur l’application de supervision.") # ========================================================= # ENV & DB # ========================================================= load_dotenv() db_config = { "host": os.getenv("DB_HOST"), "user": os.getenv("DB_USER"), "password": os.getenv("DB_PASS"), "database": os.getenv("DB_NAME"), "autocommit": False, } SITES_AUTORISES = {"Saclay", "Meudon", "Roissy"} # anti-injection sur noms de tables def get_connection(): return mysql.connector.connect(**db_config) def get_conn(): return mysql.connector.connect(**db_config) # ========================================================= # 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): 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(): ddl_generated = """ CREATE TABLE IF NOT EXISTS Journal_Erreurs ( Id INT AUTO_INCREMENT PRIMARY KEY, Site VARCHAR(50) NOT NULL, Sonde VARCHAR(100) NOT NULL, DateJour DATE NOT NULL, Type VARCHAR(30) NOT NULL, Source_Id INT NULL, Source_Id_norm INT GENERATED ALWAYS AS (COALESCE(Source_Id, 0)) STORED, Resume TEXT NOT NULL, Statut VARCHAR(20) NOT NULL DEFAULT 'Nouveau', Priorite TINYINT NOT NULL DEFAULT 3, Assignation VARCHAR(100) NULL, Commentaire TEXT NULL, Tag VARCHAR(50) NULL, CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_site_sonde_date_type (Site, Sonde, DateJour, Type, Source_Id_norm), KEY idx_journal_date_site (DateJour, Site), KEY idx_journal_statut (Statut) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; """ ddl_fallback = """ CREATE TABLE IF NOT EXISTS Journal_Erreurs ( Id INT AUTO_INCREMENT PRIMARY KEY, Site VARCHAR(50) NOT NULL, Sonde VARCHAR(100) NOT NULL, DateJour DATE NOT NULL, Type VARCHAR(30) NOT NULL, Source_Id INT NULL, Source_Id_norm INT NULL DEFAULT 0, Resume TEXT NOT NULL, Statut VARCHAR(20) NOT NULL DEFAULT 'Nouveau', Priorite TINYINT NOT NULL DEFAULT 3, Assignation VARCHAR(100) NULL, Commentaire TEXT NULL, Tag VARCHAR(50) NULL, CreatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UpdatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; """ idxs_fallback = [ "CREATE UNIQUE INDEX IF NOT EXISTS uk_site_sonde_date_type ON Sondes.Journal_Erreurs (Site, Sonde, DateJour, Type, Source_Id_norm)", "CREATE INDEX IF NOT EXISTS idx_journal_date_site ON Sondes.Journal_Erreurs (DateJour, Site)", "CREATE INDEX IF NOT EXISTS idx_journal_statut ON Sondes.Journal_Erreurs (Statut)", ] triggers_fallback = [ """ CREATE TRIGGER IF NOT EXISTS trg_je_bi BEFORE INSERT ON Sondes.Journal_Erreurs FOR EACH ROW BEGIN SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); END """, """ CREATE TRIGGER IF NOT EXISTS trg_je_bu BEFORE UPDATE ON Sondes.Journal_Erreurs FOR EACH ROW BEGIN SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); END """, ] try: with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur: cur.execute(ddl_generated) cnx.commit() except Exception: # fallback si la colonne gĂ©nĂ©rĂ©e n'est pas supportĂ©e with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur: cur.execute(ddl_fallback) for q in idxs_fallback: try: cur.execute(q) except Exception: 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"): try: cur.execute(f"DROP TRIGGER IF EXISTS {name}") except Exception: pass for q in triggers_fallback: try: cur.execute(q) except Exception: pass cnx.commit() # ExĂ©cution Ă  l’import try: ensure_schema() except Exception as e: st.warning(f"Init schĂ©ma Journal_Erreurs : {e}") # ========================================================= # Connexion utilisateur # ========================================================= if not st.session_state.get("authenticated", False): login = st.sidebar.text_input("Nom d'utilisateur") password = st.sidebar.text_input("Mot de passe", type="password") if st.sidebar.button("Se connecter"): try: conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute( """ SELECT NomUtilisateur, role, MotDePasseHash, Site, DateExpiration FROM Acces.Utilisateurs WHERE NomUtilisateur = %s LIMIT 1 """, (login,), ) result = cursor.fetchone() if not result: st.sidebar.error("Identifiants invalides") elif result["DateExpiration"] and result["DateExpiration"] < date.today(): st.sidebar.error("⛔ AccĂšs expirĂ©.") elif not verifier_password(password, result["MotDePasseHash"]): st.sidebar.error("Identifiants invalides") else: st.session_state.update({ "authenticated": True, "role": result["role"], "site_autorise": result["Site"], "onglet_actif": "Accueil", # 👈 reset }) now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute( """ INSERT INTO Sondes.Connexion_Log (Utilisateur, Lieu, Date_Connexion) VALUES (%s, %s, %s) """, (result["NomUtilisateur"], result["Site"], now_str), ) conn.commit() st.rerun() cursor.close() conn.close() except Exception as e: st.sidebar.error(f"Erreur connexion : {e}") else: st.sidebar.success(f"ConnectĂ© ({st.session_state.get('role')})") if st.sidebar.button("🔓 DĂ©connexion"): for key in ["authenticated", "role", "site_autorise"]: st.session_state[key] = False if key == "authenticated" else None st.session_state["onglet_actif"] = "Accueil" # 👈 reset st.rerun() # ========================================================= # PDF # ========================================================= def generer_pdf(site, 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 pdf_cursor.execute( f"SELECT Sonde, Date, Temperature FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", (date_str,), ) rows = pdf_cursor.fetchall() df = pd.DataFrame(rows) if df.empty: st.warning("Aucune donnĂ©e ce jour.") return df["Heure"] = pd.to_datetime(df["Date"]).dt.strftime("%H:%M") 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))) if heure_debut < heure_fin: df = df[(df["Heure_obj"] >= heure_debut) & (df["Heure_obj"] <= heure_fin)] else: df = df[(df["Heure_obj"] >= heure_debut) | (df["Heure_obj"] <= heure_fin)] releves = {} for sonde in sorted(df["Sonde"].unique()): df_sonde = df[df["Sonde"] == sonde] releves[sonde] = list(zip(df_sonde["Heure"], df_sonde["Temperature"])) # Alertes table_alertes = f"Alertes_{site}" pdf_cursor.execute( f"SELECT Sonde, Debut_defaut, Etat FROM `{table_alertes}` WHERE DATE(Debut_defaut) = %s", (date_str,), ) alertes = pdf_cursor.fetchall() pdf_cursor.close() conn.close() class RapportPDF(FPDF): def header(self): self.set_font("Arial", "B", 14) self.cell(0, 10, "Rapport de surveillance des sondes", ln=1, align="C") self.set_font("Arial", "", 12) self.cell(0, 8, f"Date : {date_str}", ln=1, align="C") self.cell(0, 8, f"PĂ©riode : {getattr(self, 'periode', '')}", ln=1, align="C") self.ln(4) def site_info(self, site_name): self.set_font("Arial", "B", 12) self.cell(0, 8, f"Site : {site_name}", ln=1) self.ln(2) def releves_section(self, data): self.set_font("Arial", "B", 12) self.cell(0, 8, "RelevĂ©s de tempĂ©rature", ln=1) for sonde, mesures in data.items(): self.set_font("Arial", "B", 11) self.cell(0, 7, f"Sonde : {sonde}", ln=1) col1 = mesures[::2] col2 = mesures[1::2] self.set_font("Arial", "B", 10) self.cell(40, 6, "Heure", border=1) self.cell(30, 6, "Temp (°C)", border=1) self.cell(20, 6, "", border=0) self.cell(40, 6, "Heure", border=1) self.cell(30, 6, "Temp (°C)", border=1) self.ln() self.set_font("Arial", "", 10) for i in range(max(len(col1), len(col2))): if i < len(col1): h1, t1 = col1[i] self.cell(40, 6, h1, border=1) self.cell(30, 6, f"{t1:.2f}", border=1) else: self.cell(70, 6, "", border=0) self.cell(20, 6, "", border=0) if i < len(col2): h2, t2 = col2[i] self.cell(40, 6, h2, border=1) self.cell(30, 6, f"{t2:.2f}", border=1) self.ln() self.ln(3) def alertes_section(self, data): self.set_font("Arial", "B", 12) self.cell(0, 8, "Alertes enregistrĂ©es", ln=1) self.set_font("Arial", "", 10) if not data: self.cell(0, 6, "Aucune alerte ce jour.", ln=1) else: for a in data: self.cell(0, 6, f"{a['Sonde']} - {a['Debut_defaut']} - {a['Etat']}", ln=1) pdf = RapportPDF() pdf.periode = periode pdf.add_page() pdf.site_info(site) pdf.releves_section(releves) pdf.alertes_section(alertes) output_dir = "PDF" os.makedirs(output_dir, exist_ok=True) file_name = f"rapport_{site}_{date_str}.pdf" output_path = os.path.join(output_dir, file_name) pdf.output(output_path) with open(output_path, "rb") as f: st.download_button( label="đŸ“„ TĂ©lĂ©charger le rapport PDF", data=f, file_name=file_name, mime="application/pdf", ) except Exception as err1: st.error(f"Erreur lors de la gĂ©nĂ©ration du PDF : {err1}") # ========================================================= # Fonctions Journal erreurs (SQL) # ========================================================= def _get_site_courant(): role = st.session_state.get("role") if role != "superviseur": return st.session_state.get("site_autorise") return st.session_state.get("selected_site", "Saclay") def load_alertes(site: str, jour: date): table_alertes = f"Alertes_{site}" q = f""" SELECT a.Id AS Source_Id, %s AS Site, a.Sonde AS Sonde, DATE(a.Debut_defaut) AS DateJour, 'Alerte' AS Type, CONCAT('Alerte ', a.Etat, ' depuis ', DATE_FORMAT(a.Debut_defaut,'%H:%i')) AS Resume, a.Etat AS Etat FROM `{table_alertes}` a WHERE 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: cur.execute(q, (site, jour, jour)) rows = cur.fetchall() cols = ["Source_Id","Site","Sonde","DateJour","Type","Resume","Etat"] return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) def load_anomalies_auto(site: str, jour: date): """ 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 gap_threshold_min = 20 # seuil "trou" (ex. mesures toutes 5 min → 20 min = 4 crĂ©neaux manquĂ©s) jump_deg = 10 # saut suspect min_phys, max_phys = -60, 120 q = f""" WITH J AS ( SELECT Sonde, `Date`, Temperature FROM `{table_mesures}` WHERE DATE(`Date`) = %s ), L AS ( SELECT Sonde, `Date`, Temperature, LAG(`Date`) OVER (PARTITION BY Sonde ORDER BY `Date`) AS prev_date, LAG(Temperature) OVER (PARTITION BY Sonde ORDER BY `Date`) AS prev_temp FROM J ), gaps AS ( SELECT Sonde, COUNT(*) AS nb_gaps, MAX(TIMESTAMPDIFF(MINUTE, prev_date, `Date`)) AS max_gap_min FROM L WHERE prev_date IS NOT NULL AND TIMESTAMPDIFF(MINUTE, prev_date, `Date`) >= {gap_threshold_min} GROUP BY Sonde ), jumps AS ( SELECT Sonde, COUNT(*) AS nb_jumps, MAX(ABS(Temperature - prev_temp)) AS max_jump FROM L WHERE prev_temp IS NOT NULL AND ABS(Temperature - prev_temp) > {jump_deg} GROUP BY Sonde ), bounds AS ( SELECT Sonde, MIN(Temperature) AS tmin, MAX(Temperature) AS tmax FROM J GROUP BY Sonde HAVING MIN(Temperature) < {min_phys} OR MAX(Temperature) > {max_phys} ) SELECT NULL AS Source_Id, %s AS Site, g.Sonde AS Sonde, %s AS DateJour, 'Auto' AS Type, CONCAT('Trous de mesures : ', nb_gaps, ' fois (max ', max_gap_min, ' min)') AS Resume FROM gaps g UNION ALL SELECT NULL, %s, j.Sonde, %s, 'Auto', CONCAT('Sauts de tempĂ©rature suspects : ', nb_jumps, ' (max ', ROUND(max_jump,1), '°C)') FROM jumps j UNION ALL SELECT NULL, %s, b.Sonde, %s, 'Auto', CONCAT('Valeurs hors bornes physiques (min=', ROUND(tmin,1), '°C, max=', ROUND(tmax,1), '°C)') FROM bounds b ORDER BY Sonde; """ params = (jour, site, jour, site, jour, site, jour) with closing(get_conn()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: cur.execute(q, params) rows = cur.fetchall() cols = ["Source_Id","Site","Sonde","DateJour","Type","Resume"] return pd.DataFrame(rows, columns=cols) if rows else pd.DataFrame(columns=cols) def load_journal_existants(site: str, jour: date): q = """ SELECT Id, Site, Sonde, DateJour, Type, Source_Id, Resume, Statut, Priorite, Assignation, Commentaire, Tag FROM Sondes.Journal_Erreurs WHERE Site=%s AND DateJour=%s; """ with closing(get_conn()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: cur.execute(q, (site, jour)) rows = cur.fetchall() 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) def upsert_journal(rows: list[dict]): if not rows: return q_insert = """ INSERT INTO Sondes.Journal_Erreurs (Site, Sonde, DateJour, Type, Source_Id, Resume, Statut, Priorite, Assignation, Commentaire, Tag) VALUES (%(Site)s, %(Sonde)s, %(DateJour)s, %(Type)s, %(Source_Id)s, %(Resume)s, %(Statut)s, %(Priorite)s, %(Assignation)s, %(Commentaire)s, %(Tag)s) ON DUPLICATE KEY UPDATE Resume=VALUES(Resume), Statut=VALUES(Statut), Priorite=VALUES(Priorite), Assignation=VALUES(Assignation), Commentaire=VALUES(Commentaire), Tag=VALUES(Tag), UpdatedAt=CURRENT_TIMESTAMP; """ with closing(get_conn()) as cnx, closing(cnx.cursor()) as cur: cur.executemany(q_insert, rows) cnx.commit() # ========================================================= # Page Journal erreurs # ========================================================= def page_journal_erreurs(): st.header("📝 Journal des erreurs") site = _get_site_courant() if not site: st.warning("Veuillez d’abord choisir un site sur la page d’accueil.") return if site not in SITES_AUTORISES: st.error("Site invalide.") return 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')}") # --- Chargement des sources df_alertes = load_alertes(site, jour) df_auto = load_anomalies_auto(site, jour) df_saved = load_journal_existants(site, jour) base = pd.concat([df_auto, df_alertes], ignore_index=True) if base.empty and df_saved.empty: st.info("Aucune anomalie dĂ©tectĂ©e ni alerte pour cette date.") return # Colonnes attendues for col in ["Statut","Priorite","Assignation","Commentaire","Tag","Id","Source_Id"]: if col not in base.columns: base[col] = pd.NA key_join = ["Site","Sonde","DateJour","Type","Source_Id","Resume"] df = base.merge(df_saved, on=key_join, how="left", suffixes=("","_saved")) # Garantir l'existence des *_saved si df_saved est vide for c in ["Statut_saved","Priorite_saved","Assignation_saved","Commentaire_saved", "Tag_saved","Id_saved","Source_Id_saved"]: 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: df[c] = df[c].astype("string").fillna("") # tout en string, pas de NaN # PrioritĂ© = entier nullable (Ă©vite 3.0) df["Priorite"] = pd.to_numeric(df["Priorite"], errors="coerce").fillna(3).astype("Int64") # --- Éditeur st.subheader("SynthĂšse (Ă©ditable)") edit_cols = ["Sonde","Type","Resume","Statut","Priorite","Assignation","Tag","Commentaire"] disabled_cols = ["Sonde","Type","Resume"] st.subheader("SynthĂšse (Ă©ditable)") edit_cols = ["Sonde", "Type", "Resume", "Statut", "Priorite", "Assignation", "Tag", "Commentaire"] disabled_cols = ["Sonde", "Type", "Resume"] editable = st.data_editor( df[edit_cols].copy(), # <<< important disabled=disabled_cols, use_container_width=True, hide_index=True, column_config={ "Statut": st.column_config.SelectboxColumn( "Statut", options=["Nouveau", "En cours", "PlanifiĂ©", "Clos"], help="État de la tĂąche" ), "Priorite": st.column_config.NumberColumn( "PrioritĂ©", min_value=1, max_value=3, step=1, format="%d", help="1=Haute, 2=Moyenne, 3=Basse" ), "Assignation": st.column_config.TextColumn("Assignation", help="Technicien / ticket"), "Tag": st.column_config.TextColumn("Tag", help="Intermittent, RĂ©seau, Capteur, etc."), "Commentaire": st.column_config.TextColumn( "Commentaire", help="Notes (SHIFT+EntrĂ©e pour un saut de ligne)", max_chars=2000 ), } ) # --- Sauvegarde def _none_if_empty(x): if x is None: return None if isinstance(x, float) and pd.isna(x): return None if isinstance(x, str) and x.strip() == "": return None return x if st.session_state.get("role") == "superviseur": 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[["Site","Sonde","DateJour","Type","Source_Id","Resume"]] df_to_save = editable.merge(df_keys, on=["Sonde","Type","Resume"], how="left") payload = [] for _, r in df_to_save.iterrows(): payload.append({ "Site": r["Site"], "Sonde": r["Sonde"], "DateJour": r["DateJour"], "Type": r["Type"], "Source_Id": int(r["Source_Id"]) if pd.notna(r.get("Source_Id")) else None, "Resume": r["Resume"], "Statut": _none_if_empty(r.get("Statut")) or "Nouveau", "Priorite": int(r.get("Priorite", 3)) if pd.notna(r.get("Priorite", 3)) else 3, "Assignation": _none_if_empty(r.get("Assignation")), "Commentaire": _none_if_empty(r.get("Commentaire")), "Tag": _none_if_empty(r.get("Tag")), }) upsert_journal(payload) st.success("Journal mis Ă  jour.") else: st.info("Lecture seule (rĂ©servĂ© aux superviseurs pour l’édition).") # ========================================================= # Affichage ALERTES non acquittĂ©es (global) # ========================================================= if st.session_state.get("authenticated"): try: role = st.session_state.get("role") site_selectionne = ( st.session_state.get("site_autorise") if role != "superviseur" else st.session_state.get("selected_site", "Saclay") ) if not site_selectionne: st.info("Connectez-vous et choisissez un site pour afficher les alertes.") 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}" cursor.execute( f"SELECT Sonde, Debut_defaut, Etat " f"FROM `{table_alertes}` " f"WHERE Etat != 'AcquittĂ©' " f"ORDER BY Debut_defaut DESC" ) alertes = cursor.fetchall() if alertes: df_alertes = pd.DataFrame(alertes) st.subheader("🚹 Alertes non acquittĂ©es") st.dataframe(df_alertes, use_container_width=True) else: st.success("✅ Aucune alerte en cours.") cursor.close() conn.close() except Exception as e: st.error(f"Erreur lors de la rĂ©cupĂ©ration des alertes : {e}") else: st.info("Connectez-vous pour voir les alertes en cours.") # ========================================================= # Navigation # ========================================================= if st.session_state["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) if st.session_state.get("onglet_actif") not in onglets: st.session_state["onglet_actif"] = onglets[0] onglet_selectionne = st.sidebar.radio( "📁 Navigation", onglets, index=onglets.index(st.session_state["onglet_actif"]) ) site_actuel = ( st.session_state.get("site_autorise") if st.session_state["role"] != "superviseur" else st.session_state.get("selected_site", "Saclay") ) date_selectionnee = st.session_state.get("selected_date", date.today()) # ------------------ Accueil ------------------ if onglet_selectionne == "Accueil": try: 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_selectionnee = st.date_input("📅 Date du relevĂ©", value=date_selectionnee) st.session_state["selected_date"] = date_selectionnee cursor.execute( f"SELECT * FROM `{site_actuel}` WHERE DATE(Date) = %s ORDER BY Sonde, Date DESC", (date_selectionnee.strftime("%Y-%m-%d"),), ) rows = cursor.fetchall() if rows: df = pd.DataFrame(rows) df["Date"] = pd.to_datetime(df["Date"]) sondes = sorted(df["Sonde"].unique()) sonde_choisie = st.selectbox("đŸ§Ș Choisissez une sonde :", sondes) df_sonde = df[df["Sonde"] == sonde_choisie].copy() df_sonde["Heure"] = df_sonde["Date"].dt.hour tranche = st.radio( "🕒 Tranche horaire :", ["Toute la journĂ©e", "Matin (6h-12h)", "AprĂšs-midi (12h-18h)", "Nuit (18h-6h)"], ) st.session_state["selected_periode"] = tranche # GĂ©nĂ©ration PDF if st.button("đŸ§Ÿ GĂ©nĂ©rer le PDF du jour"): generer_pdf( site_actuel, date_selectionnee.strftime("%Y-%m-%d"), st.session_state.get("selected_periode", "Toute la journĂ©e"), ) if tranche == "Matin (6h-12h)": df_sonde = df_sonde[(df_sonde["Heure"] >= 6) & (df_sonde["Heure"] < 12)] elif tranche == "AprĂšs-midi (12h-18h)": df_sonde = df_sonde[(df_sonde["Heure"] >= 12) & (df_sonde["Heure"] < 18)] elif tranche == "Nuit (18h-6h)": df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)] seuil_temp = 10 cursor.execute( "SELECT Temp_Max FROM Sondes.Chambres_froides WHERE Lieu = %s AND Sonde = %s", (site_actuel, sonde_choisie), ) seuil = cursor.fetchone() if seuil: seuil_temp = seuil["Temp_Max"] st.subheader("📊 Tableau des relevĂ©s") def surlignage_temp(val): try: if float(val) > seuil_temp: return "color: red; font-weight: bold" except Exception: pass return "" styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"]) st.dataframe(styled_df, use_container_width=True) st.subheader("📈 Évolution de la tempĂ©rature") fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") ax.axhline(seuil_temp, linestyle="--", label=f"Seuil {seuil_temp}°C") ax.set_xlabel("Heure") ax.set_ylabel("TempĂ©rature (°C)") ax.set_title(f"{sonde_choisie} - {date_selectionnee.strftime('%d/%m/%Y')}") ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) ax.legend() st.pyplot(fig) cursor.close() conn.close() except Exception as e: st.error(f"Erreur : {e}") st.text(traceback.format_exc()) # ------------------ Statistiques ------------------ elif onglet_selectionne == "Statistiques": st.markdown("## 📈 Statistiques de tempĂ©rature") site = ( st.session_state["site_autorise"] if st.session_state["role"] != "superviseur" else st.session_state.get("selected_site", "Saclay") ) date_val = st.session_state.get("selected_date", date.today()) try: conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute( f"SELECT * FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", (date_val.strftime("%Y-%m-%d"),), ) rows = cursor.fetchall() df = pd.DataFrame(rows) if df.empty: st.info("Aucune donnĂ©e pour cette date.") else: df["Date"] = pd.to_datetime(df["Date"]) sondes = sorted(df["Sonde"].unique()) sonde = st.selectbox("Choisir une sonde :", sondes, key="selectbox_stats") df_sonde = df[df["Sonde"] == sonde] st.subheader("Évolution journaliĂšre") fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(df_sonde["Date"], df_sonde["Temperature"], marker="o") ax.set_title(f"{sonde} - {date_val.strftime('%d/%m/%Y')}") ax.set_xlabel("Heure") ax.set_ylabel("TempĂ©rature (°C)") ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M")) st.pyplot(fig) cursor.close() conn.close() except Exception as e: st.error(f"Erreur chargement statistiques : {e}") # Admin Chambres froides if st.session_state["role"] == "superviseur": with st.expander("đŸ› ïž Gestion des chambres froides (administrateur)", expanded=True): if st.button("🔄 Actualiser la liste"): st.session_state["refresh_admin"] = random.randint(0, 9999) try: conn_admin = get_connection() cursor_admin = conn_admin.cursor(dictionary=True) cursor_admin.execute("SELECT * FROM Sondes.Chambres_froides WHERE Lieu = %s", (site,)) chambres = cursor_admin.fetchall() if not chambres: st.warning("Aucune chambre froide pour ce site.") else: for chambre in chambres: col1, col2, col3 = st.columns([3, 1, 2]) with col1: st.markdown(f"**{chambre['Sonde']}**") with col2: etat = st.checkbox( "ON", value=(chambre["Etat"] == "ON"), key=f"etat_{chambre['Id']}_{st.session_state.get('refresh_admin', 0)}", ) new_etat = "ON" if etat else "OFF" with col3: temp_max = chambre["Temp_Max"] moins, temp_display, plus = st.columns([1, 2, 1]) with moins: if st.button("â–Œ", key=f"moins_{chambre['Id']}"): temp_max -= 1 with temp_display: st.markdown( f"
{temp_max}°C
", unsafe_allow_html=True, ) with plus: if st.button("â–Č", key=f"plus_{chambre['Id']}"): temp_max += 1 if new_etat != chambre["Etat"] or temp_max != chambre["Temp_Max"]: cursor_admin.execute( "UPDATE Sondes.Chambres_froides SET Etat = %s, Temp_Max = %s WHERE Id = %s", (new_etat, temp_max, chambre["Id"]), ) conn_admin.commit() st.success(f"{chambre['Sonde']} mise Ă  jour") cursor_admin.close() conn_admin.close() except Exception as 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()) # ------------------ Traffic ------------------ elif onglet_selectionne == "Traffic": st.header("🚩 Connexions rĂ©centes") try: conn = get_connection() cursor = conn.cursor(dictionary=True) cursor.execute( "SELECT Utilisateur, Lieu, Date_Connexion FROM Sondes.Connexion_Log ORDER BY Date_Connexion DESC LIMIT 100" ) logs = cursor.fetchall() df_logs = pd.DataFrame(logs) st.dataframe(df_logs, use_container_width=True) cursor.close() conn.close() except Exception as e: st.error(f"Erreur : {e}") # ------------------ Journal erreurs ------------------ elif onglet_selectionne == "Journal erreurs": page_journal_erreurs()