Update project echo files
This commit is contained in:
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# On part d'une version légère de Python
|
||||||
|
FROM python:3.10-slim
|
||||||
|
|
||||||
|
# On se place dans le dossier /app du conteneur
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# On installe les outils système nécessaires pour la base de données
|
||||||
|
RUN apt-get update && apt-get install -y libpq-dev gcc
|
||||||
|
|
||||||
|
# On copie d'abord le fichier des dépendances
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# On installe les bibliothèques Python
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# On copie tout le reste du projet (tes HTML, CSS, app.py, etc.)
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# On expose le port 5000 pour ton Mac
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# La commande pour démarrer le serveur
|
||||||
|
CMD ["python", "app.py"]
|
||||||
288
app.py
Normal file
288
app.py
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
from flask import Flask, jsonify, render_template, request, redirect, url_for, session
|
||||||
|
from flask_cors import CORS
|
||||||
|
from config import Config
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_object(Config)
|
||||||
|
app.secret_key = app.config["SECRET_KEY"]
|
||||||
|
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
|
||||||
|
def get_connection():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=app.config["DB_HOST"],
|
||||||
|
port=app.config["DB_PORT"],
|
||||||
|
database=app.config["DB_NAME"],
|
||||||
|
user=app.config["DB_USER"],
|
||||||
|
password=app.config["DB_PASSWORD"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_setting_value(cur, key, default_value):
|
||||||
|
cur.execute("SELECT value FROM settings WHERE key = %s", (key,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
return row[0] if row else default_value
|
||||||
|
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# MODIFICATION ÉTUDIANT 3 : Sécurisation de l'accueil
|
||||||
|
# ========================================================
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
# Si l'utilisateur n'est pas connecté en tant qu'admin,
|
||||||
|
# on le force à aller sur la page de connexion.
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
# S'il est connecté, on lui affiche le Dashboard.
|
||||||
|
return render_template("index.html")
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/status")
|
||||||
|
def status():
|
||||||
|
return jsonify({
|
||||||
|
"project": "EcoCharge",
|
||||||
|
"api": "ok",
|
||||||
|
"database": "postgresql_connected"
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/data", methods=["POST"])
|
||||||
|
def receive_data():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({"error": "Aucune donnée JSON reçue"}), 400
|
||||||
|
|
||||||
|
# Données environnement
|
||||||
|
temperature_ext = data.get("temperature_ext")
|
||||||
|
humidity_ext = data.get("humidity_ext")
|
||||||
|
system_status_msg = data.get("system_status")
|
||||||
|
|
||||||
|
# Données panneau solaire
|
||||||
|
voltage_pv = data.get("voltage_pv")
|
||||||
|
current_pv = data.get("current_pv")
|
||||||
|
|
||||||
|
# compatible solar_power
|
||||||
|
power_pv = data.get("power_pv", data.get("solar_power"))
|
||||||
|
|
||||||
|
luminosity = data.get("luminosity")
|
||||||
|
|
||||||
|
# Données batterie
|
||||||
|
voltage_battery = data.get("voltage_battery", data.get("battery_voltage"))
|
||||||
|
current_battery = data.get("current_battery")
|
||||||
|
|
||||||
|
# compatible battery_temperature
|
||||||
|
battery_temp = data.get("battery_temp", data.get("battery_temperature"))
|
||||||
|
|
||||||
|
battery_level = data.get("battery_level")
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
max_battery_temperature = float(get_setting_value(cur, "max_battery_temperature", "60"))
|
||||||
|
min_battery_voltage = float(get_setting_value(cur, "min_battery_voltage", "11"))
|
||||||
|
min_solar_power = float(get_setting_value(cur, "min_solar_power", "5"))
|
||||||
|
|
||||||
|
battery_alert = "none"
|
||||||
|
|
||||||
|
if battery_temp is not None and float(battery_temp) > max_battery_temperature:
|
||||||
|
battery_alert = "Température batterie trop élevée"
|
||||||
|
|
||||||
|
elif voltage_battery is not None and float(voltage_battery) < min_battery_voltage:
|
||||||
|
battery_alert = "Tension batterie trop faible"
|
||||||
|
|
||||||
|
elif power_pv is not None and float(power_pv) < min_solar_power:
|
||||||
|
battery_alert = "Puissance solaire insuffisante"
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO telemetry (
|
||||||
|
temperature_ext,
|
||||||
|
humidity_ext,
|
||||||
|
system_status_msg,
|
||||||
|
voltage_pv,
|
||||||
|
current_pv,
|
||||||
|
power_pv,
|
||||||
|
luminosity,
|
||||||
|
voltage_battery,
|
||||||
|
current_battery,
|
||||||
|
battery_temp,
|
||||||
|
battery_level,
|
||||||
|
battery_alert
|
||||||
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
|
""", (
|
||||||
|
temperature_ext,
|
||||||
|
humidity_ext,
|
||||||
|
system_status_msg,
|
||||||
|
voltage_pv,
|
||||||
|
current_pv,
|
||||||
|
power_pv,
|
||||||
|
luminosity,
|
||||||
|
voltage_battery,
|
||||||
|
current_battery,
|
||||||
|
battery_temp,
|
||||||
|
battery_level,
|
||||||
|
battery_alert
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"message": "Données enregistrées avec succès",
|
||||||
|
"battery_alert": battery_alert
|
||||||
|
}), 201
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/latest", methods=["GET"])
|
||||||
|
def latest_data():
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
created_at,
|
||||||
|
temperature_ext,
|
||||||
|
humidity_ext,
|
||||||
|
system_status_msg,
|
||||||
|
voltage_pv,
|
||||||
|
current_pv,
|
||||||
|
power_pv,
|
||||||
|
luminosity,
|
||||||
|
voltage_battery,
|
||||||
|
current_battery,
|
||||||
|
battery_temp,
|
||||||
|
battery_level,
|
||||||
|
battery_alert
|
||||||
|
FROM telemetry
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return jsonify({"message": "Aucune donnée disponible"}), 404
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"id": row[0],
|
||||||
|
"created_at": str(row[1]),
|
||||||
|
"temperature_ext": row[2],
|
||||||
|
"humidity_ext": row[3],
|
||||||
|
"system_status": row[4],
|
||||||
|
"voltage_pv": row[5],
|
||||||
|
"current_pv": row[6],
|
||||||
|
"power_pv": row[7],
|
||||||
|
"luminosity": row[8],
|
||||||
|
"voltage_battery": row[9],
|
||||||
|
"current_battery": row[10],
|
||||||
|
"battery_temp": row[11],
|
||||||
|
"battery_level": row[12],
|
||||||
|
"battery_alert": row[13]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/login", methods=["GET", "POST"])
|
||||||
|
def admin_login():
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
username = request.form.get("username")
|
||||||
|
password = request.form.get("password")
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id FROM users WHERE username = %s AND password = %s AND is_admin = TRUE",
|
||||||
|
(username, password)
|
||||||
|
)
|
||||||
|
|
||||||
|
user = cur.fetchone()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if user:
|
||||||
|
session["admin_logged_in"] = True
|
||||||
|
|
||||||
|
# ========================================================
|
||||||
|
# MODIFICATION ÉTUDIANT 3 : Redirection vers le Dashboard
|
||||||
|
# ========================================================
|
||||||
|
return redirect(url_for("home"))
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
|
return render_template("login.html", error="Identifiants incorrects")
|
||||||
|
|
||||||
|
return render_template("login.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/dashboard", methods=["GET", "POST"])
|
||||||
|
def admin_dashboard():
|
||||||
|
|
||||||
|
if not session.get("admin_logged_in"):
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
min_battery = request.form.get("min_battery")
|
||||||
|
max_temp = request.form.get("max_temp")
|
||||||
|
min_power = request.form.get("min_power")
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
VALUES ('min_battery_voltage', %s)
|
||||||
|
ON CONFLICT (key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (min_battery,))
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
VALUES ('max_battery_temperature', %s)
|
||||||
|
ON CONFLICT (key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (max_temp,))
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
VALUES ('min_solar_power', %s)
|
||||||
|
ON CONFLICT (key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value
|
||||||
|
""", (min_power,))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
"min_battery": get_setting_value(cur, "min_battery_voltage", "11"),
|
||||||
|
"max_temp": get_setting_value(cur, "max_battery_temperature", "60"),
|
||||||
|
"min_power": get_setting_value(cur, "min_solar_power", "5")
|
||||||
|
}
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return render_template("admin_dashboard.html", settings=settings)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/logout")
|
||||||
|
def admin_logout():
|
||||||
|
|
||||||
|
session.pop("admin_logged_in", None)
|
||||||
|
|
||||||
|
return redirect(url_for("admin_login"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
10
config.py
Normal file
10
config.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
# S'il trouve la variable Docker, il l'utilise. Sinon, il met "localhost" par défaut.
|
||||||
|
DB_HOST = os.environ.get("DB_HOST", "localhost")
|
||||||
|
DB_PORT = os.environ.get("DB_PORT", "5432")
|
||||||
|
DB_NAME = os.environ.get("DB_NAME", "ecocharge")
|
||||||
|
DB_USER = os.environ.get("DB_USER", "ecocharge_user")
|
||||||
|
DB_PASSWORD = os.environ.get("DB_PASSWORD", "ecocharge_password")
|
||||||
|
SECRET_KEY = os.environ.get("SECRET_KEY", "cle_par_defaut")
|
||||||
46
database.sql
Normal file
46
database.sql
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
DROP TABLE IF EXISTS telemetry CASCADE;
|
||||||
|
DROP TABLE IF EXISTS system_status CASCADE;
|
||||||
|
DROP TABLE IF EXISTS settings CASCADE;
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
|
|
||||||
|
CREATE TABLE users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
is_admin BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE settings (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
key VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
value VARCHAR(100) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE telemetry (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
temperature_ext DOUBLE PRECISION,
|
||||||
|
humidity_ext DOUBLE PRECISION,
|
||||||
|
system_status_msg VARCHAR(100),
|
||||||
|
|
||||||
|
voltage_pv DOUBLE PRECISION,
|
||||||
|
current_pv DOUBLE PRECISION,
|
||||||
|
power_pv DOUBLE PRECISION,
|
||||||
|
luminosity DOUBLE PRECISION,
|
||||||
|
|
||||||
|
voltage_battery DOUBLE PRECISION,
|
||||||
|
current_battery DOUBLE PRECISION,
|
||||||
|
battery_temp DOUBLE PRECISION,
|
||||||
|
battery_level DOUBLE PRECISION,
|
||||||
|
battery_alert VARCHAR(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users (username, password, is_admin)
|
||||||
|
VALUES ('admin', 'admin123', TRUE);
|
||||||
|
|
||||||
|
INSERT INTO settings (key, value)
|
||||||
|
VALUES
|
||||||
|
('min_battery_voltage', '11'),
|
||||||
|
('max_battery_temperature', '60'),
|
||||||
|
('min_solar_power', '5');
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 1. LA BASE DE DONNÉES (PostgreSQL)
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine
|
||||||
|
container_name: ecocharge_db
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ecocharge_user
|
||||||
|
POSTGRES_PASSWORD: ecocharge_password
|
||||||
|
POSTGRES_DB: ecocharge
|
||||||
|
ports:
|
||||||
|
- "5432:5432" # <-- Très important : ça te permet de t'y connecter avec DBeaver !
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
# La ligne magique : Docker va lire ton database.sql et créer les tables tout seul au premier lancement !
|
||||||
|
- ./database.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
|
||||||
|
# 2. TON SERVEUR WEB (Flask)
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
container_name: ecocharge_web
|
||||||
|
ports:
|
||||||
|
- "5001:5000" # Ton Dashboard sera sur http://localhost:5000
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
environment:
|
||||||
|
# On donne les identifiants au serveur Python pour qu'il trouve la BDD
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_NAME=ecocharge
|
||||||
|
- DB_USER=ecocharge_user
|
||||||
|
- DB_PASSWORD=ecocharge_password
|
||||||
|
- SECRET_KEY=super_cle_secrete_projet
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==3.0.0
|
||||||
|
Flask-Cors==4.0.0
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
115
script.js
115
script.js
@@ -1,115 +0,0 @@
|
|||||||
// --- VARIABLES D'ÉTAT ---
|
|
||||||
let isOnline = true;
|
|
||||||
let lastHeartbeat = Date.now();
|
|
||||||
const TIMEOUT_MS = 5000; // 5 secondes pour les tests
|
|
||||||
|
|
||||||
// --- ÉLÉMENTS DU DOM ---
|
|
||||||
const statusDot = document.getElementById('system-status');
|
|
||||||
const statusText = document.getElementById('status-text');
|
|
||||||
|
|
||||||
const tempEl = document.getElementById('temp-val');
|
|
||||||
const humEl = document.getElementById('hum-val');
|
|
||||||
|
|
||||||
const apiTempEl = document.getElementById('api-temp');
|
|
||||||
const apiDescEl = document.getElementById('api-desc');
|
|
||||||
const apiIconEl = document.getElementById('api-icon');
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// --- NOUVEAU : GESTION DE LA VRAIE API MÉTÉO (OPEN-METEO) ---
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
// 1. Traduction des codes météo de l'API en Icônes et Textes
|
|
||||||
function getWeatherDetails(code) {
|
|
||||||
if (code === 0) return { desc: "Dégagé", icon: "bi-brightness-high", color: "text-warning" };
|
|
||||||
if (code === 1 || code === 2 || code === 3) return { desc: "Nuageux", icon: "bi-cloud", color: "text-light" };
|
|
||||||
if (code >= 45 && code <= 48) return { desc: "Brouillard", icon: "bi-cloud-haze", color: "text-secondary" };
|
|
||||||
if (code >= 51 && code <= 67) return { desc: "Pluie", icon: "bi-cloud-rain", color: "text-info" };
|
|
||||||
if (code >= 71 && code <= 77) return { desc: "Neige", icon: "bi-cloud-snow", color: "text-white" };
|
|
||||||
if (code >= 95) return { desc: "Orage", icon: "bi-cloud-lightning", color: "text-danger" };
|
|
||||||
return { desc: "Inconnu", icon: "bi-thermometer", color: "text-muted" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fonction qui va chercher les données sur Internet
|
|
||||||
async function fetchRealWeather() {
|
|
||||||
try {
|
|
||||||
// --- METS LES COORDONNÉES DE TA VILLE ICI ---
|
|
||||||
const lat = 48.8566; // Latitude (Ex: Paris)
|
|
||||||
const lon = 2.3522; // Longitude (Ex: Paris)
|
|
||||||
|
|
||||||
// L'URL magique de l'API Open-Meteo
|
|
||||||
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code`;
|
|
||||||
|
|
||||||
// On lance la requête
|
|
||||||
const response = await fetch(url);
|
|
||||||
const data = await response.json(); // On convertit la réponse en JSON
|
|
||||||
|
|
||||||
// On extrait la température et le code météo
|
|
||||||
const temp = data.current.temperature_2m;
|
|
||||||
const code = data.current.weather_code;
|
|
||||||
|
|
||||||
// On récupère la bonne icône et le bon texte
|
|
||||||
const weather = getWeatherDetails(code);
|
|
||||||
|
|
||||||
// On met à jour l'interface HTML !
|
|
||||||
apiTempEl.innerText = temp + "°C";
|
|
||||||
apiDescEl.innerText = weather.desc;
|
|
||||||
apiIconEl.className = "bi " + weather.icon + " " + weather.color + " display-4 me-3";
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur avec l'API Météo:", error);
|
|
||||||
apiDescEl.innerText = "Erreur Web";
|
|
||||||
apiIconEl.className = "bi bi-wifi-off text-danger display-4 me-3";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
// --- FONCTION DE MISE À JOUR DES DONNÉES LOCALES (Ton DHT11) ---
|
|
||||||
function fetchLocalData() {
|
|
||||||
if (!isOnline) return;
|
|
||||||
|
|
||||||
// Simulation de ton capteur DHT11 local
|
|
||||||
tempEl.innerText = (22 + (Math.random() * 2 - 1)).toFixed(1);
|
|
||||||
humEl.innerText = Math.floor(45 + Math.random() * 5);
|
|
||||||
|
|
||||||
lastHeartbeat = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- FONCTION WATCHDOG (LE HEARTBEAT) ---
|
|
||||||
function checkSystemStatus() {
|
|
||||||
if (Date.now() - lastHeartbeat > TIMEOUT_MS) {
|
|
||||||
statusDot.className = "status-dot rounded-circle pulse-offline";
|
|
||||||
statusText.innerText = "HORS LIGNE";
|
|
||||||
statusText.style.color = "#ef4444";
|
|
||||||
} else {
|
|
||||||
statusDot.className = "status-dot rounded-circle pulse-online";
|
|
||||||
statusText.innerText = "EN LIGNE";
|
|
||||||
statusText.style.color = "#e2e8f0";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- LANCEMENT DES PROGRAMMES ---
|
|
||||||
|
|
||||||
// 1. Lancer la météo Web une première fois tout de suite
|
|
||||||
fetchRealWeather();
|
|
||||||
|
|
||||||
// 2. Les boucles automatiques
|
|
||||||
setInterval(fetchLocalData, 3000); // Mise à jour de ton capteur local toutes les 3s
|
|
||||||
setInterval(checkSystemStatus, 1000); // Surveillance de la connexion
|
|
||||||
|
|
||||||
// ATTENTION: On ne met à jour la météo Web que toutes les 15 minutes (900 000 ms)
|
|
||||||
// pour ne pas saturer l'API gratuite et se faire bloquer !
|
|
||||||
setInterval(fetchRealWeather, 900000);
|
|
||||||
|
|
||||||
// --- GESTION DU BOUTON DE TEST ---
|
|
||||||
document.getElementById('btn-test').addEventListener('click', function () {
|
|
||||||
isOnline = !isOnline;
|
|
||||||
if (isOnline) {
|
|
||||||
this.innerHTML = '<i class="bi bi-wifi-off"></i> Simuler Coupure';
|
|
||||||
this.classList.replace('btn-outline-success', 'btn-outline-danger');
|
|
||||||
lastHeartbeat = Date.now();
|
|
||||||
} else {
|
|
||||||
this.innerHTML = '<i class="bi bi-wifi"></i> Rétablir Connexion';
|
|
||||||
this.classList.replace('btn-outline-danger', 'btn-outline-success');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
202
static/script.js
Normal file
202
static/script.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// --- VARIABLES D'ÉTAT ---
|
||||||
|
let isOnline = true;
|
||||||
|
let lastHeartbeat = Date.now();
|
||||||
|
const TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
// --- ÉLÉMENTS DU DOM PRINCIPAUX ---
|
||||||
|
const statusDot = document.getElementById('system-status');
|
||||||
|
const statusText = document.getElementById('status-text');
|
||||||
|
|
||||||
|
const tempEl = document.getElementById('temp-val');
|
||||||
|
const humEl = document.getElementById('hum-val');
|
||||||
|
|
||||||
|
const apiTempEl = document.getElementById('api-temp');
|
||||||
|
const apiDescEl = document.getElementById('api-desc');
|
||||||
|
const apiIconEl = document.getElementById('api-icon');
|
||||||
|
|
||||||
|
// --- ÉLÉMENTS DOM BATTERIE (Étudiant 2) ---
|
||||||
|
const batValEl = document.getElementById('bat-val');
|
||||||
|
const batCircleEl = document.getElementById('bat-circle');
|
||||||
|
const batAlertBadge = document.getElementById('bat-alert-badge');
|
||||||
|
const batAlertIcon = document.getElementById('bat-alert-icon');
|
||||||
|
const batAlertText = document.getElementById('bat-alert');
|
||||||
|
|
||||||
|
// --- CONSTANTES SOLAIRE (Étudiant 1) ---
|
||||||
|
const MAX_POWER = 100; // Puissance maximum du panneau en Watts (à adapter si besoin)
|
||||||
|
const gaugeCircle = document.querySelector('.gauge-fill circle');
|
||||||
|
const halfCircumference = 283 / 2;
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// --- GESTION DE LA VRAIE API MÉTÉO WEB (Open-Meteo) ---
|
||||||
|
// =================================================================
|
||||||
|
function getWeatherDetails(code) {
|
||||||
|
if (code === 0) return { desc: "Dégagé", icon: "bi-brightness-high", color: "text-warning" };
|
||||||
|
if (code >= 1 && code <= 3) return { desc: "Nuageux", icon: "bi-cloud", color: "text-light" };
|
||||||
|
if (code >= 45 && code <= 48) return { desc: "Brouillard", icon: "bi-cloud-haze", color: "text-secondary" };
|
||||||
|
if (code >= 51 && code <= 67) return { desc: "Pluie", icon: "bi-cloud-rain", color: "text-info" };
|
||||||
|
if (code >= 71 && code <= 77) return { desc: "Neige", icon: "bi-cloud-snow", color: "text-white" };
|
||||||
|
if (code >= 95) return { desc: "Orage", icon: "bi-cloud-lightning", color: "text-danger" };
|
||||||
|
return { desc: "Inconnu", icon: "bi-thermometer", color: "text-muted" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchRealWeather() {
|
||||||
|
try {
|
||||||
|
const lat = 48.8566; // Tu peux changer avec la latitude de ta ville
|
||||||
|
const lon = 2.3522; // Tu peux changer avec la longitude de ta ville
|
||||||
|
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,weather_code`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const temp = data.current.temperature_2m;
|
||||||
|
const code = data.current.weather_code;
|
||||||
|
const weather = getWeatherDetails(code);
|
||||||
|
|
||||||
|
apiTempEl.innerText = temp + "°C";
|
||||||
|
apiDescEl.innerText = weather.desc;
|
||||||
|
apiIconEl.className = "bi " + weather.icon + " " + weather.color + " display-4 me-3";
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur Météo:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// --- MISE À JOUR VISUELLE DES WIDGETS ---
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
function updateBattery(percent) {
|
||||||
|
batValEl.innerText = percent + "%";
|
||||||
|
let color = "#34d399";
|
||||||
|
let shadowColor = "rgba(52, 211, 153, 0.4)";
|
||||||
|
|
||||||
|
if (percent <= 20) {
|
||||||
|
color = "#ef4444";
|
||||||
|
shadowColor = "rgba(239, 68, 68, 0.6)";
|
||||||
|
batAlertText.innerText = "Tension Critique !";
|
||||||
|
batAlertIcon.className = "bi bi-exclamation-triangle-fill text-danger";
|
||||||
|
batAlertBadge.className = "badge bg-dark border border-danger text-secondary w-100 py-2";
|
||||||
|
} else if (percent <= 50) {
|
||||||
|
color = "#fbbf24";
|
||||||
|
shadowColor = "rgba(251, 191, 36, 0.5)";
|
||||||
|
batAlertText.innerText = "Niveau Moyen";
|
||||||
|
batAlertIcon.className = "bi bi-exclamation-circle text-warning";
|
||||||
|
batAlertBadge.className = "badge bg-dark border border-warning text-secondary w-100 py-2";
|
||||||
|
} else {
|
||||||
|
batAlertText.innerText = "Système Normal";
|
||||||
|
batAlertIcon.className = "bi bi-check-circle text-success";
|
||||||
|
batAlertBadge.className = "badge bg-dark border border-secondary text-secondary w-100 py-2";
|
||||||
|
}
|
||||||
|
|
||||||
|
const angle = percent * 3.6;
|
||||||
|
batCircleEl.style.background = `conic-gradient(${color} ${angle}deg, #0f172a 0deg)`;
|
||||||
|
batCircleEl.style.boxShadow = `0 0 15px ${shadowColor}, inset 0 0 15px ${shadowColor}`;
|
||||||
|
batValEl.style.textShadow = `0 0 10px ${shadowColor}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSolar(voltage, current, power, lux) {
|
||||||
|
// On s'assure que ce sont des nombres et on les formate
|
||||||
|
const v = Number(voltage).toFixed(1);
|
||||||
|
const c = Number(current).toFixed(2);
|
||||||
|
const p = Number(power).toFixed(1);
|
||||||
|
const l = Math.floor(Number(lux));
|
||||||
|
|
||||||
|
// Le serveur de l'étudiant 4 ne stocke pas l'efficacité, on simule ~15% quand il y a du soleil
|
||||||
|
const efficiency = (p > 0) ? (14 + Math.random() * 2).toFixed(1) : "0.0";
|
||||||
|
|
||||||
|
document.getElementById('powerValue').textContent = p;
|
||||||
|
document.getElementById('voltage').innerHTML = v + ' <span class="stat-unit">V</span>';
|
||||||
|
document.getElementById('current').innerHTML = c + ' <span class="stat-unit">A</span>';
|
||||||
|
document.getElementById('lux').innerHTML = l.toLocaleString() + ' <span class="stat-unit">Lux</span>';
|
||||||
|
document.getElementById('efficiency').innerHTML = efficiency + ' <span class="stat-unit">%</span>';
|
||||||
|
|
||||||
|
// Animation de la jauge SVG
|
||||||
|
const ratio = Math.min(p / MAX_POWER, 1);
|
||||||
|
const offset = halfCircumference - (ratio * halfCircumference);
|
||||||
|
|
||||||
|
if (gaugeCircle) {
|
||||||
|
gaugeCircle.style.strokeDashoffset = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// --- NOUVEAU : CONNEXION AU SERVEUR PYTHON (BASE DE DONNÉES) ---
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
async function fetchDatabaseData() {
|
||||||
|
if (!isOnline) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// On interroge l'URL magique de l'étudiant 4
|
||||||
|
const response = await fetch('/api/latest');
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Base de données vide ou serveur injoignable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 1. Mise à jour de ton capteur local DHT11
|
||||||
|
tempEl.innerText = data.temperature_ext !== null ? Number(data.temperature_ext).toFixed(1) : "--";
|
||||||
|
humEl.innerText = data.humidity_ext !== null ? Math.floor(data.humidity_ext) : "--";
|
||||||
|
|
||||||
|
// 2. Mise à jour de la Batterie
|
||||||
|
if (data.battery_level !== null) {
|
||||||
|
updateBattery(Math.floor(data.battery_level));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mise à jour du Solaire
|
||||||
|
updateSolar(
|
||||||
|
data.voltage_pv || 0,
|
||||||
|
data.current_pv || 0,
|
||||||
|
data.power_pv || 0,
|
||||||
|
data.luminosity || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si on arrive ici, c'est qu'on a bien communiqué avec le serveur : on met à jour le Heartbeat
|
||||||
|
lastHeartbeat = Date.now();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("En attente de données capteurs...", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FONCTION WATCHDOG ---
|
||||||
|
function checkSystemStatus() {
|
||||||
|
if (Date.now() - lastHeartbeat > TIMEOUT_MS) {
|
||||||
|
statusDot.className = "status-dot rounded-circle pulse-offline";
|
||||||
|
statusText.innerText = "HORS LIGNE";
|
||||||
|
statusText.style.color = "#ef4444";
|
||||||
|
} else {
|
||||||
|
statusDot.className = "status-dot rounded-circle pulse-online";
|
||||||
|
statusText.innerText = "EN LIGNE";
|
||||||
|
statusText.style.color = "#e2e8f0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// --- LANCEMENT DES BOUCLES ---
|
||||||
|
// =================================================================
|
||||||
|
|
||||||
|
// Météo Web (1 fois au lancement, puis toutes les 15 min)
|
||||||
|
fetchRealWeather();
|
||||||
|
setInterval(fetchRealWeather, 900000);
|
||||||
|
|
||||||
|
// Base de données locale (toutes les 3 secondes)
|
||||||
|
setInterval(fetchDatabaseData, 3000);
|
||||||
|
|
||||||
|
// Watchdog (toutes les secondes)
|
||||||
|
setInterval(checkSystemStatus, 1000);
|
||||||
|
|
||||||
|
// Bouton de simulation de panne
|
||||||
|
document.getElementById('btn-test').addEventListener('click', function () {
|
||||||
|
isOnline = !isOnline;
|
||||||
|
if (isOnline) {
|
||||||
|
this.innerHTML = '<i class="bi bi-wifi-off"></i> Simuler Coupure';
|
||||||
|
this.classList.replace('btn-outline-success', 'btn-outline-danger');
|
||||||
|
lastHeartbeat = Date.now();
|
||||||
|
} else {
|
||||||
|
this.innerHTML = '<i class="bi bi-wifi"></i> Rétablir Connexion';
|
||||||
|
this.classList.replace('btn-outline-danger', 'btn-outline-success');
|
||||||
|
}
|
||||||
|
});
|
||||||
248
static/style.css
Normal file
248
static/style.css
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/* --- COULEURS GLOBALES ET THEME SOMBRE --- */
|
||||||
|
body {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
background-color: #1e293b !important;
|
||||||
|
border-bottom: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- DESIGN DES WIDGETS (CARTES) --- */
|
||||||
|
.card {
|
||||||
|
background-color: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
min-height: 320px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meteo {
|
||||||
|
border-top: 4px solid #38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-solaire {
|
||||||
|
border-top: 4px solid #fbbf24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-batterie {
|
||||||
|
border-top: 4px solid #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-admin {
|
||||||
|
border-top: 4px solid #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ANIMATION DU HEARTBEAT PRINCIPAL --- */
|
||||||
|
.status-dot {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-online {
|
||||||
|
background-color: #22c55e;
|
||||||
|
box-shadow: 0 0 12px #22c55e;
|
||||||
|
animation: pulse-green 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-offline {
|
||||||
|
background-color: #ef4444;
|
||||||
|
box-shadow: 0 0 15px #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-green {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- TYPOGRAPHIE ET ELEMENTS VISUELS --- */
|
||||||
|
.data-value {
|
||||||
|
color: #e0f2fe;
|
||||||
|
text-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-box {
|
||||||
|
border: 2px dashed #475569;
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
WIDGET BATTERIE (Code de l'Étudiant 2)
|
||||||
|
========================================= */
|
||||||
|
.battery-circle {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: conic-gradient(#34d399 0deg, #0f172a 0deg);
|
||||||
|
box-shadow: 0 0 15px rgba(52, 211, 153, 0.3), inset 0 0 15px rgba(52, 211, 153, 0.2);
|
||||||
|
transition: background 0.5s ease, box-shadow 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.battery-inner {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1e293b;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bat-percent {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e0f2fe;
|
||||||
|
text-shadow: 0 0 10px rgba(52, 211, 153, 0.5);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================
|
||||||
|
WIDGET SOLAIRE (Code de l'Étudiant 1 adapté et corrigé)
|
||||||
|
========================================= */
|
||||||
|
|
||||||
|
/* CORRECTION ICI : La boîte est maintenant carrée et ne déborde plus */
|
||||||
|
.gauge-container {
|
||||||
|
position: relative;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
margin: 0 auto 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-bg,
|
||||||
|
.gauge-fill {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-bg circle,
|
||||||
|
.gauge-fill circle {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 18;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-bg circle {
|
||||||
|
stroke: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-fill circle {
|
||||||
|
stroke: url(#gaugeGradient);
|
||||||
|
stroke-dasharray: 283;
|
||||||
|
stroke-dashoffset: 283;
|
||||||
|
transition: stroke-dashoffset 1s ease;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CORRECTION ICI : Le texte est parfaitement centré au milieu du cercle */
|
||||||
|
.gauge-value {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value .number {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #facc15;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gauge-value .unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value .stat-unit {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sol-status-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: rgba(34, 197, 94, 0.08);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.15);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sol-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #22c55e;
|
||||||
|
animation: blink-sol 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-sol {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sol-status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #22c55e;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
81
style.css
81
style.css
@@ -1,81 +0,0 @@
|
|||||||
/* --- COULEURS GLOBALES ET THEME SOMBRE --- */
|
|
||||||
body {
|
|
||||||
background-color: #0f172a;
|
|
||||||
color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background-color: #1e293b !important;
|
|
||||||
border-bottom: 1px solid #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- DESIGN DES WIDGETS (CARTES) --- */
|
|
||||||
.card {
|
|
||||||
background-color: #1e293b;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
min-height: 320px;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-meteo {
|
|
||||||
border-top: 4px solid #38bdf8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-solaire {
|
|
||||||
border-top: 4px solid #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-batterie {
|
|
||||||
border-top: 4px solid #34d399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-admin {
|
|
||||||
border-top: 4px solid #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- ANIMATION DU HEARTBEAT --- */
|
|
||||||
.status-dot {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-online {
|
|
||||||
background-color: #22c55e;
|
|
||||||
box-shadow: 0 0 12px #22c55e;
|
|
||||||
animation: pulse-green 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pulse-offline {
|
|
||||||
background-color: #ef4444;
|
|
||||||
box-shadow: 0 0 15px #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse-green {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.95);
|
|
||||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
70% {
|
|
||||||
transform: scale(1);
|
|
||||||
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: scale(0.95);
|
|
||||||
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- TYPOGRAPHIE ET ELEMENTS VISUELS --- */
|
|
||||||
.data-value {
|
|
||||||
color: #e0f2fe;
|
|
||||||
text-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-box {
|
|
||||||
border: 2px dashed #475569;
|
|
||||||
background-color: #0f172a;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
283
templates/admin_dashboard.html
Normal file
283
templates/admin_dashboard.html
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EcoCharge — Dashboard Admin</title>
|
||||||
|
<style>
|
||||||
|
/* ── Variables de couleur ── */
|
||||||
|
:root {
|
||||||
|
--vert: #2ecc71;
|
||||||
|
--bleu: #1a1a2e;
|
||||||
|
--gris: #16213e;
|
||||||
|
--blanc: #f0f4f8;
|
||||||
|
--muted: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reset simple ── */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bleu);
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
color: var(--blanc);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Conteneur principal centré ── */
|
||||||
|
.conteneur {
|
||||||
|
max-width: 640px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── En-tête de la page ── */
|
||||||
|
.entete {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entete-titre {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entete-titre span {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entete-titre h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: var(--vert);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entete-titre p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lien de déconnexion ── */
|
||||||
|
.deconnexion {
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #2c3e50;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deconnexion:hover {
|
||||||
|
color: #e74c3c;
|
||||||
|
border-color: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Carte blanche contenant le formulaire ── */
|
||||||
|
.carte {
|
||||||
|
background-color: var(--gris);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 36px 32px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carte h2 {
|
||||||
|
font-size: 17px;
|
||||||
|
color: var(--blanc);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carte>p {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Séparateur entre les champs ── */
|
||||||
|
.separateur {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #2c3e50;
|
||||||
|
margin: 28px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Chaque groupe label + input + description ── */
|
||||||
|
.champ {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ-entete {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ label {
|
||||||
|
color: var(--blanc);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Petite description sous le label */
|
||||||
|
.champ-info {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ligne input + unité */
|
||||||
|
.input-ligne {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #2c3e50;
|
||||||
|
background-color: #0f3460;
|
||||||
|
color: var(--blanc);
|
||||||
|
font-size: 16px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ input:focus {
|
||||||
|
border-color: var(--vert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unité affichée à droite (%, °C, W) */
|
||||||
|
.unite {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 15px;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Bouton Sauvegarder ── */
|
||||||
|
.bouton {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
background-color: var(--vert);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-top: 8px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouton:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message de confirmation Flask/Jinja ── */
|
||||||
|
.succes {
|
||||||
|
background-color: rgba(46, 204, 113, 0.15);
|
||||||
|
border: 1px solid var(--vert);
|
||||||
|
color: var(--vert);
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="conteneur">
|
||||||
|
|
||||||
|
<!-- ── En-tête avec titre et bouton déconnexion ── -->
|
||||||
|
<div class="entete">
|
||||||
|
<div class="entete-titre">
|
||||||
|
<span>⚡</span>
|
||||||
|
<div>
|
||||||
|
<h1>ECOCHARGE</h1>
|
||||||
|
<p>Panneau d'administration</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Lien vers la route Flask /admin/logout -->
|
||||||
|
<a class="deconnexion" href="/admin/logout">🚪 Déconnexion</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Carte principale ── -->
|
||||||
|
<div class="carte">
|
||||||
|
|
||||||
|
<h2>Seuils d'alerte</h2>
|
||||||
|
<p>Modifiez les valeurs puis cliquez sur "Sauvegarder".</p>
|
||||||
|
|
||||||
|
<!-- Formulaire — action vers la route Flask /admin/dashboard -->
|
||||||
|
<form method="POST" action="/admin/dashboard">
|
||||||
|
|
||||||
|
<!-- ── Champ 1 : batterie minimum ── -->
|
||||||
|
<div class="champ">
|
||||||
|
<div class="champ-entete">
|
||||||
|
<label for="min_battery">🔋 Tension batterie minimum</label>
|
||||||
|
</div>
|
||||||
|
<p class="champ-info">Alerte si la tension de la batterie descend sous ce seuil.</p>
|
||||||
|
<br>
|
||||||
|
<div class="input-ligne">
|
||||||
|
<input type="number" id="min_battery" name="min_battery" value="{{ settings.min_battery }}"
|
||||||
|
min="0" required>
|
||||||
|
<span class="unite">V</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="separateur">
|
||||||
|
|
||||||
|
<!-- ── Champ 2 : température maximum ── -->
|
||||||
|
<div class="champ">
|
||||||
|
<div class="champ-entete">
|
||||||
|
<label for="max_temp">🌡️ Température maximum</label>
|
||||||
|
</div>
|
||||||
|
<p class="champ-info">Alerte si la température dépasse cette valeur.</p>
|
||||||
|
<br>
|
||||||
|
<div class="input-ligne">
|
||||||
|
<input type="number" id="max_temp" name="max_temp" value="{{ settings.max_temp }}" min="0"
|
||||||
|
max="100" required>
|
||||||
|
<span class="unite">°C</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="separateur">
|
||||||
|
|
||||||
|
<!-- ── Champ 3 : puissance minimum ── -->
|
||||||
|
<div class="champ">
|
||||||
|
<div class="champ-entete">
|
||||||
|
<label for="min_power">☀️ Puissance minimum</label>
|
||||||
|
</div>
|
||||||
|
<p class="champ-info">Alerte si la production solaire descend sous ce seuil.</p>
|
||||||
|
<br>
|
||||||
|
<div class="input-ligne">
|
||||||
|
<input type="number" id="min_power" name="min_power" value="{{ settings.min_power }}" min="0"
|
||||||
|
required>
|
||||||
|
<span class="unite">W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="bouton" type="submit">💾 Sauvegarder</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -78,48 +78,104 @@
|
|||||||
|
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-solaire rounded-3">
|
<div class="card card-solaire rounded-3">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #fbbf24;"><i
|
<h5 class="card-title fw-bold mb-4" style="color: #fbbf24;">
|
||||||
class="bi bi-sun me-2"></i>Production Solaire</h5>
|
<i class="bi bi-sun me-2"></i>Production Solaire
|
||||||
<div class="d-flex h-100 align-items-center justify-content-center">
|
</h5>
|
||||||
<div
|
|
||||||
class="placeholder-box text-center p-3 rounded w-100 h-100 d-flex flex-column justify-content-center">
|
<div class="gauge-container mx-auto">
|
||||||
<i class="bi bi-code-slash d-block fs-2 mb-2 opacity-50"></i><span>Espace Étudiant
|
<svg class="gauge-bg" viewBox="0 0 200 200">
|
||||||
1</span>
|
<circle cx="100" cy="100" r="90" />
|
||||||
|
</svg>
|
||||||
|
<svg class="gauge-fill" viewBox="0 0 200 200">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gaugeGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stop-color="#f59e0b" />
|
||||||
|
<stop offset="100%" stop-color="#facc15" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="100" cy="100" r="90" />
|
||||||
|
</svg>
|
||||||
|
<div class="gauge-value">
|
||||||
|
<span class="number" id="powerValue">0</span>
|
||||||
|
<span class="unit">W</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="stats mt-auto">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Tension (Upv)</div>
|
||||||
|
<div class="stat-value" id="voltage">0 <span class="stat-unit">V</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Courant (Ipv)</div>
|
||||||
|
<div class="stat-value" id="current">0 <span class="stat-unit">A</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Luminosité</div>
|
||||||
|
<div class="stat-value" id="lux">0 <span class="stat-unit">Lux</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-label">Rendement</div>
|
||||||
|
<div class="stat-value" id="efficiency">0 <span class="stat-unit">%</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sol-status-bar mt-3">
|
||||||
|
<div class="sol-status-dot"></div>
|
||||||
|
<span class="sol-status-text">Système connecté — MPPT actif</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-batterie rounded-3">
|
<div class="card card-batterie rounded-3">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #34d399;"><i
|
<h5 class="card-title fw-bold mb-4" style="color: #34d399;">
|
||||||
class="bi bi-battery-charging me-2"></i>État Batterie</h5>
|
<i class="bi bi-battery-charging me-2"></i>État Batterie
|
||||||
<div class="d-flex h-100 align-items-center justify-content-center">
|
</h5>
|
||||||
<div
|
|
||||||
class="placeholder-box text-center p-3 rounded w-100 h-100 d-flex flex-column justify-content-center">
|
<div class="d-flex flex-column align-items-center justify-content-center flex-grow-1">
|
||||||
<i class="bi bi-code-slash d-block fs-2 mb-2 opacity-50"></i><span>Espace Étudiant
|
<div class="battery-circle" id="bat-circle">
|
||||||
2</span>
|
<div class="battery-inner">
|
||||||
|
<span class="bat-percent" id="bat-val">--%</span>
|
||||||
|
<small class="text-secondary text-uppercase fw-bold">SoC</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<div class="badge bg-dark border border-secondary text-secondary w-100 py-2"
|
||||||
|
id="bat-alert-badge">
|
||||||
|
<i class="bi bi-check-circle text-success" id="bat-alert-icon"></i> <span
|
||||||
|
id="bat-alert">Système Normal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6 col-lg-3">
|
<div class="col-12 col-md-6 col-lg-3">
|
||||||
<div class="card card-admin rounded-3">
|
<div class="card card-admin rounded-3">
|
||||||
<div class="card-body p-4">
|
<div class="card-body p-4 d-flex flex-column">
|
||||||
<h5 class="card-title fw-bold mb-4" style="color: #f87171;"><i
|
<h5 class="card-title fw-bold mb-4" style="color: #f87171;">
|
||||||
class="bi bi-sliders me-2"></i>Administration</h5>
|
<i class="bi bi-sliders me-2"></i>Administration
|
||||||
<div class="d-flex h-100 align-items-center justify-content-center">
|
</h5>
|
||||||
<div
|
|
||||||
class="placeholder-box text-center p-3 rounded w-100 h-100 d-flex flex-column justify-content-center">
|
<div class="d-flex flex-column h-100 justify-content-center mt-auto gap-3">
|
||||||
<i class="bi bi-code-slash d-block fs-2 mb-2 opacity-50"></i><span>Espace Étudiant
|
<a href="/admin/dashboard"
|
||||||
4</span>
|
class="btn btn-outline-danger w-100 py-3 fw-bold rounded-3 d-flex flex-column align-items-center shadow-sm">
|
||||||
</div>
|
<i class="bi bi-gear-fill display-4 mb-2"></i>
|
||||||
|
<span>Paramètres Système</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/admin/logout" class="btn btn-dark border-secondary w-100 text-secondary mt-2">
|
||||||
|
<i class="bi bi-box-arrow-right me-2"></i>Déconnexion
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +183,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="script.js"></script>
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
174
templates/login.html
Normal file
174
templates/login.html
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>EcoCharge — Connexion</title>
|
||||||
|
<style>
|
||||||
|
/* ── Variables de couleur ── */
|
||||||
|
:root {
|
||||||
|
--vert: #2ecc71;
|
||||||
|
--bleu: #1a1a2e;
|
||||||
|
--gris: #16213e;
|
||||||
|
--blanc: #f0f4f8;
|
||||||
|
--rouge: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reset simple ── */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Fond de page ── */
|
||||||
|
body {
|
||||||
|
background-color: var(--bleu);
|
||||||
|
font-family: 'Segoe UI', sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Carte de connexion ── */
|
||||||
|
.carte {
|
||||||
|
background-color: var(--gris);
|
||||||
|
padding: 48px 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Logo / en-tête ── */
|
||||||
|
.logo {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icone {
|
||||||
|
font-size: 48px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
color: var(--vert);
|
||||||
|
font-size: 26px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo p {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Champs du formulaire ── */
|
||||||
|
.champ {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ label {
|
||||||
|
display: block;
|
||||||
|
color: var(--blanc);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.champ input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #2c3e50;
|
||||||
|
background-color: #0f3460;
|
||||||
|
color: var(--blanc);
|
||||||
|
font-size: 15px;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Met en valeur le champ actif */
|
||||||
|
.champ input:focus {
|
||||||
|
border-color: var(--vert);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Message d'erreur Flask/Jinja ── */
|
||||||
|
.erreur {
|
||||||
|
background-color: rgba(231, 76, 60, 0.15);
|
||||||
|
border: 1px solid var(--rouge);
|
||||||
|
color: var(--rouge);
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Bouton de connexion ── */
|
||||||
|
.bouton {
|
||||||
|
width: 100%;
|
||||||
|
padding: 13px;
|
||||||
|
background-color: var(--vert);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bouton:hover {
|
||||||
|
background-color: #27ae60;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="carte">
|
||||||
|
|
||||||
|
<!-- En-tête avec icône et nom du projet -->
|
||||||
|
<div class="logo">
|
||||||
|
<span class="logo-icone">⚡</span>
|
||||||
|
<h1>ECOCHARGE</h1>
|
||||||
|
<p>Interface d'administration</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message d'erreur affiché si Flask envoie une erreur -->
|
||||||
|
{% if error %}
|
||||||
|
<div class="erreur">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Formulaire de connexion — action vers la route Flask /admin/login -->
|
||||||
|
<form method="POST" action="/admin/login">
|
||||||
|
|
||||||
|
<div class="champ">
|
||||||
|
<label for="username">Identifiant</label>
|
||||||
|
<input type="text" id="username" name="username" placeholder="admin" required autocomplete="username">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="champ">
|
||||||
|
<label for="password">Mot de passe</label>
|
||||||
|
<input type="password" id="password" name="password" placeholder="••••••••" required
|
||||||
|
autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="bouton" type="submit">Se connecter</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user