Correction bugs mineurs Domo91

This commit is contained in:
2025-12-16 10:26:11 +01:00
parent b4c2ca8400
commit 92b57df303
4 changed files with 204 additions and 173 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
app/assets/QR_Domo91FR.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
app/assets/qr_domo91.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -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 lapplication 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,9 +112,10 @@ 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
@@ -157,45 +175,51 @@ 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),
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;
) 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,
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;
) 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)",
@@ -205,7 +229,8 @@ def ensure_schema():
triggers_fallback = [
"""
CREATE TRIGGER trg_je_bi
BEFORE INSERT ON Sondes.Journal_Erreurs
BEFORE INSERT
ON Sondes.Journal_Erreurs
FOR EACH ROW
BEGIN
SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0);
@@ -213,7 +238,8 @@ def ensure_schema():
""",
"""
CREATE TRIGGER trg_je_bu
BEFORE UPDATE ON Sondes.Journal_Erreurs
BEFORE UPDATE
ON Sondes.Journal_Erreurs
FOR EACH ROW
BEGIN
SET NEW.Source_Id_norm = IFNULL(NEW.Source_Id, 0);
@@ -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,10 +599,21 @@ 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
SELECT Id, \
Site, \
Sonde, \
DateJour, \
Type, \
Source_Id, \
Resume,
Statut, \
Priorite, \
Assignation, \
Commentaire, \
Tag
FROM Sondes.Journal_Erreurs
WHERE Site=%s AND DateJour=%s;
WHERE Site = %s \
AND DateJour = %s; \
"""
with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
cur.execute(q, (site, jour))
@@ -572,14 +631,13 @@ def upsert_journal(rows: list[dict]):
(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;
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)
@@ -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,8 +905,8 @@ 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)
@@ -911,14 +945,11 @@ if st.session_state.get("authenticated"):
def surlignage_temp(val):
try:
if float(val) > seuil_temp:
return "color: red; font-weight: bold"
except Exception:
pass
return "color: red; font-weight: bold" if float(val) > seuil_temp else ""
except (ValueError, TypeError):
return ""
styled_df = df_sonde.style.map(surlignage_temp, subset=["Temperature"])
styled_df = df_sonde.style.applymap(surlignage_temp, subset=["Temperature"])
st.dataframe(styled_df, use_container_width=True)
st.subheader("📈 Évolution de la température")
@@ -931,12 +962,11 @@ if st.session_state.get("authenticated"):
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
ax.legend()
st.pyplot(fig)
else:
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()