Consolidation de Domo91
This commit is contained in:
17
.env
17
.env
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
482
app/domo91.py
482
app/domo91.py
@@ -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 l’application 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 à l’import
|
|
||||||
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,7 +531,7 @@ 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"]
|
||||||
@@ -546,18 +539,21 @@ def load_anomalies_auto(site: str, jour: date):
|
|||||||
|
|
||||||
|
|
||||||
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 d’abord choisir un site sur la page d’accueil.")
|
st.warning("Veuillez d’abord choisir un site sur la page d’accueil.")
|
||||||
@@ -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}")
|
||||||
|
st.text(traceback.format_exc())
|
||||||
else:
|
else:
|
||||||
st.info("Connectez-vous pour voir les alertes en cours.")
|
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 à l’application.")
|
||||||
|
|||||||
Reference in New Issue
Block a user