diff --git a/.env b/.env
index 582b375..61e11cb 100644
--- a/.env
+++ b/.env
@@ -1,10 +1,15 @@
-#Connexion base de données
+# --- Identité principale (domo91)
DB_HOST=162.19.78.131
DB_USER=excel
DB_PASSWORD='%n#%3Lay1MPa$%kR^5@'
-DB_NAME=Acces
-DB_NAME2=Sondes
+DB_NAME=Sondes
+
+# --- Auth admin de l’app users ---
ADMIN_USER=Michel
+DB_USER2=excel
+DB_PASSWORD2='%n#%3Lay1MPa$%kR^5@'
+DB_NAME2=Acces
+ADMIN_PASSWORD=Gabrielle
ADMIN_PASS_HASH='$2b$12$Dgv7jNLJuR.3hQminSVE9OP6hCSmW4nISArR3HF5LTPGFK0Zw29N2'
# connexion OVH pour les SMS
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
index a1dccde..01a8cd9 100644
--- a/.idea/dataSources.xml
+++ b/.idea/dataSources.xml
@@ -5,7 +5,7 @@
mariadb
true
org.mariadb.jdbc.Driver
- jdbc:mariadb://162.19.78.131:3306/Sondes
+ jdbc:mariadb://162.19.78.131:3306/Acces
$ProjectFileDir$
diff --git a/app/Logs.py b/app/Logs.py
index 68db89e..105d6ed 100644
--- a/app/Logs.py
+++ b/app/Logs.py
@@ -1,225 +1,67 @@
-# Logs.py
-# Visualiseur de logs SSH ultra-simplifié (Streamlit)
-# - Lecture de la config via .env uniquement
-# - Connexion SSH via clé (chemin) ou mot de passe
-# - Liste des fichiers, tail dernier N, recherche, téléchargement
-
import os
-import time
-import stat
-import paramiko
import streamlit as st
-from dotenv import load_dotenv
-from typing import Optional, Tuple
-# ================
-# CONFIG .env
-# ================
-load_dotenv() # cherche automatiquement un .env dans le dossier courant
+# Dossier des logs
+LOG_DIR = "/home/debian/Gestion_sondes/Logs"
-VPS_HOST = os.getenv("SSH_HOST", "").strip()
-VPS_PORT = int(os.getenv("SSH_PORT", 22))
-VPS_USER = os.getenv("SSH_USER", "").strip()
-VPS_PASSWORD = os.getenv("SSH_PASSWORD", "")
-VPS_KEY_PATH = os.getenv("SSH_KEY_PATH", "").strip()
-VPS_LOG_DIR = os.getenv("SSH_LOG_DIR", "/home/debian/Gestion_sondes/Logs").rstrip("/")
+st.title("Gestion des logs - Gestion_sondes")
-# ================
-# UTILITAIRES
-# ================
-# --- Connexion strictement au MOT DE PASSE ---
-@st.cache_resource(show_spinner=False)
-def get_ssh_client_password_only() -> paramiko.SSHClient:
- host = os.getenv("SSH_HOST", "").strip()
- user = os.getenv("SSH_USER", "").strip()
- port = int(os.getenv("SSH_PORT", 22))
- pwd = os.getenv("SSH_PASSWORD", "")
-
- if not host or not user or not pwd:
- raise RuntimeError("SSH_HOST / SSH_USER / SSH_PASSWORD manquant(s) dans .env")
-
- client = paramiko.SSHClient()
- client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-
- # IMPORTANT : on force l’usage du mot de passe, aucune clé n’est chargée
- try:
- client.connect(
- hostname=host, port=port, username=user,
- password=pwd, timeout=10, allow_agent=False, look_for_keys=False
- )
- # vérifie que le transport est bien up
- t = client.get_transport()
- if not t or not t.is_active():
- raise RuntimeError("Transport SSH inactif après connexion.")
- return client
- except Exception as e:
- raise RuntimeError(f"Échec connexion SSH par mot de passe : {e}") from e
-
-# --- Ouverture SFTP sûre ---
-try:
- client = get_ssh_client_password_only()
-except Exception as e:
- st.error(str(e))
+# --- Vérification du dossier ---
+if not os.path.isdir(LOG_DIR):
+ st.error(f"Le dossier {LOG_DIR} n'existe pas.")
st.stop()
-try:
- sftp = client.open_sftp()
-except Exception as e:
- st.error(f"Erreur ouverture SFTP : {e}")
- try: client.close()
- except: pass
- st.stop()
+# Liste des fichiers
+files = sorted([f for f in os.listdir(LOG_DIR) if os.path.isfile(os.path.join(LOG_DIR, f))])
-def list_remote_files(sftp: paramiko.SFTPClient, directory: str) -> list[Tuple[str, int, float]]:
- """
- Retourne [(nom, taille_bytes, mtime_epoch), ...] triés par mtime desc.
- """
- items = []
- try:
- for entry in sftp.listdir_attr(directory):
- mode = entry.st_mode
- if stat.S_ISREG(mode):
- items.append((entry.filename, entry.st_size, entry.st_mtime))
- except FileNotFoundError:
- st.error(f"❌ Dossier introuvable côté serveur : {directory}")
- return []
- except Exception as e:
- st.error(f"❌ Impossible de lister {directory} : {e}")
- return []
- items.sort(key=lambda t: t[2], reverse=True)
- return items
-
-
-def read_remote_text_tail(
- client: paramiko.SSHClient, path: str, lines: int = 500, grep: str | None = None
-) -> str:
- """
- Lit côté serveur les N dernières lignes (tail -n),
- compatible .gz via zcat si nécessaire, avec grep optionnel (insensible à la casse).
- """
- safe_path = path.replace('"', '\\"')
- if path.endswith(".gz"):
- base_cmd = f'zcat "{safe_path}"'
- else:
- base_cmd = f'tail -n {max(1, lines)} "{safe_path}" && exit 0 || head -n {max(1, lines)} "{safe_path}"'
-
- if grep:
- # -i insensible à la casse ; on passe par grep après la commande de base
- grep_safe = grep.replace('"', '\\"') # échappe juste les guillemets
- base_cmd = f'{base_cmd} | grep -i "{grep_safe}" || true'
-
- stdin, stdout, stderr = client.exec_command(base_cmd, timeout=20)
- out = stdout.read()
- err = stderr.read()
- text = (out or b"").decode("utf-8", errors="replace")
- # Si zcat sans grep et sans tail (gros fichiers), on limite côté client pour éviter d'assommer Streamlit
- if path.endswith(".gz") and not grep:
- lines_list = text.splitlines()
- text = "\n".join(lines_list[-max(1, lines):])
- if err and not text:
- # Afficher l'erreur seulement si rien en sortie
- text = (b"[stderr] " + err).decode("utf-8", errors="replace")
- return text
-
-
-def download_remote_file(sftp: paramiko.SFTPClient, path: str) -> bytes:
- """Télécharge le fichier distant (binaire) en mémoire."""
- with sftp.file(path, "rb") as f:
- return f.read()
-
-
-# ================
-# UI STREAMLIT
-# ================
-
-st.set_page_config(page_title="Visualiseur de Logs", layout="wide")
-st.title("📜 Visualiseur de logs (SSH)")
-
-with st.expander("Configuration (lecture seule)", expanded=False):
- st.code(
- f"HOST={VPS_HOST}\nUSER={VPS_USER}\nPORT={VPS_PORT}\n"
- f"KEY_PATH={VPS_KEY_PATH or '-'}\nLOG_DIR={VPS_LOG_DIR}",
- language="bash"
- )
-
-client = _get_ssh_client()
-if client is None:
- st.stop()
-
-try:
- sftp = client.open_sftp()
-except Exception as e:
- st.error(f"Erreur ouverture SFTP : {e}")
- try:
- client.close()
- except Exception:
- pass
- st.stop()
-
-with st.sidebar:
- st.header("🔎 Options")
- refresh = st.button("🔄 Rafraîchir la liste")
- default_tail = st.number_input("Dernières lignes (tail)", min_value=50, max_value=100_000, value=500, step=50)
- search_term = st.text_input("Recherche (grep -i)", value="", placeholder="ex: ERROR | Timeout | sonde")
- st.caption("Astuce : la recherche applique un grep insensible à la casse sur le résultat.")
-
-# Rafraîchissement simple : on rejoue listdir si bouton cliqué
-if refresh:
- time.sleep(0.2)
-
-files = list_remote_files(sftp, VPS_LOG_DIR)
if not files:
- sftp.close()
- client.close()
+ st.warning("Aucun fichier de log trouvé.")
st.stop()
-# Tableau simple (nom, taille, date)
-col1, col2 = st.columns([2, 1])
-with col1:
- names = [f[0] for f in files]
- choice = st.selectbox("Fichiers disponibles (triés par date desc.)", names, index=0)
-with col2:
- # Infos du fichier choisi
- chosen = next((f for f in files if f[0] == choice), None)
- if chosen:
- size_kb = chosen[1] / 1024
- mtime_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(chosen[2]))
- st.metric("Taille (KB)", f"{size_kb:,.0f}".replace(",", " "))
- st.metric("Modifié le", mtime_str)
+# Sélection du fichier
+selected_file = st.selectbox("Choisissez un fichier log :", files)
-full_path = f"{VPS_LOG_DIR}/{choice}"
+file_path = os.path.join(LOG_DIR, selected_file)
-# Actions
-place1, place2, place3 = st.columns([1, 1, 6])
-with place1:
- do_show = st.button("👁️ Afficher")
-with place2:
- do_dl = st.button("⬇️ Télécharger")
+st.subheader(f"Contenu de : {selected_file}")
-# Affichage
-if do_show:
- with st.spinner("Lecture du fichier distant..."):
- text = read_remote_text_tail(
- client,
- full_path,
- lines=int(default_tail),
- grep=(search_term or None)
- )
- st.text_area(f"Contenu : {choice}", text, height=600)
-
-# Téléchargement
-if do_dl:
+# Lecture du contenu
+def read_file():
try:
- data = download_remote_file(sftp, full_path)
- st.download_button(
- label=f"Télécharger {choice}",
- data=data,
- file_name=choice,
- mime="application/octet-stream"
- )
- except Exception as e:
- st.error(f"❌ Échec du téléchargement : {e}")
+ with open(file_path, "r") as f:
+ return f.read()
+ except Exception as err:
+ return f"Erreur lors de la lecture : {err}"
-# Fermeture propre
-sftp.close()
-client.close()
+content = read_file()
+st.text_area("", content, height=400)
+
+
+# --- Boutons d’action ---
+col1, col2, col3 = st.columns(3)
+
+# Rafraîchir
+with col1:
+ if st.button("🔄 Rafraîchir"):
+ st.rerun()
+
+# Vider
+with col2:
+ if st.button("🧹 Vider le fichier"):
+ try:
+ with open(file_path, "w") as f:
+ f.write("")
+ st.success(f"Le fichier '{selected_file}' a été vidé.")
+ st.rerun()
+ except Exception as e:
+ st.error(f"Erreur : {e}")
+
+# Supprimer
+with col3:
+ if st.button("🗑️ Supprimer le fichier"):
+ try:
+ os.remove(file_path)
+ st.success(f"Le fichier '{selected_file}' a été supprimé.")
+ st.rerun()
+ except Exception as e:
+ st.error(f"Erreur : {e}")
diff --git a/app/tracker.py b/app/tracker.py
deleted file mode 100644
index f817c6e..0000000
--- a/app/tracker.py
+++ /dev/null
@@ -1,371 +0,0 @@
-# tracker.py
-# -------------------------------------------------------------
-# Streamlit — Gestion de la table MySQL Sondes.tracker (avec address_hyphen)
-# -------------------------------------------------------------
-# Schéma attendu :
-# id (PK), address (ROM {0x..}), address_hyphen (28-..-..-..-..-..-..-..),
-# lieu, repere, mise_en_service (DATE), res_bits, date (timestamp)
-# Authentification intégrée via .env (AUTH_USERS JSON)
-# -------------------------------------------------------------
-import re
-import time
-import json
-import hmac
-from typing import Optional
-from datetime import date
-
-import pandas as pd
-import streamlit as st
-import mysql.connector as mysql
-from contextlib import contextmanager
-from dotenv import load_dotenv
-import os
-from pathlib import Path
-
-# 1) .env (avec recherche automatique depuis le fichier courant vers la racine)
-try:
- from dotenv import load_dotenv, find_dotenv
- load_dotenv(find_dotenv(usecwd=True), override=False)
-except Exception:
- pass # si python-dotenv n'est pas installé, on passe (Streamlit peut fournir st.secrets)
-
-# 2) Streamlit secrets (optionnel si tu l’utilises)
-try:
- import streamlit as st
- st_secrets_mysql = st.secrets.get("mysql", {})
-except Exception:
- st_secrets_mysql = {}
-
-def get_db_cfg():
- # Priorité à st.secrets si dispo, sinon variables d’environnement (.env)
- cfg = {
- "host": st_secrets_mysql.get("host") or os.getenv("DB_HOST"),
- "user": st_secrets_mysql.get("user") or os.getenv("DB_USER"),
- "password": st_secrets_mysql.get("password") or os.getenv("DB_PASSWORD"),
- "database": st_secrets_mysql.get("database") or os.getenv("DB_NAME") or os.getenv("DB_DATABASE"),
- "port": int(st_secrets_mysql.get("port") or os.getenv("DB_PORT") or 3306),
- "auth_plugin": os.getenv("DB_AUTH_PLUGIN") or None, # optionnel
- }
- # Nettoyage: retire les clés None
- return {k: v for k, v in cfg.items() if v not in (None, "")}
-# ==========================
-# Configuration / Constantes
-# ==========================
-load_dotenv() # lit .env si présent
-
-TABLE_NAME = "tracker"
-COL_ID = "id"
-COL_ADDRESS = "address" # format ROM : {0x28,0xFF,...}
-COL_ADDR_HYPHEN = "address_hyphen" # format hyphen : 28-xx-xx-xx-xx-xx-xx-xx
-COL_LIEU = "lieu"
-COL_REPERE = "repere"
-COL_MES = "mise_en_service" # DATE
-COL_RESBITS = "res_bits"
-COL_DATE = "date"
-
-# Configuration BDD (standardisée sur les variables d'env MYSQL_*)
-DB_CFG = dict(
- host=os.getenv("DB_HOST"),
- user=os.getenv("DB_USER"),
- password=os.getenv("DB_PASS"),
- database=os.getenv("DB_NAME"),
- port=int(os.getenv("MYSQL_PORT", "3306")),
-)
-
-# Regex d'une ROM code DS18B20 au format {0x28,0xFF,0xAA,0xBB,0xCC,0xDD,0xEE,0xCRC}
-ROM_REGEX = re.compile(r"^{(?:0x[0-9A-Fa-f]{2},){7}0x[0-9A-Fa-f]{2}}$")
-# Adresse hyphen : 8 octets hexa séparés par des tirets
-HYPHEN_REGEX = re.compile(r"^[0-9A-Fa-f]{2}(?:-[0-9A-Fa-f]{2}){7}$")
-
-# Mapping résolution DS18B20 (bits -> infos)
-RES_MAP = {
- 9: {"precision": 0.5, "tconv_ms": 94},
- 10: {"precision": 0.25, "tconv_ms": 188},
- 11: {"precision": 0.125, "tconv_ms": 375},
- 12: {"precision": 0.0625,"tconv_ms": 750},
-}
-
-# ==================
-# Authentification via .env (AUTH_USERS)
-# ==================
-AUTH_USERS_RAW = os.getenv("AUTH_USERS", "[]")
-
-def _load_users() -> dict:
- try:
- data = json.loads(AUTH_USERS_RAW)
- return {str(d.get("user", "")).strip(): str(d.get("pass", "")) for d in data if d.get("user") and d.get("pass")}
- except Exception:
- return {}
-
-USERS = _load_users()
-
-def _constant_time_equals(a: str, b: str) -> bool:
- return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))
-
-def verify_credentials(username: str, password: str) -> bool:
- if not username or not password:
- return False
- expected = USERS.get(username.strip())
- if expected is None:
- return False
- return _constant_time_equals(password, expected)
-
-def require_login() -> Optional[str]:
- if st.session_state.get("auth_ok") and st.session_state.get("auth_user"):
- return st.session_state.get("auth_user")
-
- st.markdown("
🔒 Tracker
", unsafe_allow_html=True)
- _, col2, _ = st.columns([1, 2, 1])
- with col2:
- with st.form("login_form", clear_on_submit=False):
- username = st.text_input("Utilisateur")
- password = st.text_input("Mot de passe", type="password")
- ok = st.form_submit_button("Se connecter")
- if ok:
- if verify_credentials(username, password):
- st.session_state["auth_ok"] = True
- st.session_state["auth_user"] = username.strip()
- st.success("Connexion réussie.")
- time.sleep(0.3)
- st.rerun()
- else:
- st.error("Identifiants invalides.")
- st.stop()
-
-# ==================
-# Accès Base de Données
-# ==================
-@contextmanager
-def get_conn():
- cfg = get_db_cfg()
- # Astuce debug si besoin :
- # print({k: ('***' if k=='password' else v) for k,v in cfg.items()})
- conn = mysql.connect(**cfg)
- try:
- yield conn
- finally:
- conn.close()
-
-# -----------------
-# Utilitaires
-# -----------------
-def rom_help() -> str:
- return (
- "Format ROM attendu : `{0x28,0xFF,0xAA,0xBB,0xCC,0xDD,0xEE,0x12}` "
- "(8 octets en hex). Le premier octet est souvent 0x28 pour DS18B20."
- )
-
-def is_valid_rom(address: str) -> bool:
- return bool(ROM_REGEX.match(str(address).strip()))
-
-def is_valid_hyphen(address_h: str) -> bool:
- return bool(HYPHEN_REGEX.match(str(address_h).strip()))
-
-def rom_to_hyphen(rom: str) -> str:
- hexes = re.findall(r"0x([0-9A-Fa-f]{2})", str(rom))
- if len(hexes) != 8:
- return ""
- return "-".join(h.lower() for h in hexes)
-
-def hyphen_to_rom(h: str) -> str:
- parts = str(h).strip().split("-")
- if len(parts) != 8 or not all(re.fullmatch(r"[0-9A-Fa-f]{2}", p) for p in parts):
- return ""
- return "{" + ",".join(f"0x{p.upper()}" for p in parts) + "}"
-
-def res_label(bits: int) -> str:
- info = RES_MAP.get(bits)
- if not info:
- return f"{bits} bits (inconnu)"
- return f"{bits} bits (±{info['precision']}°C, {info['tconv_ms']} ms)"
-
-# -----------------
-# Fonctions SQL
-# -----------------
-def fetch_trackers(where_lieu: str | None = None) -> pd.DataFrame:
- query = (
- f"SELECT {COL_ID}, {COL_ADDRESS}, {COL_ADDR_HYPHEN}, {COL_LIEU}, "
- f"{COL_REPERE}, {COL_MES}, {COL_RESBITS}, {COL_DATE} "
- f"FROM {TABLE_NAME}"
- )
- params = []
- if where_lieu:
- query += f" WHERE {COL_LIEU} = %s"
- params.append(where_lieu)
- query += f" ORDER BY {COL_LIEU}, {COL_REPERE}, {COL_ADDR_HYPHEN}, {COL_ADDRESS}"
- with get_conn() as conn:
- df = pd.read_sql(query, conn, params=params)
- return df
-
-def insert_tracker(address: str, lieu: str, res_bits: int,
- repere: str | None = None, mise_en_service: date | None = None,
- address_hyphen: str | None = None) -> int:
- addr_rom = (address or "").strip() if address else ""
- addr_hyp = (address_hyphen or "").strip() if address_hyphen else ""
- if addr_rom and not addr_hyp:
- addr_hyp = rom_to_hyphen(addr_rom)
- if addr_hyp and not addr_rom:
- addr_rom = hyphen_to_rom(addr_hyp)
- if not is_valid_rom(addr_rom) or not is_valid_hyphen(addr_hyp):
- raise ValueError("Adresse invalide (ROM ou hyphen).")
- sql = f"""
- INSERT INTO {TABLE_NAME}
- ({COL_ADDRESS}, {COL_ADDR_HYPHEN}, {COL_LIEU}, {COL_REPERE}, {COL_MES}, {COL_RESBITS})
- VALUES (%s, %s, %s, %s, %s, %s)
- """
- with get_conn() as conn:
- cur = conn.cursor()
- cur.execute(sql, (
- addr_rom,
- addr_hyp.lower(),
- lieu,
- (repere.strip() if repere and str(repere).strip() else None),
- mise_en_service,
- res_bits,
- ))
- conn.commit()
- return cur.lastrowid
-
-def update_tracker(row_id: int, address: str, lieu: str, res_bits: int,
- repere: str | None, mise_en_service: date | None,
- address_hyphen: str | None = None) -> None:
- addr_rom = (address or "").strip()
- addr_hyp = (address_hyphen or "").strip() if address_hyphen else ""
- if addr_rom and not addr_hyp:
- addr_hyp = rom_to_hyphen(addr_rom)
- if addr_hyp and not addr_rom:
- addr_rom = hyphen_to_rom(addr_hyp)
- if not is_valid_rom(addr_rom) or not is_valid_hyphen(addr_hyp):
- raise ValueError("Adresse invalide (ROM ou hyphen).")
- sql = f"""
- UPDATE {TABLE_NAME}
- SET {COL_ADDRESS}=%s, {COL_ADDR_HYPHEN}=%s, {COL_LIEU}=%s, {COL_REPERE}=%s, {COL_MES}=%s, {COL_RESBITS}=%s
- WHERE {COL_ID}=%s
- """
- with get_conn() as conn:
- cur = conn.cursor()
- cur.execute(sql, (
- addr_rom,
- addr_hyp.lower(),
- lieu,
- (repere.strip() if repere and str(repere).strip() else None),
- mise_en_service,
- res_bits,
- row_id,
- ))
- conn.commit()
-
-def delete_tracker(row_id: int) -> None:
- sql = f"DELETE FROM {TABLE_NAME} WHERE {COL_ID}=%s"
- with get_conn() as conn:
- cur = conn.cursor()
- cur.execute(sql, (row_id,))
- conn.commit()
-
-# ==================
-# Application Streamlit
-# ==================
-st.set_page_config(page_title="Tracker", page_icon="🌡️", layout="wide")
-user = require_login()
-
-st.title("🌡️ Gestion du parc sondes (stock ou installées)")
-with st.expander("Paramètres de connexion (lecture seule)"):
- st.write({k: ("***" if k in {"password"} else v) for k, v in DB_CFG.items()})
- st.caption("Configurez ces valeurs via le fichier .env")
-
-st.sidebar.header("Filtres & Actions")
-st.sidebar.caption(f"Connecté en tant que **{user}**")
-
-_all = fetch_trackers()
-lieux = sorted([x for x in _all[COL_LIEU].dropna().unique()]) if not _all.empty else []
-lieu_selected = st.sidebar.selectbox("Filtrer par lieu", options=["(Tous)"] + lieux, index=0)
-
-# Formulaire d'ajout
-st.sidebar.subheader("Ajouter une sonde")
-with st.sidebar.form("add_form", clear_on_submit=True):
- new_address = st.text_input("Adresse ROM", placeholder="{0x28,0xFF,...}", help=rom_help())
- preview_h = rom_to_hyphen(new_address) if new_address else ""
- st.text_input("Adresse hyphen (auto)", value=preview_h, disabled=True)
- new_lieu = st.text_input("Lieu d'installation")
- new_repere = st.text_input("Repère (optionnel)")
- new_mes = st.date_input("Mise en service (optionnel)", value=None, format="YYYY-MM-DD")
- new_res = st.selectbox("Résolution (bits)", options=[9,10,11,12])
- submitted = st.form_submit_button("Ajouter")
- if submitted:
- if not is_valid_rom(new_address):
- st.warning("Adresse ROM invalide.")
- elif not new_lieu.strip():
- st.warning("Lieu requis.")
- else:
- rid = insert_tracker(
- new_address.strip(),
- new_lieu.strip(),
- int(new_res),
- new_repere,
- new_mes if isinstance(new_mes, date) else None,
- address_hyphen=rom_to_hyphen(new_address.strip()),
- )
- st.success(f"Sonde ajoutée (id={rid}).")
- time.sleep(0.6)
- st.rerun()
-
-st.sidebar.divider()
-if st.sidebar.button("Se déconnecter"):
- for _k in list(st.session_state.keys()):
- st.session_state.pop(_k, None)
- st.success("Déconnecté.")
- time.sleep(0.3)
- st.rerun()
-
-# Vue principale
-if lieu_selected != "(Tous)":
- df = fetch_trackers(where_lieu=lieu_selected)
-else:
- df = _all.copy()
-
-if df.empty:
- st.info("Aucune sonde enregistrée.")
-else:
- df["resolution"] = df[COL_RESBITS].apply(res_label)
- st.subheader("Enregistrements")
- edited = st.data_editor(
- df[[COL_ID, COL_ADDRESS, COL_ADDR_HYPHEN, COL_LIEU, COL_REPERE, COL_MES, COL_RESBITS, "resolution", COL_DATE]],
- hide_index=True,
- use_container_width=True,
- num_rows="dynamic",
- )
- removed_ids = set(df[COL_ID]) - set(edited[COL_ID])
- to_update = []
- for _, row in edited.iterrows():
- orig = df.loc[df[COL_ID] == row[COL_ID]].iloc[0]
- changed = (
- (row[COL_ADDRESS] != orig[COL_ADDRESS]) or
- (row[COL_ADDR_HYPHEN] != orig[COL_ADDR_HYPHEN]) or
- (row[COL_LIEU] != orig[COL_LIEU]) or
- (row.get(COL_REPERE) or "") != (orig.get(COL_REPERE) or "") or
- (str(row.get(COL_MES) or "")[:10] != str(orig.get(COL_MES) or "")[:10]) or
- (int(row[COL_RESBITS]) != int(orig[COL_RESBITS]))
- )
- if changed:
- mes_val = row.get(COL_MES)
- if pd.isna(mes_val):
- mes_val = None
- elif hasattr(mes_val, "date"):
- mes_val = mes_val.date()
- to_update.append((int(row[COL_ID]), str(row[COL_ADDRESS]), str(row[COL_ADDR_HYPHEN] or ""),
- str(row[COL_LIEU]), int(row[COL_RESBITS]),
- str(row.get(COL_REPERE) or None), mes_val))
- col1, col2 = st.columns([1,1])
- if st.button("Enregistrer les modifications"):
- for rid, addr_rom, addr_hyp, lieu, rbits, repere, mes in to_update:
- update_tracker(rid, addr_rom, lieu, rbits, repere, mes, address_hyphen=addr_hyp)
- for rid in removed_ids:
- delete_tracker(rid)
- st.success("Modifications enregistrées ✔️")
- time.sleep(0.6)
- st.rerun()
- if st.button("Annuler"):
- st.rerun()
-
-st.divider()
-st.caption("Astuce : collez une adresse ROM {0x..,...} → la version hyphen est générée automatiquement.")
diff --git a/app/users.py b/app/users.py
index 9401dfc..491fa2e 100644
--- a/app/users.py
+++ b/app/users.py
@@ -85,25 +85,33 @@ logout_button()
def get_pool():
load_dotenv()
host = os.getenv("DB_HOST")
- port = int(os.getenv("MYSQL_PORT", "3306"))
- user = os.getenv("DB_USER")
- pwd = os.getenv("DB_PASSWORD")
- db = os.getenv("DB_NAME")
+ port = int(os.getenv("MYSQL_PORT", "3306")) # ✅ valeur par défaut 3306
+ user = os.getenv("DB_USER2")
+ pwd = os.getenv("DB_PASSWORD2")
+ db = os.getenv("DB_NAME2")
+
+ # ✅ contrôle des variables indispensables
missing = [k for k, v in {
- "DB_HOST": host, "MYSQL_PORT": port, "DB_USER": user, "DB_PASSWORD": pwd, "DB_NAME": db
+ "DB_HOST": host,
+ "DB_USER2": user,
+ "DB_PASSWORD2": pwd,
+ "DB_NAME2": db,
}.items() if v in (None, "")]
if missing:
raise RuntimeError(f"Variables manquantes dans .env : {', '.join(missing)}")
+
return pooling.MySQLConnectionPool(
pool_name="users_pool",
pool_size=5,
pool_reset_session=True,
- host=host, port=port, user=user, password=pwd, database=db,
- autocommit=True
+ host=host,
+ port=port,
+ user=user,
+ password=pwd,
+ database=db,
+ autocommit=True,
)
-pool = get_pool()
-
# -----------------------
# Helpers SQL + validations
# -----------------------
@@ -300,7 +308,11 @@ def get_user_email_and_field(cnx, username: str, field: str):
# -----------------------
st.set_page_config(page_title="Acces.Utilisateurs", page_icon="👤", layout="wide")
st.title("Gestion des utilisateurs")
-
+try:
+ pool = get_pool()
+except Exception as e:
+ st.error(f"❌ Impossible d'initialiser le pool MySQL : {e}")
+ st.stop()
tab_list, tab_create, tab_edit, tab_security = st.tabs(["Liste", "Créer", "Modifier", "Sécurité"])
# -----------------------
diff --git a/requirements.txt b/requirements.txt
index c240005..7b4e19f 100644
Binary files a/requirements.txt and b/requirements.txt differ