Refonte Domo91.py, mot de passe bcrypt
This commit is contained in:
8
.env
8
.env
@@ -16,11 +16,11 @@ OVH_APP_KEY=f725d07b2f98a195
|
|||||||
OVH_APP_SECRET=5ca392a0a728e2395edd426bb1e11ad6
|
OVH_APP_SECRET=5ca392a0a728e2395edd426bb1e11ad6
|
||||||
OVH_CONSUMER_KEY=305f2e8611e58b83930de84ee65c99f9
|
OVH_CONSUMER_KEY=305f2e8611e58b83930de84ee65c99f9
|
||||||
OVH_ENDPOINT=ovh-eu
|
OVH_ENDPOINT=ovh-eu
|
||||||
OVH_SMS_ACCOUNT=sms-jm164396-1
|
|
||||||
OVH_SMS_SENDER=DOMO91FR
|
OVH_SMS_SENDER=DOMO91FR
|
||||||
OVH_SERVICE_NAME=sms-jm164396-1
|
OVH_SERVICE_NAME=sms-jm164396-1
|
||||||
SMS_RECEIVER=+33635164680
|
SMS_RECEIVER=0635164680
|
||||||
|
OVH_PASSWORD=w*j&A2j*QT^HL6
|
||||||
ENVOI_SMS=1
|
ENVOI_SMS=1
|
||||||
PHONE_SACLAY=+33682069405,+33650270939
|
PHONE_SACLAY=33682069405,33650270939
|
||||||
PHONE_MEUDON=+33666271128
|
PHONE_MEUDON=33666271128
|
||||||
PHONE_ADMIN=+33635164680
|
PHONE_ADMIN=+33635164680
|
||||||
|
|||||||
174
alerte_sms.py
Normal file
174
alerte_sms.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import mysql.connector
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
if os.name != 'nt':
|
||||||
|
log_dir = Path('/home/debian/Gestion_sondes/Logs')
|
||||||
|
else:
|
||||||
|
log_dir = Path.cwd() / 'Logs'
|
||||||
|
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
ENVOI_SMS = os.getenv("ENVOI_SMS") == "1"
|
||||||
|
|
||||||
|
# --- Config MySQL ---
|
||||||
|
config = {
|
||||||
|
"host": os.getenv("DB_HOST"),
|
||||||
|
"user": os.getenv("DB_USER"),
|
||||||
|
"password": os.getenv("DB_PASSWORD"),
|
||||||
|
"database": os.getenv("DB_NAME")
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Suivi des alertes actives pour rappels ---
|
||||||
|
alertes_actives = {}
|
||||||
|
|
||||||
|
# --- Fonction d'envoi de mail ---
|
||||||
|
def envoyer_sms_ovh(message, lieu):
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
sms_data = {
|
||||||
|
"account": os.getenv("OVH_SMS_ACCOUNT"),
|
||||||
|
"login": os.getenv("OVH_SERVICE_NAME"),
|
||||||
|
"password": os.getenv("OVH_PASSWORD"),
|
||||||
|
"message": f"{lieu}: {message}",
|
||||||
|
"receivers": os.getenv("SMS_RECEIVER", "").split(","),
|
||||||
|
"sender": os.getenv("OVH_SMS_SENDER")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Exemple d'envoi avec l'API OVH (à adapter selon ton endpoint exact)
|
||||||
|
response = requests.post("https://www.ovh.com/cgi-bin/sms/http2sms.cgi", data=sms_data)
|
||||||
|
print(f"📱 SMS envoyé : {response.text}", flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur envoi SMS : {e}", flush=True)
|
||||||
|
|
||||||
|
# --- Fonction de surveillance ---
|
||||||
|
def surveiller():
|
||||||
|
global alertes_actives
|
||||||
|
log_entries = []
|
||||||
|
try:
|
||||||
|
conn = mysql.connector.connect(**config)
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
cursor.execute("SELECT DISTINCT Lieu FROM Sondes.Chambres_froides")
|
||||||
|
lieux = [row['Lieu'] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
for lieu in lieux:
|
||||||
|
table_temp = lieu
|
||||||
|
table_alertes = f"Alertes_{lieu}"
|
||||||
|
|
||||||
|
cursor.execute("SELECT Sonde, Temp_Max FROM Sondes.Chambres_froides WHERE Lieu=%s AND Etat='ON'", (lieu,))
|
||||||
|
sondes = cursor.fetchall()
|
||||||
|
|
||||||
|
for sonde in sondes:
|
||||||
|
nom_sonde = sonde['Sonde']
|
||||||
|
seuil = sonde['Temp_Max']
|
||||||
|
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT Date, Temperature FROM {table_temp}
|
||||||
|
WHERE Sonde = %s
|
||||||
|
ORDER BY Date DESC LIMIT 6
|
||||||
|
""", (nom_sonde,))
|
||||||
|
releves = cursor.fetchall()
|
||||||
|
|
||||||
|
for r in releves:
|
||||||
|
log_entries.append({
|
||||||
|
"Date": r['Date'],
|
||||||
|
"Lieu": lieu,
|
||||||
|
"Sonde": nom_sonde,
|
||||||
|
"Température": r['Temperature'],
|
||||||
|
"Seuil": seuil,
|
||||||
|
"État": "Dépassement" if r['Temperature'] > seuil else "Normal"
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(releves) == 6:
|
||||||
|
toutes_hors_seuil = all(r['Temperature'] > seuil for r in releves)
|
||||||
|
plus_ancien = releves[-1]['Date']
|
||||||
|
maintenant = datetime.now()
|
||||||
|
|
||||||
|
if toutes_hors_seuil and (maintenant - plus_ancien >= timedelta(minutes=30)):
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT COUNT(*) as total FROM {table_alertes}
|
||||||
|
WHERE Sonde=%s AND Status='En cours'
|
||||||
|
""", (nom_sonde,))
|
||||||
|
en_cours = cursor.fetchone()
|
||||||
|
if en_cours['total'] == 0:
|
||||||
|
cursor.execute(
|
||||||
|
f"INSERT INTO {table_alertes} (Sonde, Debut_defaut, Status) VALUES (%s, NOW(), 'En cours')",
|
||||||
|
(nom_sonde,)
|
||||||
|
)
|
||||||
|
print(f"🚨 Alerte déclenchée pour {nom_sonde} ({lieu})", flush=True)
|
||||||
|
|
||||||
|
sujet = f"🚨 ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})"
|
||||||
|
message = (
|
||||||
|
f"La sonde '{nom_sonde}' du site '{lieu}' a dépassé le seuil de {seuil}°C "
|
||||||
|
f"depuis plus de 30 minutes.\nHeure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ENVOI_SMS:
|
||||||
|
envoyer_sms_ovh(message, lieu)
|
||||||
|
|
||||||
|
# Suivi pour rappels
|
||||||
|
alertes_actives[nom_sonde] = maintenant
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Alerte déjà en cours : vérifier s'il faut faire un rappel
|
||||||
|
dernier_envoi = alertes_actives.get(nom_sonde)
|
||||||
|
if dernier_envoi and (maintenant - dernier_envoi >= timedelta(hours=1)):
|
||||||
|
sujet = f"🔔 RAPPEL ALERTE TEMPÉRATURE - {nom_sonde} ({lieu})"
|
||||||
|
message = (
|
||||||
|
f"La sonde '{nom_sonde}' du site '{lieu}' est TOUJOURS en dépassement de seuil (>{seuil}°C).\n"
|
||||||
|
f"Heure : {maintenant.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ENVOI_SMS:
|
||||||
|
envoyer_sms_ovh(message, lieu)
|
||||||
|
alertes_actives[nom_sonde] = maintenant
|
||||||
|
|
||||||
|
# Vérifier retour à la normale (Acquittement)
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT Temperature FROM {table_temp}
|
||||||
|
WHERE Sonde = %s
|
||||||
|
ORDER BY Date DESC LIMIT 1
|
||||||
|
""", (nom_sonde,))
|
||||||
|
derniere = cursor.fetchone()
|
||||||
|
if derniere and derniere['Temperature'] <= seuil:
|
||||||
|
cursor.execute(f"""
|
||||||
|
UPDATE {table_alertes}
|
||||||
|
SET Status = 'Acquitté'
|
||||||
|
WHERE Sonde = %s AND Status IN ('En cours', 'Test')
|
||||||
|
""", (nom_sonde,))
|
||||||
|
|
||||||
|
# Nettoyage du suivi si normalisé
|
||||||
|
if nom_sonde in alertes_actives:
|
||||||
|
del alertes_actives[nom_sonde]
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if log_entries:
|
||||||
|
import pandas as pd
|
||||||
|
df_logs = pd.DataFrame(log_entries)
|
||||||
|
|
||||||
|
# Sauvegarde principale
|
||||||
|
df_logs.to_csv(log_dir / "monitor.csv", sep=";", index=False)
|
||||||
|
|
||||||
|
# Sauvegarde secondaire (Linux uniquement)
|
||||||
|
if os.name != 'nt':
|
||||||
|
df_logs.to_csv("/var/log/monitor.csv", sep=";", index=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur : {e}", flush=True)
|
||||||
|
|
||||||
|
# --- Boucle principale ---
|
||||||
|
while True:
|
||||||
|
print(f"📡 Vérification à {datetime.now()}", flush=True)
|
||||||
|
surveiller()
|
||||||
|
time.sleep(300) # 5 minutes
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ st.set_page_config(page_title="Domo91 - Surveillance", layout="wide")
|
|||||||
if "authenticated" not in st.session_state:
|
if "authenticated" not in st.session_state:
|
||||||
st.session_state["authenticated"] = False
|
st.session_state["authenticated"] = False
|
||||||
st.session_state["role"] = None
|
st.session_state["role"] = None
|
||||||
st.session_state["lieu_autorise"] = None
|
st.session_state["site_autorise"] = None
|
||||||
|
|
||||||
st.title("📡 Supervision Températures")
|
st.title("📡 Supervision Températures")
|
||||||
|
|
||||||
@@ -154,8 +154,8 @@ if "authenticated" not in st.session_state:
|
|||||||
st.session_state["authenticated"] = False
|
st.session_state["authenticated"] = False
|
||||||
if "role" not in st.session_state:
|
if "role" not in st.session_state:
|
||||||
st.session_state["role"] = None
|
st.session_state["role"] = None
|
||||||
if "lieu_autorise" not in st.session_state:
|
if "site_autorise" not in st.session_state:
|
||||||
st.session_state["lieu_autorise"] = None
|
st.session_state["site_autorise"] = None
|
||||||
|
|
||||||
# --- Connexion utilisateur dans la sidebar ---
|
# --- Connexion utilisateur dans la sidebar ---
|
||||||
st.sidebar.header("🔐 Connexion")
|
st.sidebar.header("🔐 Connexion")
|
||||||
@@ -178,7 +178,7 @@ if not st.session_state.get("authenticated"):
|
|||||||
st.stop()
|
st.stop()
|
||||||
st.session_state["authenticated"] = True
|
st.session_state["authenticated"] = True
|
||||||
st.session_state["role"] = result["role"]
|
st.session_state["role"] = result["role"]
|
||||||
st.session_state["lieu_autorise"] = result["Lieu"]
|
st.session_state["site_autorise"] = result["Lieu"]
|
||||||
st.success(f"Connecté comme {result['role']} ({result['Lieu']})")
|
st.success(f"Connecté comme {result['role']} ({result['Lieu']})")
|
||||||
# Enregistrement de la connexion dans Connexion_Log
|
# Enregistrement de la connexion dans Connexion_Log
|
||||||
try:
|
try:
|
||||||
@@ -202,31 +202,12 @@ else:
|
|||||||
if st.sidebar.button("🔓 Déconnexion", key="logout_sidebar"):
|
if st.sidebar.button("🔓 Déconnexion", key="logout_sidebar"):
|
||||||
st.session_state["authenticated"] = False
|
st.session_state["authenticated"] = False
|
||||||
st.session_state["role"] = None
|
st.session_state["role"] = None
|
||||||
st.session_state["lieu_autorise"] = None
|
st.session_state["site_autorise"] = None
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
def hash_password(password):
|
def hash_password(password):
|
||||||
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
def ajouter_utilisateur(utilisateur, mot_de_passe, role, lieu, expiration):
|
|
||||||
try:
|
|
||||||
conn = mysql.connector.connect(**db_config)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("SELECT * FROM MotsDePasse WHERE utilisateur = %s", (utilisateur,))
|
|
||||||
if cursor.fetchone():
|
|
||||||
return False, "❌ Utilisateur déjà existant."
|
|
||||||
|
|
||||||
hash_mdp = hash_password(mot_de_passe)
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO MotsDePasse (utilisateur, mot_de_passe, role, Lieu, Expiration)
|
|
||||||
VALUES (%s, %s, %s, %s, %s)
|
|
||||||
""", (utilisateur, hash_mdp, role, lieu, expiration))
|
|
||||||
conn.commit()
|
|
||||||
cursor.close()
|
|
||||||
conn.close()
|
|
||||||
return True, "✅ Utilisateur ajouté avec succès."
|
|
||||||
except Exception as e:
|
|
||||||
return False, f"⚠️ Erreur : {e}"
|
|
||||||
|
|
||||||
def afficher_gestion_expiration(conn):
|
def afficher_gestion_expiration(conn):
|
||||||
st.subheader("🔐 Gestion des expirations d'accès")
|
st.subheader("🔐 Gestion des expirations d'accès")
|
||||||
@@ -269,7 +250,7 @@ def afficher_gestion_expiration(conn):
|
|||||||
|
|
||||||
# 📄 Affichage bouton PDF si une date est choisie
|
# 📄 Affichage bouton PDF si une date est choisie
|
||||||
site_pdf = (
|
site_pdf = (
|
||||||
st.session_state.get("lieu_autorise")
|
st.session_state.get("site_autorise")
|
||||||
if st.session_state.get("role") != "superviseur"
|
if st.session_state.get("role") != "superviseur"
|
||||||
|
|
||||||
else st.session_state.get("selected_site")
|
else st.session_state.get("selected_site")
|
||||||
@@ -285,7 +266,7 @@ if site_pdf and date_pdf:
|
|||||||
# --- Forcer une alerte de test dynamique (réservé aux superviseurs)
|
# --- Forcer une alerte de test dynamique (réservé aux superviseurs)
|
||||||
if st.session_state.get("authenticated") and st.session_state.get("role") == "superviseur":
|
if st.session_state.get("authenticated") and st.session_state.get("role") == "superviseur":
|
||||||
site_actuel = (
|
site_actuel = (
|
||||||
st.session_state.get("lieu_autorise") or "Saclay"
|
st.session_state.get("site_autorise") or "Saclay"
|
||||||
if st.session_state.get("role") != "superviseur"
|
if st.session_state.get("role") != "superviseur"
|
||||||
else st.session_state.get("selected_site")
|
else st.session_state.get("selected_site")
|
||||||
)
|
)
|
||||||
@@ -338,14 +319,14 @@ if st.session_state.get("authenticated") and st.session_state.get("role") == "su
|
|||||||
if st.button("🔓 Déconnexion", key="logout_main"):
|
if st.button("🔓 Déconnexion", key="logout_main"):
|
||||||
st.session_state["authenticated"] = False
|
st.session_state["authenticated"] = False
|
||||||
st.session_state["role"] = None
|
st.session_state["role"] = None
|
||||||
st.session_state["lieu_autorise"] = None
|
st.session_state["site_autorise"] = None
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.subheader("📄 Rapport PDF")
|
st.subheader("📄 Rapport PDF")
|
||||||
if "selected_date" in st.session_state:
|
if "selected_date" in st.session_state:
|
||||||
if st.button("📅 Télécharger l'état du jour (PDF)"):
|
if st.button("📅 Télécharger l'état du jour (PDF)"):
|
||||||
site = st.session_state["lieu_autorise"]
|
site = st.session_state["site_autorise"]
|
||||||
date_val = st.session_state["selected_date"].strftime("%Y-%m-%d")
|
date_val = st.session_state["selected_date"].strftime("%Y-%m-%d")
|
||||||
generer_pdf(site, date_val, periode)
|
generer_pdf(site, date_val, periode)
|
||||||
else:
|
else:
|
||||||
@@ -382,10 +363,10 @@ if st.session_state["authenticated"]:
|
|||||||
if "role" not in st.session_state:
|
if "role" not in st.session_state:
|
||||||
st.session_state["role"] = None
|
st.session_state["role"] = None
|
||||||
|
|
||||||
if "lieu_autorise" not in st.session_state:
|
if "site_autorise" not in st.session_state:
|
||||||
st.session_state["lieu_autorise"] = None
|
st.session_state["site_autorise"] = None
|
||||||
site_selectionne = (
|
site_selectionne = (
|
||||||
st.session_state["lieu_autorise"]
|
st.session_state["site_autorise"]
|
||||||
if st.session_state["role"] != "superviseur"
|
if st.session_state["role"] != "superviseur"
|
||||||
else st.session_state.get("selected_site", "Saclay")
|
else st.session_state.get("selected_site", "Saclay")
|
||||||
)
|
)
|
||||||
@@ -421,7 +402,46 @@ if st.session_state["authenticated"]:
|
|||||||
onglet = st.sidebar.radio("📁 Navigation", onglets_possibles, index=onglets_possibles.index(onglet))
|
onglet = st.sidebar.radio("📁 Navigation", onglets_possibles, index=onglets_possibles.index(onglet))
|
||||||
st.session_state["onglet_actif"] = onglet
|
st.session_state["onglet_actif"] = onglet
|
||||||
|
|
||||||
# --- ONGLET ACCUEIL ---
|
# --- Bandeau Alertes ---
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# Récupérer le site autorisé depuis la session
|
||||||
|
site = st.session_state.get("lieu_autorise")
|
||||||
|
|
||||||
|
if site:
|
||||||
|
# Lecture des alertes non acquittées pour ce site
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT Id, Sonde, Debut_defaut, Etat
|
||||||
|
FROM Alertes_{site}
|
||||||
|
WHERE Etat != 'Acquitté'
|
||||||
|
ORDER BY Debut_defaut DESC
|
||||||
|
""")
|
||||||
|
alertes = cursor.fetchall()
|
||||||
|
|
||||||
|
if alertes:
|
||||||
|
st.markdown(
|
||||||
|
f"<div style='background-color:#ffcccc;padding:10px;border-radius:8px;'>"
|
||||||
|
f"🚨 <b>{len(alertes)} alerte(s) non résolue(s)</b> sur <b>{site}</b>"
|
||||||
|
f"</div>",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
st.markdown(
|
||||||
|
f"<div style='background-color:#ccffcc;padding:10px;border-radius:8px;'>"
|
||||||
|
f"✅ Aucune alerte en cours sur <b>{site}</b>"
|
||||||
|
f"</div>",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Erreur lors de la récupération des alertes : {e}")
|
||||||
|
|
||||||
|
# --- ONGLET ACCUEIL ---
|
||||||
if onglet == "Accueil":
|
if onglet == "Accueil":
|
||||||
st.markdown("## Sélection du site et de la date")
|
st.markdown("## Sélection du site et de la date")
|
||||||
try:
|
try:
|
||||||
@@ -432,12 +452,12 @@ if st.session_state["authenticated"]:
|
|||||||
site_selectionne = st.selectbox("📍 Choisissez un site :", sites_possibles)
|
site_selectionne = st.selectbox("📍 Choisissez un site :", sites_possibles)
|
||||||
st.session_state["selected_site"] = site_selectionne
|
st.session_state["selected_site"] = site_selectionne
|
||||||
else:
|
else:
|
||||||
site_selectionne = st.session_state["lieu_autorise"]
|
site_selectionne = st.session_state["site_autorise"]
|
||||||
st.info(f"Site imposé : {site_selectionne}")
|
st.info(f"Site imposé : {site_selectionne}")
|
||||||
|
|
||||||
selected_date = st.date_input("📅 Date du relevé", value=date.today())
|
selected_date = st.date_input("📅 Date du relevé", value=date.today())
|
||||||
st.session_state["selected_date"] = selected_date
|
st.session_state["selected_date"] = selected_date
|
||||||
site_selectionne = st.session_state.get("lieu_autorise") or st.session_state.get("selected_site")
|
site_selectionne = st.session_state.get("site_autorise") or st.session_state.get("selected_site")
|
||||||
|
|
||||||
if not site_selectionne:
|
if not site_selectionne:
|
||||||
st.warning("Aucun site sélectionné.")
|
st.warning("Aucun site sélectionné.")
|
||||||
@@ -514,7 +534,7 @@ if st.session_state["authenticated"]:
|
|||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
site = (
|
site = (
|
||||||
st.session_state["lieu_autorise"]
|
st.session_state["site_autorise"]
|
||||||
if st.session_state["role"] != "superviseur"
|
if st.session_state["role"] != "superviseur"
|
||||||
else st.session_state.get("selected_site", "Saclay")
|
else st.session_state.get("selected_site", "Saclay")
|
||||||
)
|
)
|
||||||
@@ -589,7 +609,7 @@ if st.session_state["authenticated"]:
|
|||||||
st.error(f"Erreur lors de l'ajout de la sonde : {e}")
|
st.error(f"Erreur lors de l'ajout de la sonde : {e}")
|
||||||
# --- Affichage automatique des alertes non acquittées ---
|
# --- Affichage automatique des alertes non acquittées ---
|
||||||
site_selectionne = (
|
site_selectionne = (
|
||||||
st.session_state.get("lieu_autorise")
|
st.session_state.get("site_autorise")
|
||||||
if st.session_state.get("role") != "superviseur"
|
if st.session_state.get("role") != "superviseur"
|
||||||
else st.session_state.get("selected_site")
|
else st.session_state.get("selected_site")
|
||||||
)
|
)
|
||||||
@@ -662,7 +682,7 @@ if st.session_state["authenticated"]:
|
|||||||
|
|
||||||
st.markdown("</div><br>", unsafe_allow_html=True)
|
st.markdown("</div><br>", unsafe_allow_html=True)
|
||||||
role = st.session_state.get("role", "utilisateur")
|
role = st.session_state.get("role", "utilisateur")
|
||||||
lieu = st.session_state.get("lieu_autorise") if role != "superviseur" else st.selectbox("Choisir un lieu :",
|
lieu = st.session_state.get("site_autorise") if role != "superviseur" else st.selectbox("Choisir un lieu :",
|
||||||
["Saclay", "Meudon"])
|
["Saclay", "Meudon"])
|
||||||
if not lieu:
|
if not lieu:
|
||||||
st.warning("Aucun site sélectionné.")
|
st.warning("Aucun site sélectionné.")
|
||||||
|
|||||||
@@ -1,49 +1,37 @@
|
|||||||
Date;Lieu;Sonde;Température;Seuil;État
|
Date;Lieu;Sonde;Température;Seuil;État
|
||||||
2025-07-26 13:04:30;Saclay;Congelateur;-14.75;-15.0;Dépassement
|
2025-07-28 06:36:01;Saclay;Congelateur;-17.50;-15.0;Normal
|
||||||
2025-07-26 12:59:27;Saclay;Congelateur;-11.25;-15.0;Dépassement
|
2025-07-28 06:30:58;Saclay;Congelateur;-18.00;-15.0;Normal
|
||||||
2025-07-26 12:54:25;Saclay;Congelateur;-6.50;-15.0;Dépassement
|
2025-07-28 06:25:56;Saclay;Congelateur;-16.75;-15.0;Normal
|
||||||
2025-07-26 12:49:22;Saclay;Congelateur;-4.75;-15.0;Dépassement
|
2025-07-28 06:20:53;Saclay;Congelateur;-17.25;-15.0;Normal
|
||||||
2025-07-26 12:44:19;Saclay;Congelateur;-7.75;-15.0;Dépassement
|
2025-07-28 06:15:51;Saclay;Congelateur;-17.75;-15.0;Normal
|
||||||
2025-07-26 12:39:17;Saclay;Congelateur;-11.50;-15.0;Dépassement
|
2025-07-28 06:10:48;Saclay;Congelateur;-17.75;-15.0;Normal
|
||||||
2025-07-26 13:04:30;Saclay;BOF;1.75;8.0;Normal
|
2025-07-28 06:36:01;Saclay;BOF;3.25;8.0;Normal
|
||||||
2025-07-26 12:59:28;Saclay;BOF;2.25;8.0;Normal
|
2025-07-28 06:30:59;Saclay;BOF;2.75;8.0;Normal
|
||||||
2025-07-26 12:54:25;Saclay;BOF;1.00;8.0;Normal
|
2025-07-28 06:25:56;Saclay;BOF;2.00;8.0;Normal
|
||||||
2025-07-26 12:49:22;Saclay;BOF;2.00;8.0;Normal
|
2025-07-28 06:20:54;Saclay;BOF;2.00;8.0;Normal
|
||||||
2025-07-26 12:44:20;Saclay;BOF;0.75;8.0;Normal
|
2025-07-28 06:15:51;Saclay;BOF;0.25;8.0;Normal
|
||||||
2025-07-26 12:39:17;Saclay;BOF;2.00;8.0;Normal
|
2025-07-28 06:10:48;Saclay;BOF;2.50;8.0;Normal
|
||||||
2025-07-26 13:04:31;Saclay;Viandes;21.00;6.0;Dépassement
|
2025-07-28 06:36:03;Saclay;MeP;5.75;8.0;Normal
|
||||||
2025-07-26 12:59:28;Saclay;Viandes;21.00;6.0;Dépassement
|
2025-07-28 06:31:00;Saclay;MeP;7.50;8.0;Normal
|
||||||
2025-07-26 12:54:26;Saclay;Viandes;20.75;6.0;Dépassement
|
2025-07-28 06:25:58;Saclay;MeP;6.75;8.0;Normal
|
||||||
2025-07-26 12:49:23;Saclay;Viandes;21.00;6.0;Dépassement
|
2025-07-28 06:20:55;Saclay;MeP;5.75;8.0;Normal
|
||||||
2025-07-26 12:44:20;Saclay;Viandes;20.75;6.0;Dépassement
|
2025-07-28 06:15:53;Saclay;MeP;3.00;8.0;Normal
|
||||||
2025-07-26 12:39:18;Saclay;Viandes;21.00;6.0;Dépassement
|
2025-07-28 06:10:50;Saclay;MeP;5.00;8.0;Normal
|
||||||
2025-07-26 13:04:31;Saclay;Legumes;5.25;10.0;Normal
|
2025-07-27 09:19:44;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-26 12:59:29;Saclay;Legumes;4.25;10.0;Normal
|
2025-07-26 18:10:43;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-26 12:54:26;Saclay;Legumes;2.75;10.0;Normal
|
2025-07-26 14:20:01;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-26 12:49:23;Saclay;Legumes;5.50;10.0;Normal
|
2025-07-26 14:17:35;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-26 12:44:21;Saclay;Legumes;5.00;10.0;Normal
|
|
||||||
2025-07-26 12:39:18;Saclay;Legumes;3.75;10.0;Normal
|
|
||||||
2025-07-26 13:04:32;Saclay;MeP;6.25;8.0;Normal
|
|
||||||
2025-07-26 12:59:29;Saclay;MeP;7.75;8.0;Normal
|
|
||||||
2025-07-26 12:54:27;Saclay;MeP;7.00;8.0;Normal
|
|
||||||
2025-07-26 12:49:24;Saclay;MeP;5.75;8.0;Normal
|
|
||||||
2025-07-26 12:44:21;Saclay;MeP;3.25;8.0;Normal
|
|
||||||
2025-07-26 12:39:19;Saclay;MeP;4.75;8.0;Normal
|
|
||||||
2025-07-26 07:09:45;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-26 07:09:45;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-25 14:37:50;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-25 14:37:50;Meudon;Viandes;3.94;6.0;Normal
|
||||||
2025-07-25 14:32:11;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-27 09:19:44;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-25 14:30:20;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-26 18:10:43;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-24 11:00:36;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-26 14:20:01;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-24 10:41:08;Meudon;Viandes;3.94;6.0;Normal
|
2025-07-26 14:17:35;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-26 07:09:45;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-26 07:09:45;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-25 14:37:50;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-25 14:37:50;Meudon;Poissons;3.94;6.0;Normal
|
||||||
2025-07-25 14:32:11;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-27 09:19:44;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-25 14:30:20;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-26 18:10:43;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-24 11:00:36;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-26 14:20:01;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-24 10:41:08;Meudon;Poissons;3.94;6.0;Normal
|
2025-07-26 14:17:35;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-26 07:09:45;Meudon;BOF;3.00;8.0;Normal
|
2025-07-26 07:09:45;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-25 14:37:50;Meudon;BOF;3.00;8.0;Normal
|
2025-07-25 14:37:50;Meudon;BOF;3.00;8.0;Normal
|
||||||
2025-07-25 14:32:11;Meudon;BOF;3.00;8.0;Normal
|
|
||||||
2025-07-25 14:30:20;Meudon;BOF;3.00;8.0;Normal
|
|
||||||
2025-07-24 11:00:36;Meudon;BOF;3.00;8.0;Normal
|
|
||||||
2025-07-24 10:41:08;Meudon;BOF;3.00;8.0;Normal
|
|
||||||
|
|||||||
|
114
app/domo91.py
114
app/domo91.py
@@ -23,7 +23,7 @@ st.write("Bienvenue sur l’application de supervision.")
|
|||||||
for key, default in {
|
for key, default in {
|
||||||
"authenticated": False,
|
"authenticated": False,
|
||||||
"role": None,
|
"role": None,
|
||||||
"lieu_autorise": None,
|
"site_autorise": None,
|
||||||
"onglet_actif": "Accueil",
|
"onglet_actif": "Accueil",
|
||||||
"selected_date": date.today(),
|
"selected_date": date.today(),
|
||||||
"selected_site": "Saclay",
|
"selected_site": "Saclay",
|
||||||
@@ -53,7 +53,7 @@ def verifier_password(input_password, hash_en_base):
|
|||||||
|
|
||||||
|
|
||||||
# --- Connexion utilisateur ---
|
# --- Connexion utilisateur ---
|
||||||
if not st.session_state["authenticated"]:
|
if not st.session_state.get("authenticated", False):
|
||||||
login = st.sidebar.text_input("Nom d'utilisateur")
|
login = st.sidebar.text_input("Nom d'utilisateur")
|
||||||
password = st.sidebar.text_input("Mot de passe", type="password")
|
password = st.sidebar.text_input("Mot de passe", type="password")
|
||||||
|
|
||||||
@@ -61,27 +61,37 @@ if not st.session_state["authenticated"]:
|
|||||||
try:
|
try:
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT * FROM MotsDePasse WHERE utilisateur = %s", (login,))
|
|
||||||
|
# On interroge la bonne table
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT NomUtilisateur, MotDePasseHash, role, Site, DateExpiration
|
||||||
|
FROM Acces.Utilisateurs
|
||||||
|
WHERE NomUtilisateur = %s
|
||||||
|
LIMIT 1
|
||||||
|
""", (login,))
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
|
|
||||||
if result and verifier_password(password, result["mot_de_passe"]):
|
if not result:
|
||||||
if result["Expiration"] and result["Expiration"] < date.today():
|
st.sidebar.error("Identifiants invalides")
|
||||||
st.sidebar.error("⛔ Accès expiré.")
|
elif result["DateExpiration"] and result["DateExpiration"] < date.today():
|
||||||
cursor.close()
|
st.sidebar.error("⛔ Accès expiré.")
|
||||||
conn.close()
|
elif not verifier_password(password, result["MotDePasseHash"]):
|
||||||
st.stop()
|
st.sidebar.error("Identifiants invalides")
|
||||||
|
else:
|
||||||
|
# Authentification réussie
|
||||||
st.session_state.update({
|
st.session_state.update({
|
||||||
"authenticated": True,
|
"authenticated": True,
|
||||||
"role": result["role"],
|
"role": result["role"],
|
||||||
"lieu_autorise": result["Lieu"]
|
"site_autorise": result["Site"]
|
||||||
})
|
})
|
||||||
|
|
||||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
cursor.execute("INSERT INTO Connexion_Log (Utilisateur, Lieu, Date_Connexion) VALUES (%s, %s, %s)",
|
cursor.execute("""
|
||||||
(login, result["Lieu"], now_str))
|
INSERT INTO Sondes.Connexion_Log (Utilisateur, Lieu, Date_Connexion)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""", (result["NomUtilisateur"], result["Site"], now_str))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
else:
|
|
||||||
st.sidebar.error("Identifiants invalides")
|
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -89,22 +99,61 @@ if not st.session_state["authenticated"]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
st.sidebar.error(f"Erreur connexion : {e}")
|
st.sidebar.error(f"Erreur connexion : {e}")
|
||||||
else:
|
else:
|
||||||
st.sidebar.success(f"Connecté ({st.session_state['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", "lieu_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.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
# --- Bandeau Alertes ---
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
|
# Récupérer le site autorisé depuis la session
|
||||||
|
site = st.session_state.get("lieu_autorise")
|
||||||
|
|
||||||
|
if site:
|
||||||
|
# Lecture des alertes non acquittées pour ce site
|
||||||
|
cursor.execute(f"""
|
||||||
|
SELECT Id, Sonde, Debut_defaut, Etat
|
||||||
|
FROM Alertes_{site}
|
||||||
|
WHERE Etat != 'Acquitté'
|
||||||
|
ORDER BY Debut_defaut DESC
|
||||||
|
""")
|
||||||
|
alertes = cursor.fetchall()
|
||||||
|
|
||||||
|
if alertes:
|
||||||
|
st.markdown(
|
||||||
|
f"<div style='background-color:#ffcccc;padding:10px;border-radius:8px;'>"
|
||||||
|
f"🚨 <b>{len(alertes)} alerte(s) non résolue(s)</b> sur <b>{site}</b>"
|
||||||
|
f"</div>",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
st.markdown(
|
||||||
|
f"<div style='background-color:#ccffcc;padding:10px;border-radius:8px;'>"
|
||||||
|
f"✅ Aucune alerte en cours sur <b>{site}</b>"
|
||||||
|
f"</div>",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Erreur lors de la récupération des alertes : {e}")
|
||||||
|
|
||||||
|
|
||||||
# --- Navigation ---
|
# --- Navigation ---
|
||||||
if st.session_state["authenticated"]:
|
if st.session_state["authenticated"]:
|
||||||
onglets = ["Accueil", "Entretien"] if st.session_state["role"] != "superviseur" else ["Accueil", "Statistiques",
|
onglets = ["Accueil", "Entretien"] if st.session_state["role"] != "superviseur" else ["Accueil", "Statistiques",
|
||||||
"Entretien", "Traffic",
|
"Entretien", "Traffic"]
|
||||||
"Utilisateurs"]
|
|
||||||
onglet_selectionne = st.sidebar.radio("📁 Navigation", onglets,
|
onglet_selectionne = st.sidebar.radio("📁 Navigation", onglets,
|
||||||
index=onglets.index(st.session_state["onglet_actif"]))
|
index=onglets.index(st.session_state["onglet_actif"]))
|
||||||
st.session_state["onglet_actif"] = onglet_selectionne
|
st.session_state["onglet_actif"] = onglet_selectionne
|
||||||
|
|
||||||
site_actuel = st.session_state.get("lieu_autorise") if st.session_state[
|
site_actuel = st.session_state.get("site_autorise") if st.session_state[
|
||||||
"role"] != "superviseur" else st.session_state.get(
|
"role"] != "superviseur" else st.session_state.get(
|
||||||
"selected_site", "Saclay")
|
"selected_site", "Saclay")
|
||||||
date_selectionnee = st.session_state.get("selected_date", date.today())
|
date_selectionnee = st.session_state.get("selected_date", date.today())
|
||||||
@@ -192,7 +241,7 @@ if st.session_state["authenticated"]:
|
|||||||
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["lieu_autorise"]
|
st.session_state["site_autorise"]
|
||||||
if st.session_state["role"] != "superviseur"
|
if st.session_state["role"] != "superviseur"
|
||||||
else st.session_state.get("selected_site", "Saclay")
|
else st.session_state.get("selected_site", "Saclay")
|
||||||
)
|
)
|
||||||
@@ -203,7 +252,7 @@ if st.session_state["authenticated"]:
|
|||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
|
||||||
site = (
|
site = (
|
||||||
st.session_state["lieu_autorise"]
|
st.session_state["site_autorise"]
|
||||||
if st.session_state["role"] != "superviseur"
|
if st.session_state["role"] != "superviseur"
|
||||||
else st.session_state.get("selected_site", "Saclay")
|
else st.session_state.get("selected_site", "Saclay")
|
||||||
)
|
)
|
||||||
@@ -333,26 +382,3 @@ if st.session_state["authenticated"]:
|
|||||||
st.error(f"Erreur : {e}")
|
st.error(f"Erreur : {e}")
|
||||||
st.text(traceback.format_exc())
|
st.text(traceback.format_exc())
|
||||||
|
|
||||||
# --- Onglet Utilisateurs ---
|
|
||||||
elif onglet_selectionne == "Utilisateurs":
|
|
||||||
st.header("👥 Gestion des utilisateurs")
|
|
||||||
with st.form("ajouter_utilisateur"):
|
|
||||||
new_user = st.text_input("Nom d'utilisateur")
|
|
||||||
new_pass = st.text_input("Mot de passe", type="password")
|
|
||||||
new_role = st.selectbox("Rôle", ["utilisateur", "superviseur"])
|
|
||||||
new_lieu = st.selectbox("Lieu", ["Saclay", "Meudon", "Roissy"])
|
|
||||||
if st.form_submit_button("Ajouter"):
|
|
||||||
try:
|
|
||||||
conn = get_connection()
|
|
||||||
cursor = conn.cursor()
|
|
||||||
hash_mdp = hash_password(new_pass)
|
|
||||||
cursor.execute(
|
|
||||||
"INSERT INTO MotsDePasse (utilisateur, mot_de_passe, role, Lieu) VALUES (%s, %s, %s, %s)",
|
|
||||||
(new_user, hash_mdp, new_role, new_lieu))
|
|
||||||
conn.commit()
|
|
||||||
cursor.close()
|
|
||||||
conn.close()
|
|
||||||
st.success("Utilisateur ajouté.")
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"Erreur : {e}")
|
|
||||||
st.text(traceback.format_exc())
|
|
||||||
@@ -19,16 +19,34 @@ def envoyer_sms(message: str, lieu: str = ""):
|
|||||||
return
|
return
|
||||||
|
|
||||||
service_name = services[0]
|
service_name = services[0]
|
||||||
numero_dest = os.getenv("NUMERO_DESTINATAIRE")
|
numero_dest = os.getenv("SMS_RECEIVER")
|
||||||
|
sender = os.getenv("OVH_SMS_SENDER")
|
||||||
|
|
||||||
|
if numero_dest.startswith('+'):
|
||||||
|
numero_dest = '00' + numero_dest[1:]
|
||||||
|
|
||||||
|
if not numero_dest or not numero_dest.isdigit():
|
||||||
|
print(f"❌ Numéro de téléphone invalide ou manquant : '{numero_dest}'", flush=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"sender": sender,
|
||||||
|
"receivers": [numero_dest],
|
||||||
|
"message": message, # Pas d'encodage ni de nettoyage ici
|
||||||
|
"priority": "high",
|
||||||
|
"noStopClause": False
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
print("📤 Requête envoyée à OVH :")
|
||||||
|
print(payload)
|
||||||
|
|
||||||
|
result = client.post(f'/sms/{service_name}/jobs', **payload)
|
||||||
|
|
||||||
result = client.post(f'/sms/{service_name}/jobs',
|
|
||||||
sender='Monitor',
|
|
||||||
message=message,
|
|
||||||
receivers=[numero_dest],
|
|
||||||
noStopClause=True
|
|
||||||
)
|
|
||||||
print(f"📱 SMS envoyé à {numero_dest} pour {lieu}. Job ID : {result['ids']}", flush=True)
|
print(f"📱 SMS envoyé à {numero_dest} pour {lieu}. Job ID : {result['ids']}", flush=True)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erreur envoi SMS : {e}", flush=True)
|
print(f"❌ Erreur envoi SMS : {e}", flush=True)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
envoyer_sms("Test SMS OVH", lieu="utils_sms")
|
||||||
|
|||||||
32
scripts/envoi_sms.php
Normal file
32
scripts/envoi_sms.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Lists and displays the details for each SMS account
|
||||||
|
*
|
||||||
|
* Go to https://eu.api.ovh.com/createToken/index.cgi?GET=/sms/&GET=/sms/*/jobs&POST=/sms/*/jobs
|
||||||
|
* to generate the API access keys for:
|
||||||
|
*
|
||||||
|
* GET /sms
|
||||||
|
* GET /sms/*/jobs
|
||||||
|
* POST /sms/*/jobs
|
||||||
|
*/
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
use \Ovh\Api;
|
||||||
|
|
||||||
|
$endpoint = 'ovh-eu';
|
||||||
|
$applicationKey = "f725d07b2f98a195";
|
||||||
|
$applicationSecret = "5ca392a0a728e2395edd426bb1e11ad6";
|
||||||
|
$consumer_key = "305f2e8611e58b83930de84ee65c99f9";
|
||||||
|
|
||||||
|
$conn = new Api( $applicationKey,
|
||||||
|
$applicationSecret,
|
||||||
|
$endpoint,
|
||||||
|
$consumer_key);
|
||||||
|
|
||||||
|
$smsServices = $conn->get('/sms/');
|
||||||
|
foreach ($smsServices as $smsService) {
|
||||||
|
|
||||||
|
print_r($smsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user