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"}]
|
||||
|
||||
|
||||
# MQTT Saclay
|
||||
MQTT_HOST=54.36.188.119
|
||||
MQTT_USER=Bwps
|
||||
MQTT_PASS=scJ5ACj2keRfI^
|
||||
|
||||
# --- MQTT Meudon ---
|
||||
MQTT_HOST_MEUDON=162.19.78.131
|
||||
MQTT_USER_MEUDON=sondes
|
||||
MQTT_PASS_MEUDON=3J@bjYP0
|
||||
# MQTT
|
||||
MQTT_HOST=162.19.78.131
|
||||
MQTT_USER=sondes
|
||||
MQTT_PASS=3J@bjYP0
|
||||
MQTT_PORT_MEUDON=1883
|
||||
|
||||
# Topic gyrophare Meudon
|
||||
GYRO_MQTT_TOPIC_MEUDON=Meudon/gyrophare
|
||||
|
||||
|
||||
# Boucle rapide du gyro
|
||||
GYRO_MODE=mqtt
|
||||
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_NAME = os.getenv("DB_NAME")
|
||||
|
||||
# --- MQTT Meudon ---
|
||||
MQTT_HOST = os.getenv("MQTT_HOST_MEUDON")
|
||||
MQTT_USER = os.getenv("MQTT_USER_MEUDON")
|
||||
MQTT_PASS = os.getenv("MQTT_PASS_MEUDON")
|
||||
MQTT_PORT = int(os.getenv("MQTT_PORT_MEUDON", "1883"))
|
||||
# --- MQTT ---
|
||||
MQTT_HOST = os.getenv("MQTT_HOST")
|
||||
MQTT_USER = os.getenv("MQTT_USER")
|
||||
MQTT_PASS = os.getenv("MQTT_PASS")
|
||||
MQTT_PORT = int(os.getenv("MQTT_PORT", "1883"))
|
||||
|
||||
# Client ID (configurable, sinon suffixé avec le hostname)
|
||||
MQTT_CLIENT_ID = os.getenv(
|
||||
|
||||
@@ -26,9 +26,9 @@ DB_USER = os.getenv("DB_USER")
|
||||
DB_PASS = os.getenv("DB_PASS")
|
||||
DB_NAME = os.getenv("DB_NAME")
|
||||
|
||||
MQTT_HOST = os.getenv("MQTT_HOST")
|
||||
MQTT_USER = os.getenv("MQTT_USER")
|
||||
MQTT_PASS = os.getenv("MQTT_PASS")
|
||||
MQTT_HOST = "54.36.188.119"
|
||||
MQTT_USER = "Bwps"
|
||||
MQTT_PASS = "scJ5ACj2keRfI^"
|
||||
MQTT_PORT = int(os.getenv("MQTT_PORT", 1883))
|
||||
|
||||
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 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)
|
||||
pd.set_option("future.no_silent_downcasting", True)
|
||||
import streamlit as st
|
||||
from contextlib import closing
|
||||
from dotenv import find_dotenv, load_dotenv
|
||||
env_file = find_dotenv(usecwd=True)
|
||||
if env_file:
|
||||
load_dotenv(env_file)
|
||||
from fpdf import FPDF
|
||||
|
||||
# =========================================================
|
||||
@@ -28,7 +25,10 @@ st.write("Bienvenue sur l’application de supervision.")
|
||||
# =========================================================
|
||||
# ENV & DB
|
||||
# =========================================================
|
||||
load_dotenv()
|
||||
env_file = find_dotenv(usecwd=True)
|
||||
if env_file:
|
||||
load_dotenv(env_file)
|
||||
|
||||
db_config = {
|
||||
"host": os.getenv("DB_HOST"),
|
||||
"user": os.getenv("DB_USER"),
|
||||
@@ -37,15 +37,51 @@ db_config = {
|
||||
"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():
|
||||
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):
|
||||
"""Retourne (etat, ts) depuis la vue v_gyro_last pour le site donné."""
|
||||
assert_site_ok(site)
|
||||
q = """
|
||||
SELECT Etat, `Date`
|
||||
FROM Sondes.v_gyro_last
|
||||
@@ -53,9 +89,7 @@ def fetch_gyro(site: str):
|
||||
ORDER BY `Date` DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
cnx = get_connection()
|
||||
try:
|
||||
cur = cnx.cursor(dictionary=True)
|
||||
with closing(get_connection()) as cnx, closing(cnx.cursor(dictionary=True)) as cur:
|
||||
cur.execute(q, (site,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
@@ -63,36 +97,25 @@ def fetch_gyro(site: str):
|
||||
etat = (row.get("Etat") or "").strip().upper()
|
||||
ts = row.get("Date")
|
||||
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):
|
||||
"""Affiche un voyant Gyro (vert/rouge/orange) + fraîcheur des données."""
|
||||
etat, ts = fetch_gyro(site)
|
||||
|
||||
# Etat → couleur/label
|
||||
if etat in ("ON", "1"):
|
||||
color, label = "#ef4444", "GYRO ON" # Rouge = gyro actif
|
||||
color, label = "#ef4444", "GYRO ON"
|
||||
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"):
|
||||
color, label = "#f59e0b", "GYRO ALERTE" # Orange = alerte
|
||||
color, label = "#f59e0b", "GYRO ALERTE"
|
||||
else:
|
||||
color, label = "#9E9E9E", "GYRO INCONNU"
|
||||
|
||||
# Fraîcheur
|
||||
stale = True
|
||||
age_txt = "—"
|
||||
if ts is not None:
|
||||
try:
|
||||
# ts provient normalement de MySQL déjà en datetime
|
||||
from datetime import datetime as _dt
|
||||
now = _dt.now(ts.tzinfo) if hasattr(ts, "tzinfo") and ts.tzinfo else _dt.now()
|
||||
mins = int((now - ts).total_seconds() // 60)
|
||||
@@ -117,37 +140,10 @@ def render_gyro_badge(site: str, stale_after_min: int = 10):
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
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
|
||||
# Bootstrap schéma : Journal_Erreurs
|
||||
# =========================================================
|
||||
def ensure_schema():
|
||||
ddl_generated = """
|
||||
@@ -198,7 +194,7 @@ def ensure_schema():
|
||||
]
|
||||
triggers_fallback = [
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_je_bi
|
||||
CREATE TRIGGER trg_je_bi
|
||||
BEFORE INSERT ON Sondes.Journal_Erreurs
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
@@ -206,7 +202,7 @@ def ensure_schema():
|
||||
END
|
||||
""",
|
||||
"""
|
||||
CREATE TRIGGER IF NOT EXISTS trg_je_bu
|
||||
CREATE TRIGGER trg_je_bu
|
||||
BEFORE UPDATE ON Sondes.Journal_Erreurs
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
@@ -216,19 +212,17 @@ def ensure_schema():
|
||||
]
|
||||
|
||||
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)
|
||||
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:
|
||||
with closing(get_connection()) 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}")
|
||||
@@ -241,12 +235,13 @@ def ensure_schema():
|
||||
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
|
||||
# =========================================================
|
||||
@@ -256,8 +251,7 @@ if not st.session_state.get("authenticated", False):
|
||||
|
||||
if st.sidebar.button("Se connecter"):
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT NomUtilisateur, role, MotDePasseHash, Site, DateExpiration
|
||||
@@ -280,8 +274,9 @@ if not st.session_state.get("authenticated", False):
|
||||
"authenticated": True,
|
||||
"role": result["role"],
|
||||
"site_autorise": result["Site"],
|
||||
"onglet_actif": "Accueil", # 👈 reset
|
||||
"onglet_actif": "Accueil",
|
||||
})
|
||||
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -293,33 +288,40 @@ if not st.session_state.get("authenticated", False):
|
||||
conn.commit()
|
||||
st.rerun()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
st.sidebar.error(f"Erreur connexion : {e}")
|
||||
st.sidebar.text(traceback.format_exc())
|
||||
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.session_state["onglet_actif"] = "Accueil"
|
||||
st.rerun()
|
||||
|
||||
|
||||
# =========================================================
|
||||
# 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})")
|
||||
try:
|
||||
conn = get_connection()
|
||||
pdf_cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# Relevés
|
||||
pdf_cursor.execute(
|
||||
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)),
|
||||
}
|
||||
|
||||
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",
|
||||
(date_str,),
|
||||
)
|
||||
rows = pdf_cursor.fetchall()
|
||||
rows = cur.fetchall()
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
if df.empty:
|
||||
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_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)]
|
||||
@@ -346,16 +341,12 @@ def generer_pdf(site, date_str, periode):
|
||||
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(
|
||||
cur.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()
|
||||
alertes = cur.fetchall()
|
||||
|
||||
class RapportPDF(FPDF):
|
||||
def header(self):
|
||||
@@ -397,7 +388,9 @@ def generer_pdf(site, date_str, periode):
|
||||
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)
|
||||
@@ -436,11 +429,13 @@ def generer_pdf(site, date_str, periode):
|
||||
mime="application/pdf",
|
||||
)
|
||||
|
||||
except Exception as err1:
|
||||
st.error(f"Erreur lors de la génération du PDF : {err1}")
|
||||
except Exception as err:
|
||||
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():
|
||||
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("selected_site", "Saclay")
|
||||
|
||||
|
||||
def load_alertes(site: str, jour: date):
|
||||
assert_site_ok(site)
|
||||
table_alertes = f"Alertes_{site}"
|
||||
q = f"""
|
||||
SELECT
|
||||
@@ -463,22 +460,18 @@ def load_alertes(site: str, jour: date):
|
||||
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:
|
||||
with closing(get_connection()) 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
|
||||
"""
|
||||
assert_site_ok(site)
|
||||
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
|
||||
gap_threshold_min = 20
|
||||
jump_deg = 10
|
||||
min_phys, max_phys = -60, 120
|
||||
|
||||
q = f"""
|
||||
@@ -538,7 +531,7 @@ def load_anomalies_auto(site: str, jour: date):
|
||||
ORDER BY Sonde;
|
||||
"""
|
||||
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)
|
||||
rows = cur.fetchall()
|
||||
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):
|
||||
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;
|
||||
"""
|
||||
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))
|
||||
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)
|
||||
|
||||
|
||||
def upsert_journal(rows: list[dict]):
|
||||
if not rows:
|
||||
return
|
||||
@@ -575,15 +571,17 @@ def upsert_journal(rows: list[dict]):
|
||||
Tag=VALUES(Tag),
|
||||
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)
|
||||
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.")
|
||||
@@ -595,7 +593,6 @@ def page_journal_erreurs():
|
||||
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)
|
||||
@@ -605,54 +602,69 @@ def page_journal_erreurs():
|
||||
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
|
||||
def _source_norm(x):
|
||||
return 0 if pd.isna(x) else int(x)
|
||||
|
||||
key_join = ["Site","Sonde","DateJour","Type","Source_Id","Resume"]
|
||||
df = base.merge(df_saved, on=key_join, how="left", suffixes=("","_saved"))
|
||||
# base : garantir Source_Id + Source_Id_norm
|
||||
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
|
||||
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
|
||||
# df_saved : garantir colonnes et Key, même si vide
|
||||
if df_saved is None or df_saved.empty:
|
||||
df_saved = pd.DataFrame(columns=["Key", "Statut", "Priorite", "Assignation", "Commentaire", "Tag"])
|
||||
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")
|
||||
# --- Éditeur
|
||||
st.subheader("Synthèse (éditable)")
|
||||
edit_cols = ["Sonde","Type","Resume","Statut","Priorite","Assignation","Tag","Commentaire"]
|
||||
disabled_cols = ["Sonde","Type","Resume"]
|
||||
for c in ["Assignation", "Commentaire", "Tag"]:
|
||||
df[c] = df[c].astype("string").fillna("")
|
||||
|
||||
for c in ["Sonde", "Type", "Resume"]:
|
||||
df[c] = df[c].astype("string").fillna("")
|
||||
|
||||
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(
|
||||
df[edit_cols].copy(), # <<< important
|
||||
df[view_cols].copy(),
|
||||
disabled=disabled_cols,
|
||||
use_container_width=True,
|
||||
hide_index=True,
|
||||
column_config={
|
||||
"Key": st.column_config.TextColumn("Key", disabled=True),
|
||||
"Statut": st.column_config.SelectboxColumn(
|
||||
"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):
|
||||
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
|
||||
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")
|
||||
df_keys = df[["Key", "Site", "Sonde", "DateJour", "Type", "Source_Id", "Resume"]].copy()
|
||||
df_to_save = editable.merge(df_keys, on="Key", how="left")
|
||||
|
||||
payload = []
|
||||
for _, r in df_to_save.iterrows():
|
||||
@@ -713,14 +726,10 @@ if st.session_state.get("authenticated"):
|
||||
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)
|
||||
if site_selectionne:
|
||||
assert_site_ok(site_selectionne)
|
||||
table_alertes = f"Alertes_{site_selectionne}"
|
||||
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||
cursor.execute(
|
||||
f"SELECT Sonde, Debut_defaut, Etat "
|
||||
f"FROM `{table_alertes}` "
|
||||
@@ -728,45 +737,47 @@ if st.session_state.get("authenticated"):
|
||||
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)
|
||||
st.dataframe(pd.DataFrame(alertes), use_container_width=True)
|
||||
else:
|
||||
st.success("✅ Aucune alerte en cours.")
|
||||
cursor.close()
|
||||
conn.close()
|
||||
else:
|
||||
st.info("Connectez-vous et choisissez un site pour afficher les alertes.")
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la récupération des alertes : {e}")
|
||||
st.text(traceback.format_exc())
|
||||
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"]:
|
||||
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"]
|
||||
)
|
||||
if st.session_state.get("authenticated"):
|
||||
|
||||
# 🔒 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:
|
||||
st.session_state["onglet_actif"] = onglets[0]
|
||||
|
||||
# Menu (créé pour tous)
|
||||
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 = (
|
||||
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")
|
||||
)
|
||||
date_selectionnee = st.session_state.get("selected_date", date.today())
|
||||
@@ -774,28 +785,40 @@ if st.session_state["authenticated"]:
|
||||
# ------------------ Accueil ------------------
|
||||
if onglet_selectionne == "Accueil":
|
||||
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}")
|
||||
try:
|
||||
st.autorefresh(interval=30000, key="gyro_autorefresh")
|
||||
except Exception:
|
||||
pass
|
||||
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)
|
||||
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(
|
||||
f"SELECT * FROM `{site_actuel}` WHERE DATE(Date) = %s ORDER BY Sonde, Date DESC",
|
||||
(date_selectionnee.strftime("%Y-%m-%d"),),
|
||||
@@ -807,6 +830,7 @@ if st.session_state["authenticated"]:
|
||||
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
|
||||
|
||||
@@ -816,7 +840,6 @@ if st.session_state["authenticated"]:
|
||||
)
|
||||
st.session_state["selected_periode"] = tranche
|
||||
|
||||
# Génération PDF
|
||||
if st.button("🧾 Générer le PDF du jour"):
|
||||
generer_pdf(
|
||||
site_actuel,
|
||||
@@ -824,6 +847,7 @@ if st.session_state["authenticated"]:
|
||||
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)":
|
||||
@@ -831,15 +855,16 @@ if st.session_state["authenticated"]:
|
||||
elif tranche == "Nuit (18h-6h)":
|
||||
df_sonde = df_sonde[(df_sonde["Heure"] >= 18) | (df_sonde["Heure"] < 6)]
|
||||
|
||||
seuil_temp = 10
|
||||
# Seuil
|
||||
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"]
|
||||
if seuil and seuil.get("Temp_Max") is not None:
|
||||
seuil_temp = float(seuil["Temp_Max"])
|
||||
|
||||
if rows and not df_sonde.empty:
|
||||
st.subheader("📊 Tableau des relevés")
|
||||
|
||||
def surlignage_temp(val):
|
||||
@@ -852,6 +877,7 @@ if st.session_state["authenticated"]:
|
||||
|
||||
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")
|
||||
@@ -862,9 +888,9 @@ if st.session_state["authenticated"]:
|
||||
ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M"))
|
||||
ax.legend()
|
||||
st.pyplot(fig)
|
||||
elif not rows:
|
||||
st.info("Aucun relevé pour cette date.")
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
st.error(f"Erreur : {e}")
|
||||
st.text(traceback.format_exc())
|
||||
@@ -872,21 +898,24 @@ if st.session_state["authenticated"]:
|
||||
# ------------------ Statistiques ------------------
|
||||
elif onglet_selectionne == "Statistiques":
|
||||
st.markdown("## 📈 Statistiques de température")
|
||||
|
||||
site = (
|
||||
st.session_state["site_autorise"]
|
||||
if st.session_state["role"] != "superviseur"
|
||||
st.session_state.get("site_autorise")
|
||||
if st.session_state.get("role") != "superviseur"
|
||||
else st.session_state.get("selected_site", "Saclay")
|
||||
)
|
||||
assert_site_ok(site)
|
||||
|
||||
date_val = st.session_state.get("selected_date", date.today())
|
||||
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||
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.")
|
||||
@@ -895,6 +924,7 @@ if st.session_state["authenticated"]:
|
||||
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")
|
||||
@@ -903,21 +933,22 @@ if st.session_state["authenticated"]:
|
||||
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}")
|
||||
st.text(traceback.format_exc())
|
||||
|
||||
# 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):
|
||||
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)
|
||||
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()
|
||||
|
||||
if not chambres:
|
||||
st.warning("Aucune chambre froide pour ce site.")
|
||||
else:
|
||||
@@ -925,6 +956,7 @@ if st.session_state["authenticated"]:
|
||||
col1, col2, col3 = st.columns([3, 1, 2])
|
||||
with col1:
|
||||
st.markdown(f"**{chambre['Sonde']}**")
|
||||
|
||||
with col2:
|
||||
etat = st.checkbox(
|
||||
"ON",
|
||||
@@ -932,8 +964,9 @@ if st.session_state["authenticated"]:
|
||||
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"]
|
||||
temp_max = int(chambre["Temp_Max"])
|
||||
moins, temp_display, plus = st.columns([1, 2, 1])
|
||||
with moins:
|
||||
if st.button("▼", key=f"moins_{chambre['Id']}"):
|
||||
@@ -946,6 +979,7 @@ if st.session_state["authenticated"]:
|
||||
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",
|
||||
@@ -953,54 +987,36 @@ if st.session_state["authenticated"]:
|
||||
)
|
||||
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)
|
||||
with closing(get_connection()) as conn, closing(conn.cursor(dictionary=True)) as cursor:
|
||||
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()
|
||||
|
||||
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}")
|
||||
st.text(traceback.format_exc())
|
||||
|
||||
# ------------------ Journal erreurs ------------------
|
||||
elif onglet_selectionne == "Journal erreurs":
|
||||
page_journal_erreurs()
|
||||
|
||||
else:
|
||||
st.info("Connectez-vous pour accéder à l’application.")
|
||||
|
||||
Reference in New Issue
Block a user