diff --git a/app/assets/Logo.png b/app/assets/Logo.png index 0e045d7..6aa2d63 100644 Binary files a/app/assets/Logo.png and b/app/assets/Logo.png differ diff --git a/app/assets/QR_Domo91FR.png b/app/assets/QR_Domo91FR.png new file mode 100644 index 0000000..f88c518 Binary files /dev/null and b/app/assets/QR_Domo91FR.png differ diff --git a/app/assets/qr_domo91.png b/app/assets/qr_domo91.png new file mode 100644 index 0000000..f88c518 Binary files /dev/null and b/app/assets/qr_domo91.png differ diff --git a/app/domo91.py b/app/domo91.py index 786ccfe..e21612d 100644 --- a/app/domo91.py +++ b/app/domo91.py @@ -4,12 +4,12 @@ import random import traceback from datetime import datetime, date, time from contextlib import closing - import bcrypt import matplotlib.dates as mdates import matplotlib.pyplot as plt import mysql.connector import pandas as pd + pd.set_option("future.no_silent_downcasting", True) import streamlit as st from PIL import Image @@ -17,7 +17,6 @@ from dotenv import find_dotenv, load_dotenv from fpdf import FPDF from streamlit_autorefresh import st_autorefresh - # ========================================================= # Config de page # ========================================================= @@ -32,7 +31,6 @@ st.sidebar.image(logo, use_container_width=True) st.title("📊 Domo91 - Surveillance des sondes") st.write("Bienvenue sur l’application de supervision.") - # ========================================================= # ENV & DB # ========================================================= @@ -62,6 +60,24 @@ def assert_site_ok(site_name: str): if site_name not in SITES_AUTORISES: raise ValueError(f"Site invalide: {site_name}") + +def get_chambres_froides_actives(conn, site: str): + """Retourne la liste des chambres froides actives (Etat=ON) pour un site, avec leur Temp_Max.""" + assert_site_ok(site) + with closing(conn.cursor(dictionary=True)) as cur: + cur.execute( + """ + SELECT Sonde, Temp_Max + FROM Sondes.Chambres_froides + WHERE Lieu = %s + AND UPPER(Etat) = 'ON' + ORDER BY Sonde + """, + (site,), + ) + return cur.fetchall() + + # ========================================================= # Session state # ========================================================= @@ -76,6 +92,7 @@ for key, default in { }.items(): st.session_state.setdefault(key, default) + # ========================================================= # Sécurité mots de passe # ========================================================= @@ -95,10 +112,11 @@ def fetch_gyro(site_name: str): q = """ SELECT Etat, `Date` FROM Sondes.v_gyro_last - WHERE Lieu = %s AND Sonde = 'Gyro' + WHERE Lieu = %s \ + AND Sonde = 'Gyro' ORDER BY `Date` DESC - LIMIT 1 - """ + LIMIT 1 \ + """ with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: cur.execute(q, (site_name,)) # <-- FIX row = cur.fetchone() @@ -157,46 +175,52 @@ def render_gyro_badge(site_name: str, stale_after_min: int = 10): # ========================================================= 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; - """ + 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; - """ + 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)", @@ -205,18 +229,20 @@ def ensure_schema(): triggers_fallback = [ """ CREATE TRIGGER trg_je_bi - BEFORE INSERT ON Sondes.Journal_Erreurs - FOR EACH ROW + BEFORE INSERT + ON Sondes.Journal_Erreurs + FOR EACH ROW BEGIN - SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); + SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); END """, """ CREATE TRIGGER trg_je_bu - BEFORE UPDATE ON Sondes.Journal_Erreurs - FOR EACH ROW + BEFORE UPDATE + ON Sondes.Journal_Erreurs + FOR EACH ROW BEGIN - SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); + SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0); END """, ] @@ -251,7 +277,6 @@ try: except Exception as e: st.warning(f"Init schéma Journal_Erreurs : {e}") - # ========================================================= # Connexion utilisateur # ========================================================= @@ -259,7 +284,21 @@ 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"): + # --- Bouton connexion --- + clicked = st.sidebar.button("Se connecter") + + # --- QR code SOUS le bouton --- + st.sidebar.markdown("---") + st.sidebar.caption("Accès rapide depuis un smartphone :") + qr_path = os.path.join(BASE_DIR, "assets", "QR_Domo91FR.png") + + if os.path.exists(qr_path): + st.sidebar.image(qr_path, width=180) + else: + st.sidebar.error(f"QR code introuvable : {qr_path}") + + # --- Traitement de la connexion --- + if clicked: try: with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: cursor.execute( @@ -310,12 +349,21 @@ else: st.rerun() +# ========================================================= +# PDF +# ========================================================= # ========================================================= # PDF # ========================================================= def generer_pdf(site_name: str, date_str: str, periode: str): + """ + Génère un PDF de relevés + alertes pour un site et une date. + - site_name : nom de table (Saclay/Meudon) + - date_str : YYYY-MM-DD + - periode : libellé de tranche horaire + """ assert_site_ok(site_name) - 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_name} à la date {date_str} ({periode})") plages = { "Toute la journée": (time(0, 0), time(23, 59)), @@ -326,8 +374,9 @@ def generer_pdf(site_name: str, date_str: str, periode: str): try: with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cur: + # Mesures cur.execute( - f"SELECT Sonde, Date, Temperature FROM `{site}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", + f"SELECT Sonde, Date, Temperature FROM `{site_name}` WHERE DATE(Date) = %s ORDER BY Sonde, Date", (date_str,), ) rows = cur.fetchall() @@ -351,7 +400,8 @@ def generer_pdf(site_name: str, date_str: str, periode: str): df_sonde = df[df["Sonde"] == sonde] releves[sonde] = list(zip(df_sonde["Heure"], df_sonde["Temperature"])) - table_alertes = f"Alertes_{site}" + # Alertes du jour + table_alertes = f"Alertes_{site_name}" cur.execute( f"SELECT Sonde, Debut_defaut, Etat FROM `{table_alertes}` WHERE DATE(Debut_defaut) = %s", (date_str,), @@ -363,15 +413,11 @@ def generer_pdf(site_name: str, date_str: str, periode: str): 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"Site : {getattr(self, 'site_name', '')}", ln=1, align="C") 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) @@ -405,6 +451,8 @@ def generer_pdf(site_name: str, date_str: str, periode: str): h2, t2 = col2[i] self.cell(40, 6, h2, border=1) self.cell(30, 6, f"{t2:.2f}", border=1) + else: + self.cell(70, 6, "", border=0) self.ln() self.ln(3) @@ -419,15 +467,15 @@ def generer_pdf(site_name: str, date_str: str, periode: str): self.cell(0, 6, f"{a['Sonde']} - {a['Debut_defaut']} - {a['Etat']}", ln=1) pdf = RapportPDF() + pdf.site_name = site_name pdf.periode = periode pdf.add_page() - pdf.site_info(site) pdf.releves_section(releves) pdf.alertes_section(alertes) - output_dir = "../PDF" + output_dir = os.path.join(BASE_DIR, "PDF") os.makedirs(output_dir, exist_ok=True) - file_name = f"rapport_{site}_{date_str}.pdf" + file_name = f"rapport_{site_name}_{date_str}.pdf" output_path = os.path.join(output_dir, file_name) pdf.output(output_path) @@ -551,11 +599,22 @@ def load_anomalies_auto(site_name: str, jour: date): def load_journal_existants(site: str, jour: date): assert_site_ok(site) 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; - """ + 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_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur: cur.execute(q, (site, jour)) rows = cur.fetchall() @@ -568,19 +627,18 @@ 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; - """ + 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_connection()) as cnx, closing(cnx.cursor()) as cur: cur.executemany(q_insert, rows) cnx.commit() @@ -629,11 +687,11 @@ def page_journal_erreurs(): df_saved["Source_Id_norm"] = df_saved["Source_Id"].apply(_source_norm) 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) + 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: @@ -641,11 +699,11 @@ def page_journal_erreurs(): # 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) + 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( @@ -798,7 +856,6 @@ if st.session_state.get("authenticated"): 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( @@ -827,41 +884,18 @@ if st.session_state.get("authenticated"): 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( - """ - SELECT Sonde, Temp_Max - FROM Sondes.Chambres_froides - WHERE Lieu = %s - AND UPPER(Etat) = 'ON' - ORDER BY Sonde - """, - (site_actuel,), - ) - rows_mesures = [] # important pour éviter NameError - - with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: - cursor.execute( - """ - SELECT Sonde, Temp_Max - FROM Sondes.Chambres_froides - WHERE Lieu = %s - AND UPPER(Etat) = 'ON' - ORDER BY Sonde - """, - (site_actuel,), - ) - cfg_on = cursor.fetchall() + # 1) Charger config (sondes ON + seuils) puis mesures du jour + with closing(get_connection()) as conn: + cfg_on = get_chambres_froides_actives(conn, site_actuel) sondes_on = [r["Sonde"] for r in cfg_on] seuils_on = {r["Sonde"]: float(r["Temp_Max"]) for r in cfg_on} - # IMPORTANT: test avant la requête mesures if not sondes_on: st.warning("Aucune sonde active (Etat=ON) dans Chambres_froides pour ce site.") st.stop() - with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor: + with closing(conn.cursor(dictionary=True)) as cur_mesures: placeholders = ", ".join(["%s"] * len(sondes_on)) q = f""" SELECT Sonde, Date, Temperature @@ -871,72 +905,68 @@ if st.session_state.get("authenticated"): ORDER BY Sonde, Date DESC """ params = [date_selectionnee.strftime("%Y-%m-%d")] + sondes_on - cursor.execute(q, params) - rows_mesures = cursor.fetchall() + cur_mesures.execute(q, params) + rows_mesures = cur_mesures.fetchall() - if rows_mesures: - df = pd.DataFrame(rows_mesures) - df["Date"] = pd.to_datetime(df["Date"]) + if rows_mesures: + df = pd.DataFrame(rows_mesures) + df["Date"] = pd.to_datetime(df["Date"]) - sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes_on) - df_sonde = df[df["Sonde"] == sonde_choisie].copy() - df_sonde["Heure"] = df_sonde["Date"].dt.hour + sonde_choisie = st.selectbox("🧪 Choisissez une sonde :", sondes_on) + 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)"], + 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 + + 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"), ) - st.session_state["selected_periode"] = tranche - 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"), - ) + # Filtre tranche + 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)] - # Filtre tranche - 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 = seuils_on.get(sonde_choisie, 10.0) - seuil_temp = seuils_on.get(sonde_choisie, 10.0) - - if not df_sonde.empty: - st.subheader("📊 Tableau des relevés") + if not df_sonde.empty: + 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 + def surlignage_temp(val): + try: + return "color: red; font-weight: bold" if float(val) > seuil_temp else "" + except (ValueError, TypeError): return "" + styled_df = df_sonde.style.applymap(surlignage_temp, subset=["Temperature"]) + st.dataframe(styled_df, use_container_width=True) - 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) + 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) else: - st.info("Aucun relevé pour cette date.") + st.info("Aucun relevé dans la tranche horaire sélectionnée.") + else: + st.info("Aucun relevé pour cette date.") - if not sondes_on: - st.warning("Aucune sonde active (Etat=ON) dans Chambres_froides pour ce site.") - st.stop() except Exception as e: st.error(f"Erreur : {e}") st.text(traceback.format_exc()) @@ -991,7 +1021,8 @@ if st.session_state.get("authenticated"): st.session_state["refresh_admin"] = random.randint(0, 9999) try: - with closing(get_connection()) as conn_admin, closing(conn_admin.cursor(dictionary=True)) as cursor_admin: + with closing(get_connection()) as conn_admin, closing( + conn_admin.cursor(dictionary=True)) as cursor_admin: cursor_admin.execute("SELECT * FROM Sondes.Chambres_froides WHERE Lieu = %s", (site,)) chambres = cursor_admin.fetchall()