Compare commits
10 Commits
2fdcf50bc1
...
bbb5fdec3a
| Author | SHA1 | Date | |
|---|---|---|---|
| bbb5fdec3a | |||
| 761c7e76ed | |||
| ea8bdc1808 | |||
| 1722dd9c7f | |||
| 8cad794c83 | |||
| a5490d8875 | |||
| 1413fe3c9c | |||
| 97427acc7a | |||
| 3603ad5e61 | |||
| f8b8c0bf67 |
4
.gitignore
vendored
@ -48,3 +48,7 @@ config/db_credentials.txt
|
|||||||
*.bak_*
|
*.bak_*
|
||||||
*.backup
|
*.backup
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
# Evaluations générées (reconstruites depuis evaluations_transit)
|
||||||
|
evaluations/*
|
||||||
|
!evaluations/.gitkeep
|
||||||
155
deploy_prod.sh
Executable file
@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Deploy PROD webval (Jetson)
|
||||||
|
# Repo -> /var/www/mathematiques
|
||||||
|
# Backup + exclusions + rollback
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
APP_NAME="webval"
|
||||||
|
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DST_DIR="/var/www/mathematiques"
|
||||||
|
|
||||||
|
# Dossier backups (hors web)
|
||||||
|
BACKUP_ROOT="/var/backups/${APP_NAME}"
|
||||||
|
TS="$(date +%Y%m%d_%H%M%S)"
|
||||||
|
BACKUP_DIR="${BACKUP_ROOT}/prev_${TS}"
|
||||||
|
|
||||||
|
# URL de check (adapte si besoin)
|
||||||
|
HEALTH_URL="http://127.0.0.1/"
|
||||||
|
HEALTH_TIMEOUT=10
|
||||||
|
|
||||||
|
# Exclusions: runtime + config sensible + git
|
||||||
|
RSYNC_EXCLUDES=(
|
||||||
|
".git/"
|
||||||
|
"evaluations/"
|
||||||
|
"evaluations_transit/"
|
||||||
|
"uploads/"
|
||||||
|
"logs/"
|
||||||
|
"temp/"
|
||||||
|
"backup/"
|
||||||
|
"backups/"
|
||||||
|
"config/database.php"
|
||||||
|
"config/secrets.php"
|
||||||
|
"config/db_credentials.txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- helpers ---
|
||||||
|
die() { echo "ERREUR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
print_excludes() {
|
||||||
|
for e in "${RSYNC_EXCLUDES[@]}"; do
|
||||||
|
echo " - $e"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
rsync_exclude_args() {
|
||||||
|
for e in "${RSYNC_EXCLUDES[@]}"; do
|
||||||
|
printf -- "--exclude=%q " "$e"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || die "Commande manquante: $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
healthcheck() {
|
||||||
|
# Healthcheck simple: HTTP 200/301/302 attendu sur la home
|
||||||
|
# (Si ton app a une URL spécifique de health, mets-la dans HEALTH_URL)
|
||||||
|
if curl -fsS -m "${HEALTH_TIMEOUT}" -o /dev/null -I "${HEALTH_URL}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
rollback() {
|
||||||
|
echo
|
||||||
|
echo "=== ROLLBACK ==="
|
||||||
|
echo "Restauration depuis: ${BACKUP_DIR}"
|
||||||
|
if [[ ! -d "${BACKUP_DIR}" ]]; then
|
||||||
|
die "Backup introuvable, rollback impossible: ${BACKUP_DIR}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo rsync -a --delete "${BACKUP_DIR}/" "${DST_DIR}/"
|
||||||
|
|
||||||
|
sudo systemctl reload php8.1-fpm >/dev/null 2>&1 || true
|
||||||
|
sudo systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "Rollback terminé."
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- prérequis ---
|
||||||
|
need_cmd rsync
|
||||||
|
need_cmd curl
|
||||||
|
need_cmd date
|
||||||
|
|
||||||
|
echo "=== Deploy PROD (${APP_NAME}) ==="
|
||||||
|
echo "Source: ${SRC_DIR}"
|
||||||
|
echo "Dest : ${DST_DIR}"
|
||||||
|
echo "Backup: ${BACKUP_DIR}"
|
||||||
|
echo
|
||||||
|
echo "Exclusions:"
|
||||||
|
print_excludes
|
||||||
|
echo
|
||||||
|
|
||||||
|
[[ -d "${SRC_DIR}/.git" ]] || die "Pas de .git dans ${SRC_DIR} (pas un repo ?)."
|
||||||
|
[[ -d "${DST_DIR}" ]] || die "Destination inexistante: ${DST_DIR}"
|
||||||
|
|
||||||
|
# Si tu veux empêcher un deploy avec un working tree sale:
|
||||||
|
if ! git -C "${SRC_DIR}" diff --quiet || ! git -C "${SRC_DIR}" diff --cached --quiet; then
|
||||||
|
echo "AVERTISSEMENT: ton repo a des modifications non commit."
|
||||||
|
echo "Tu peux quand même déployer, mais c'est risqué."
|
||||||
|
read -r -p "Continuer malgré tout ? (oui/non) " ans_dirty
|
||||||
|
[[ "${ans_dirty}" == "oui" ]] || exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- aperçu ---
|
||||||
|
echo "--- Aperçu (dry-run) ---"
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
sudo rsync -a --delete --dry-run \
|
||||||
|
$(rsync_exclude_args) \
|
||||||
|
"${SRC_DIR}/" "${DST_DIR}/" | sed -n '1,200p'
|
||||||
|
echo "(aperçu tronqué à 200 lignes)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -r -p "Lancer le backup + déploiement maintenant ? (oui/non) " ans
|
||||||
|
[[ "${ans}" == "oui" ]] || { echo "Annulé."; exit 0; }
|
||||||
|
|
||||||
|
# --- backup ---
|
||||||
|
echo
|
||||||
|
echo "--- Backup PROD -> ${BACKUP_DIR} ---"
|
||||||
|
sudo mkdir -p "${BACKUP_ROOT}"
|
||||||
|
sudo rsync -a --delete "${DST_DIR}/" "${BACKUP_DIR}/"
|
||||||
|
|
||||||
|
# --- deploy ---
|
||||||
|
echo
|
||||||
|
echo "--- Déploiement ---"
|
||||||
|
# shellcheck disable=SC2046
|
||||||
|
sudo rsync -a --delete \
|
||||||
|
$(rsync_exclude_args) \
|
||||||
|
"${SRC_DIR}/" "${DST_DIR}/"
|
||||||
|
|
||||||
|
# --- reload ---
|
||||||
|
echo
|
||||||
|
echo "--- Reload services ---"
|
||||||
|
sudo systemctl reload php8.1-fpm >/dev/null 2>&1 || true
|
||||||
|
sudo systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# --- healthcheck ---
|
||||||
|
echo
|
||||||
|
echo "--- Healthcheck: ${HEALTH_URL} ---"
|
||||||
|
if healthcheck; then
|
||||||
|
echo "OK: deploy réussi."
|
||||||
|
echo "Backup conservé: ${BACKUP_DIR}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "ECHEC: healthcheck KO. On rollback."
|
||||||
|
rollback
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Après rollback, je te conseille de regarder :"
|
||||||
|
echo " - sudo tail -n 80 /var/log/nginx/error.log"
|
||||||
|
echo " - sudo journalctl -u php8.1-fpm -n 80 --no-pager"
|
||||||
|
exit 2
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Dashboard élève (classe fixe + libre)
|
* Dashboard élève - VERSION FINALE CORRECTE
|
||||||
* Fichier : eleve/dashboard.php
|
* Basé sur la structure BDD réelle analysée le 02/01/2026
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../config/config.php';
|
require_once __DIR__ . '/../config/config.php';
|
||||||
@ -27,9 +27,8 @@ $db = Database::getInstance();
|
|||||||
$isEleveLibre = ($user['id_type'] == 3);
|
$isEleveLibre = ($user['id_type'] == 3);
|
||||||
$isEleveClasse = ($user['id_type'] == 2);
|
$isEleveClasse = ($user['id_type'] == 2);
|
||||||
|
|
||||||
// Récupérer les évaluations disponibles
|
// Récupérer les évaluations disponibles (non encore passées)
|
||||||
if ($isEleveLibre) {
|
if ($isEleveLibre) {
|
||||||
// Élève libre : évaluations de type soutien uniquement
|
|
||||||
$queryEvals = "SELECT DISTINCT e.*,
|
$queryEvals = "SELECT DISTINCT e.*,
|
||||||
COALESCE(t.statut, 'non_commence') as statut_tentative,
|
COALESCE(t.statut, 'non_commence') as statut_tentative,
|
||||||
t.note as note_obtenue,
|
t.note as note_obtenue,
|
||||||
@ -48,7 +47,6 @@ if ($isEleveLibre) {
|
|||||||
$evaluations = $db->fetchAll($queryEvals, [$user['id_utilisateur']]);
|
$evaluations = $db->fetchAll($queryEvals, [$user['id_utilisateur']]);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Élève classe fixe : évaluations de sa classe
|
|
||||||
if ($user['id_classe']) {
|
if ($user['id_classe']) {
|
||||||
$queryEvals = "SELECT DISTINCT e.*,
|
$queryEvals = "SELECT DISTINCT e.*,
|
||||||
ae.date_debut as debut_acces,
|
ae.date_debut as debut_acces,
|
||||||
@ -72,17 +70,62 @@ if ($isEleveLibre) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les statistiques de l'élève
|
// Récupérer les 4 dernières évaluations terminées
|
||||||
|
$query4Dernieres = "SELECT
|
||||||
|
e.id_evaluation,
|
||||||
|
e.titre,
|
||||||
|
e.description,
|
||||||
|
te.note,
|
||||||
|
te.note_sur,
|
||||||
|
te.pourcentage,
|
||||||
|
te.temps_passe,
|
||||||
|
te.date_fin,
|
||||||
|
(SELECT COUNT(*) FROM questions WHERE id_evaluation = e.id_evaluation) as nb_questions
|
||||||
|
FROM tentatives_eleves te
|
||||||
|
INNER JOIN evaluations e ON te.id_evaluation = e.id_evaluation
|
||||||
|
WHERE te.id_eleve = ?
|
||||||
|
AND te.statut = 'terminee'
|
||||||
|
ORDER BY te.date_fin DESC
|
||||||
|
LIMIT 4";
|
||||||
|
|
||||||
|
$dernieresEvals = $db->fetchAll($query4Dernieres, [$user['id_utilisateur']]);
|
||||||
|
|
||||||
|
// Calculer la moyenne des 4 dernières (arrondie au demi-point supérieur)
|
||||||
|
$moyenne4Dernieres = 0;
|
||||||
|
if (!empty($dernieresEvals)) {
|
||||||
|
$somme_notes_sur_20 = 0;
|
||||||
|
foreach ($dernieresEvals as $eval) {
|
||||||
|
$note_sur_20 = $eval['nb_questions'] > 0 ? ($eval['note'] / $eval['nb_questions']) * 20 : 0;
|
||||||
|
$somme_notes_sur_20 += $note_sur_20;
|
||||||
|
}
|
||||||
|
$moyenne_brute = $somme_notes_sur_20 / count($dernieresEvals);
|
||||||
|
$moyenne4Dernieres = ceil($moyenne_brute * 2) / 2; // Arrondi demi-point sup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer TOUTES les évaluations terminées pour le graphique
|
||||||
|
$queryToutesEvals = "SELECT
|
||||||
|
e.id_evaluation,
|
||||||
|
e.titre,
|
||||||
|
e.description,
|
||||||
|
te.note,
|
||||||
|
te.pourcentage,
|
||||||
|
te.date_fin
|
||||||
|
FROM tentatives_eleves te
|
||||||
|
INNER JOIN evaluations e ON te.id_evaluation = e.id_evaluation
|
||||||
|
WHERE te.id_eleve = ?
|
||||||
|
AND te.statut = 'terminee'
|
||||||
|
ORDER BY te.date_fin ASC";
|
||||||
|
|
||||||
|
$toutesEvals = $db->fetchAll($queryToutesEvals, [$user['id_utilisateur']]);
|
||||||
|
|
||||||
|
// Récupérer les statistiques générales
|
||||||
$queryStats = "SELECT
|
$queryStats = "SELECT
|
||||||
COUNT(DISTINCT t.id_evaluation) as nb_evaluations_passees,
|
COUNT(DISTINCT t.id_evaluation) as nb_evaluations_passees,
|
||||||
COUNT(CASE WHEN t.statut = 'terminee' THEN 1 END) as nb_evaluations_terminees,
|
COUNT(CASE WHEN t.statut = 'terminee' THEN 1 END) as nb_evaluations_terminees,
|
||||||
AVG(
|
AVG(CASE WHEN t.statut = 'terminee'
|
||||||
CASE WHEN t.statut = 'terminee'
|
THEN (t.note / (SELECT COUNT(*) FROM questions WHERE id_evaluation = t.id_evaluation)) * 20
|
||||||
THEN CEILING((t.note / e.note_totale * 20) * 2) / 2
|
ELSE NULL END) as moyenne_generale
|
||||||
ELSE NULL END
|
|
||||||
) as moyenne_generale
|
|
||||||
FROM tentatives_eleves t
|
FROM tentatives_eleves t
|
||||||
INNER JOIN evaluations e ON t.id_evaluation = e.id_evaluation
|
|
||||||
WHERE t.id_eleve = ?";
|
WHERE t.id_eleve = ?";
|
||||||
|
|
||||||
$stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
$stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
||||||
@ -91,6 +134,11 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
'moyenne_generale' => null
|
'moyenne_generale' => null
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Arrondir la moyenne générale au demi-point supérieur
|
||||||
|
if ($stats['moyenne_generale'] !== null) {
|
||||||
|
$stats['moyenne_generale'] = ceil($stats['moyenne_generale'] * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
@ -98,6 +146,7 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dashboard Élève - <?= htmlspecialchars($user['prenom'] . ' ' . $user['nom']) ?></title>
|
<title>Dashboard Élève - <?= htmlspecialchars($user['prenom'] . ' ' . $user['nom']) ?></title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -119,7 +168,7 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.header-content {
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -161,14 +210,14 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1400px;
|
||||||
margin: 30px auto;
|
margin: 30px auto;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
@ -195,6 +244,130 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-card .sub-value {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Historique 4 dernières évaluations */
|
||||||
|
.historique-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dernieres-evals-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-card-small {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-card-small:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 15px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-card-small h4 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-card-small .description {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
height: 32px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-display-large {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-fraction {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-sur-20 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-card-small .date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #999;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyenne-4 {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyenne-4 h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moyenne-4 .value {
|
||||||
|
font-size: 42px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Graphique progression */
|
||||||
|
.graph-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
position: relative;
|
||||||
|
height: 300px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Évaluations disponibles */
|
||||||
.section {
|
.section {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -203,14 +376,6 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 22px;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-bottom: 2px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eval-grid {
|
.eval-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 15px;
|
gap: 15px;
|
||||||
@ -347,7 +512,7 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- STATISTIQUES -->
|
<!-- STATISTIQUES GLOBALES -->
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<h3>📚 Évaluations passées</h3>
|
<h3>📚 Évaluations passées</h3>
|
||||||
@ -363,9 +528,14 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
<h3>📊 Moyenne générale</h3>
|
<h3>📊 Moyenne générale</h3>
|
||||||
<div class="value">
|
<div class="value">
|
||||||
<?= $stats['moyenne_generale'] !== null
|
<?= $stats['moyenne_generale'] !== null
|
||||||
? number_format($stats['moyenne_generale'], 2) . '/20'
|
? number_format($stats['moyenne_generale'], 1) . '/20'
|
||||||
: '-' ?>
|
: '-' ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
|
||||||
|
<div class="sub-value">
|
||||||
|
Sur <?= count($toutesEvals) ?> évaluation<?= count($toutesEvals) > 1 ? 's' : '' ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -378,6 +548,74 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- HISTORIQUE 4 DERNIÈRES ÉVALUATIONS -->
|
||||||
|
<?php if (!empty($dernieresEvals)): ?>
|
||||||
|
<div class="historique-section">
|
||||||
|
<h2 class="section-title">📋 Mes 4 dernières évaluations</h2>
|
||||||
|
|
||||||
|
<div class="dernieres-evals-grid">
|
||||||
|
<?php foreach ($dernieresEvals as $eval):
|
||||||
|
// Convertir la note brute en note sur 20
|
||||||
|
$note_sur_20 = $eval['nb_questions'] > 0 ? ($eval['note'] / $eval['nb_questions']) * 20 : 0;
|
||||||
|
|
||||||
|
// Arrondir au demi-point supérieur
|
||||||
|
$note_arrondie = ceil($note_sur_20 * 2) / 2;
|
||||||
|
|
||||||
|
// Le nombre de questions réussies = la note brute elle-même !
|
||||||
|
$questions_reussies = $eval['note'];
|
||||||
|
?>
|
||||||
|
<div class="eval-card-small">
|
||||||
|
<h4 title="<?= htmlspecialchars($eval['titre']) ?>">
|
||||||
|
<?= htmlspecialchars($eval['titre']) ?>
|
||||||
|
</h4>
|
||||||
|
<div class="description">
|
||||||
|
<?= htmlspecialchars($eval['description'] ?? '') ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="note-display-large">
|
||||||
|
<div>
|
||||||
|
<div class="note-fraction">
|
||||||
|
<?= number_format($questions_reussies, 1) ?>/<?= $eval['nb_questions'] ?> questions
|
||||||
|
</div>
|
||||||
|
<div class="note-sur-20">
|
||||||
|
<?= number_format($note_arrondie, 1) ?>/20
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<div class="note-fraction">
|
||||||
|
<?= round($eval['pourcentage']) ?>%
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 11px; color: #999; margin-top: 3px;">
|
||||||
|
⏱️ <?= gmdate('i:s', $eval['temps_passe'] ?? 0) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="date">
|
||||||
|
<?= date('d/m/Y à H:i', strtotime($eval['date_fin'])) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<!-- Moyenne des 4 dernières -->
|
||||||
|
<div class="moyenne-4">
|
||||||
|
<h3>Moyenne sur ces 4 évaluations</h3>
|
||||||
|
<div class="value"><?= number_format($moyenne4Dernieres, 1) ?>/20</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- GRAPHIQUE PROGRESSION -->
|
||||||
|
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
|
||||||
|
<div class="graph-section">
|
||||||
|
<h2 class="section-title">📈 Ma progression</h2>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="progressionChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- ÉVALUATIONS DISPONIBLES -->
|
<!-- ÉVALUATIONS DISPONIBLES -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2 class="section-title">📝 Évaluations disponibles</h2>
|
<h2 class="section-title">📝 Évaluations disponibles</h2>
|
||||||
@ -399,8 +637,7 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
<p><?= htmlspecialchars($eval['description']) ?></p>
|
<p><?= htmlspecialchars($eval['description']) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<p>
|
<p>
|
||||||
<strong>Chapitre :</strong> <?= htmlspecialchars($eval['chapitre'] ?? 'Non spécifié') ?>
|
<strong>Durée :</strong> <?= $eval['duree_minutes'] ?? 'Libre' ?> min
|
||||||
| <strong>Durée :</strong> <?= $eval['duree_minutes'] ?? 'Libre' ?> min
|
|
||||||
| <strong>Note :</strong> /<?= $eval['note_totale'] ?? 20 ?>
|
| <strong>Note :</strong> /<?= $eval['note_totale'] ?? 20 ?>
|
||||||
</p>
|
</p>
|
||||||
<?php if (isset($eval['fin_acces'])): ?>
|
<?php if (isset($eval['fin_acces'])): ?>
|
||||||
@ -416,11 +653,21 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if ($statut === 'terminee'): ?>
|
<?php if ($statut === 'terminee'): ?>
|
||||||
|
<?php
|
||||||
|
// Récupérer le nombre de questions pour cette évaluation
|
||||||
|
$queryNbQ = "SELECT COUNT(*) as nb FROM questions WHERE id_evaluation = ?";
|
||||||
|
$resultNbQ = $db->fetchOne($queryNbQ, [$eval['id_evaluation']]);
|
||||||
|
$nb_q = $resultNbQ['nb'] ?? 1;
|
||||||
|
|
||||||
|
// Calculer la note sur 20
|
||||||
|
$note_sur_20 = ($eval['note_obtenue'] / $nb_q) * 20;
|
||||||
|
$note_affichee = ceil($note_sur_20 * 2) / 2;
|
||||||
|
?>
|
||||||
<span class="note-display">
|
<span class="note-display">
|
||||||
<?= number_format($eval['note_obtenue'], 2) ?>/<?= $eval['note_totale'] ?? 20 ?>
|
<?= number_format($note_affichee, 1) ?>/20
|
||||||
</span>
|
</span>
|
||||||
<span class="status-badge status-termine">✅ Terminé</span>
|
<span class="status-badge status-termine">✅ Terminé</span>
|
||||||
<a href="../resultats.php?id_evaluation=<?= $eval['id_evaluation'] ?>"
|
<a href="../voir_resultat.php?id_evaluation=<?= $eval['id_evaluation'] ?>"
|
||||||
class="btn btn-secondary">
|
class="btn btn-secondary">
|
||||||
Voir le résultat
|
Voir le résultat
|
||||||
</a>
|
</a>
|
||||||
@ -446,5 +693,160 @@ $stats = $db->fetchOne($queryStats, [$user['id_utilisateur']]) ?? [
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($toutesEvals) && count($toutesEvals) > 1): ?>
|
||||||
|
<script>
|
||||||
|
// Données pour le graphique
|
||||||
|
const toutesEvaluations = <?= json_encode($toutesEvals) ?>;
|
||||||
|
|
||||||
|
// Préparer les labels
|
||||||
|
const labels = toutesEvaluations.map((e, index) => {
|
||||||
|
const date = new Date(e.date_fin);
|
||||||
|
return `Eval ${index + 1}\n${date.getDate()}/${date.getMonth() + 1}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer le nombre de questions pour chaque évaluation (via AJAX)
|
||||||
|
// Pour simplifier, on utilise le pourcentage pour recalculer
|
||||||
|
const notes = toutesEvaluations.map(e => {
|
||||||
|
// note brute → note sur 20 via le pourcentage
|
||||||
|
// pourcentage = (note / nb_questions) * 100
|
||||||
|
// donc note_sur_20 = (pourcentage / 100) * 20 = pourcentage / 5
|
||||||
|
const note_sur_20 = parseFloat(e.pourcentage) / 5;
|
||||||
|
return Math.ceil(note_sur_20 * 2) / 2; // Arrondir demi-point sup
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculer la moyenne mobile (sur 3 évaluations)
|
||||||
|
const moyennesMobiles = notes.map((note, index, arr) => {
|
||||||
|
if (index < 2) return null;
|
||||||
|
const sum = arr[index - 2] + arr[index - 1] + arr[index];
|
||||||
|
const avg = sum / 3;
|
||||||
|
return Math.ceil(avg * 2) / 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Créer le graphique
|
||||||
|
const ctx = document.getElementById('progressionChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Note (/20)',
|
||||||
|
data: notes,
|
||||||
|
borderColor: '#667eea',
|
||||||
|
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
||||||
|
borderWidth: 3,
|
||||||
|
pointRadius: 6,
|
||||||
|
pointBackgroundColor: '#667eea',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
pointHoverRadius: 8,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Moyenne mobile (3 évals)',
|
||||||
|
data: moyennesMobiles,
|
||||||
|
borderColor: '#28a745',
|
||||||
|
backgroundColor: 'rgba(40, 167, 69, 0.05)',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderDash: [5, 5],
|
||||||
|
pointRadius: 4,
|
||||||
|
pointBackgroundColor: '#28a745',
|
||||||
|
pointBorderColor: '#fff',
|
||||||
|
pointBorderWidth: 2,
|
||||||
|
tension: 0.3,
|
||||||
|
fill: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: true,
|
||||||
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
padding: 15,
|
||||||
|
font: {
|
||||||
|
size: 12,
|
||||||
|
weight: '600'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
padding: 12,
|
||||||
|
titleFont: {
|
||||||
|
size: 13,
|
||||||
|
weight: 'bold'
|
||||||
|
},
|
||||||
|
bodyFont: {
|
||||||
|
size: 12
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
title: function(context) {
|
||||||
|
const index = context[0].dataIndex;
|
||||||
|
return toutesEvaluations[index].titre;
|
||||||
|
},
|
||||||
|
label: function(context) {
|
||||||
|
const index = context.dataIndex;
|
||||||
|
const eval = toutesEvaluations[index];
|
||||||
|
if (context.datasetIndex === 0) {
|
||||||
|
return `Note: ${context.parsed.y.toFixed(1)}/20 (${Math.round(eval.pourcentage)}%)`;
|
||||||
|
} else {
|
||||||
|
return context.parsed.y ? `Moyenne mobile: ${context.parsed.y.toFixed(1)}/20` : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
afterLabel: function(context) {
|
||||||
|
if (context.datasetIndex === 0) {
|
||||||
|
const index = context.dataIndex;
|
||||||
|
const eval = toutesEvaluations[index];
|
||||||
|
const date = new Date(eval.date_fin);
|
||||||
|
return `Date: ${date.toLocaleDateString('fr-FR')}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 20,
|
||||||
|
ticks: {
|
||||||
|
stepSize: 2,
|
||||||
|
callback: function(value) {
|
||||||
|
return value + '/20';
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.05)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
interaction: {
|
||||||
|
intersect: false,
|
||||||
|
mode: 'index'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* MATRICE MONITORING TEMPS RÉEL - ENSEIGNANT
|
* MATRICE MONITORING TEMPS RÉEL - VERSION AMÉLIORÉE
|
||||||
* Supervision élèves pendant passage évaluation
|
* Affichage matriciel des questions pour éviter débordement horizontal
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once '../config/config.php';
|
require_once '../config/config.php';
|
||||||
@ -42,6 +42,10 @@ try {
|
|||||||
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$questions = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
$nb_questions = count($questions);
|
$nb_questions = count($questions);
|
||||||
|
|
||||||
|
// Calculer dimensions matrice optimales (proche du carré)
|
||||||
|
$cols = ceil(sqrt($nb_questions));
|
||||||
|
$rows = ceil($nb_questions / $cols);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
die('Erreur: ' . $e->getMessage());
|
die('Erreur: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
@ -63,20 +67,20 @@ try {
|
|||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 20px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1400px;
|
max-width: 1600px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px 30px;
|
padding: 12px 20px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -84,23 +88,23 @@ try {
|
|||||||
|
|
||||||
.header h1 {
|
.header h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.refresh-indicator {
|
|
||||||
padding: 8px 16px;
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refresh-indicator {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.refresh-indicator.active {
|
.refresh-indicator.active {
|
||||||
background: #d4edda;
|
background: #d4edda;
|
||||||
color: #155724;
|
color: #155724;
|
||||||
@ -108,13 +112,13 @@ try {
|
|||||||
|
|
||||||
.stats-bar {
|
.stats-bar {
|
||||||
background: white;
|
background: white;
|
||||||
padding: 15px 30px;
|
padding: 10px 20px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 12px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||||
gap: 20px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
@ -122,90 +126,88 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 32px;
|
font-size: 22px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #667eea;
|
color: #667eea;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 14px;
|
font-size: 10px;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-top: 5px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.monitoring-table {
|
/* Cards élèves - VERSION COMPACTE */
|
||||||
|
.eleves-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eleve-card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
overflow: hidden;
|
padding: 12px;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
.eleve-card:hover {
|
||||||
width: 100%;
|
transform: translateY(-3px);
|
||||||
border-collapse: collapse;
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
thead {
|
.eleve-header {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
display: flex;
|
||||||
color: white;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
.eleve-info {
|
||||||
padding: 15px 10px;
|
flex: 1;
|
||||||
text-align: left;
|
min-width: 0; /* Pour ellipsis */
|
||||||
|
}
|
||||||
|
|
||||||
|
.eleve-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eleve-classe {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th.question-col {
|
.status-en-ligne { background: #d4edda; color: #155724; }
|
||||||
text-align: center;
|
.status-en-cours { background: #fff3cd; color: #856404; }
|
||||||
min-width: 40px;
|
.status-inactif { background: #fff3cd; color: #856404; }
|
||||||
}
|
.status-hors-ligne { background: #f8d7da; color: #721c24; }
|
||||||
|
.status-termine { background: #e2e3e5; color: #383d41; }
|
||||||
|
|
||||||
tbody tr {
|
.progress-section {
|
||||||
border-bottom: 1px solid #f0f0f0;
|
margin-bottom: 8px;
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody tr:hover {
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody td {
|
|
||||||
padding: 12px 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-en-ligne { background: #28a745; }
|
|
||||||
.status-en-cours { background: #ffc107; }
|
|
||||||
.status-inactif { background: #fd7e14; }
|
|
||||||
.status-hors-ligne { background: #dc3545; }
|
|
||||||
.status-termine { background: #6c757d; }
|
|
||||||
|
|
||||||
.question-cell {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.question-cell:hover {
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar-container {
|
.progress-bar-container {
|
||||||
background: #e9ecef;
|
background: #e9ecef;
|
||||||
border-radius: 10px;
|
border-radius: 6px;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -213,73 +215,117 @@ try {
|
|||||||
.progress-bar {
|
.progress-bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
|
background: linear-gradient(90deg, #28a745 0%, #20c997 100%);
|
||||||
transition: width 0.3s ease;
|
transition: width 0.5s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 11px;
|
font-size: 9px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-text {
|
.progress-text {
|
||||||
font-size: 12px;
|
font-size: 9px;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
text-align: center;
|
||||||
|
|
||||||
.eleve-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eleve-classe {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #666;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-info {
|
.time-info {
|
||||||
font-size: 12px;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-info span {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Matrice questions - VERSION ULTRA COMPACTE */
|
||||||
|
.questions-matrix {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(<?= $cols ?>, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-cell {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s;
|
||||||
|
background: #f8f9fa;
|
||||||
|
position: relative;
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-cell:hover {
|
||||||
|
transform: scale(1.3);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.25);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-cell.repondu {
|
||||||
|
background: #cce5ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-cell.correct {
|
||||||
|
background: #d4edda;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-cell.incorrect {
|
||||||
|
background: #f8d7da;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-number {
|
||||||
|
position: absolute;
|
||||||
|
top: 1px;
|
||||||
|
left: 2px;
|
||||||
|
font-size: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-legend {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 11px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legende {
|
.matrix-legend-item {
|
||||||
background: white;
|
|
||||||
padding: 15px 30px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
gap: 30px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legende-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legende-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 5px;
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-retour {
|
.btn-retour {
|
||||||
background: #6c757d;
|
background: white;
|
||||||
color: white;
|
color: #667eea;
|
||||||
padding: 10px 20px;
|
border: 2px solid #667eea;
|
||||||
border-radius: 5px;
|
padding: 6px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 14px;
|
font-weight: 600;
|
||||||
transition: background 0.2s;
|
font-size: 11px;
|
||||||
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-retour:hover {
|
.btn-retour:hover {
|
||||||
background: #5a6268;
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@ -292,9 +338,51 @@ try {
|
|||||||
|
|
||||||
.no-data {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 60px 20px;
|
||||||
color: #666;
|
color: #666;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Légende globale */
|
||||||
|
.legende {
|
||||||
|
background: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legende-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legende-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -303,10 +391,13 @@ try {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>
|
<h1>
|
||||||
📊 Monitoring Temps Réel
|
📊 Monitoring
|
||||||
<span style="font-size: 18px; font-weight: normal; color: #666;">
|
<span style="font-size: 14px; font-weight: normal; color: #666;">
|
||||||
- <?= htmlspecialchars($evaluation['titre']) ?>
|
- <?= htmlspecialchars($evaluation['titre']) ?>
|
||||||
</span>
|
</span>
|
||||||
|
<span style="font-size: 11px; font-weight: normal; color: #999;">
|
||||||
|
(<?= $nb_questions ?>Q - <?= $rows ?>×<?= $cols ?>)
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div style="display: flex; gap: 15px; align-items: center;">
|
<div style="display: flex; gap: 15px; align-items: center;">
|
||||||
<div class="refresh-indicator" id="refreshIndicator">
|
<div class="refresh-indicator" id="refreshIndicator">
|
||||||
@ -341,68 +432,51 @@ try {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tableau monitoring -->
|
<!-- Grille élèves -->
|
||||||
<div class="monitoring-table">
|
<div class="eleves-grid" id="elevesGrid">
|
||||||
<table>
|
<div class="no-data">
|
||||||
<thead>
|
🔄 Chargement des données...
|
||||||
<tr>
|
</div>
|
||||||
<th style="width: 40px;">Statut</th>
|
|
||||||
<th style="width: 200px;">Élève</th>
|
|
||||||
<th style="width: 200px;">Progression</th>
|
|
||||||
<th style="width: 120px;">Temps écoulé</th>
|
|
||||||
<th style="width: 120px;">Dernière activité</th>
|
|
||||||
<?php foreach ($questions as $q): ?>
|
|
||||||
<th class="question-col" title="<?= htmlspecialchars($q['enonce']) ?>">
|
|
||||||
Q<?= $q['ordre'] ?>
|
|
||||||
</th>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="monitoringBody">
|
|
||||||
<tr>
|
|
||||||
<td colspan="<?= 5 + $nb_questions ?>" class="no-data">
|
|
||||||
🔄 Chargement des données...
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Légende -->
|
<!-- Légende -->
|
||||||
<div class="legende">
|
<div class="legende">
|
||||||
<div class="legende-title">Légende :</div>
|
<div class="legende-title">Légende :</div>
|
||||||
<div class="legende-item">
|
<div class="legende-item">
|
||||||
<span class="status-indicator status-en-ligne"></span>
|
<span style="display: inline-block; width: 14px; height: 14px; background: #f8f9fa; border-radius: 2px;"></span>
|
||||||
<span>En ligne (<1 min)</span>
|
<span>Non répondu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legende-item">
|
<div class="legende-item">
|
||||||
<span class="status-indicator status-en-cours"></span>
|
<span style="display: inline-block; width: 14px; height: 14px; background: #cce5ff; border-radius: 2px;"></span>
|
||||||
<span>En cours (<3 min)</span>
|
<span>✅ Répondu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legende-item">
|
<div class="legende-item">
|
||||||
<span class="status-indicator status-inactif"></span>
|
<span style="display: inline-block; width: 14px; height: 14px; background: #d4edda; border-radius: 2px;"></span>
|
||||||
<span>Inactif (3-5 min)</span>
|
<span>✔️ Correct</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legende-item">
|
<div class="legende-item">
|
||||||
<span class="status-indicator status-hors-ligne"></span>
|
<span style="display: inline-block; width: 14px; height: 14px; background: #f8d7da; border-radius: 2px;"></span>
|
||||||
<span>Hors ligne (>5 min)</span>
|
<span>❌ Incorrect</span>
|
||||||
|
</div>
|
||||||
|
<div class="legende-item" style="margin-left: 15px;">
|
||||||
|
<span>🟢 En ligne (<1min)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="legende-item">
|
<div class="legende-item">
|
||||||
<span class="status-indicator status-termine"></span>
|
<span>🟡 En cours (<3min)</span>
|
||||||
<span>Terminé</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="legende-item" style="margin-left: 30px;">
|
<div class="legende-item">
|
||||||
<span>⬜ Vide</span>
|
<span>🔴 Inactif (>3min)</span>
|
||||||
<span style="margin-left: 15px;">✅ Répondu</span>
|
|
||||||
<span style="margin-left: 15px;">✔️ Correct</span>
|
|
||||||
<span style="margin-left: 15px;">❌ Incorrect</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tooltip -->
|
||||||
|
<div class="tooltip" id="tooltip"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ID_EVALUATION = <?= $id_evaluation ?>;
|
const ID_EVALUATION = <?= $id_evaluation ?>;
|
||||||
const NB_QUESTIONS = <?= $nb_questions ?>;
|
const NB_QUESTIONS = <?= $nb_questions ?>;
|
||||||
|
const MATRIX_COLS = <?= $cols ?>;
|
||||||
let refreshInterval;
|
let refreshInterval;
|
||||||
|
|
||||||
// Charger données initiales
|
// Charger données initiales
|
||||||
@ -426,7 +500,7 @@ try {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
updateStats(data.stats);
|
updateStats(data.stats);
|
||||||
updateTable(data.tentatives);
|
updateElevesGrid(data.tentatives);
|
||||||
|
|
||||||
// Animation succès
|
// Animation succès
|
||||||
refreshIcon.textContent = '✅';
|
refreshIcon.textContent = '✅';
|
||||||
@ -460,50 +534,111 @@ try {
|
|||||||
document.getElementById('statTermines').textContent = stats.termine;
|
document.getElementById('statTermines').textContent = stats.termine;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTable(tentatives) {
|
function updateElevesGrid(tentatives) {
|
||||||
const tbody = document.getElementById('monitoringBody');
|
const grid = document.getElementById('elevesGrid');
|
||||||
|
|
||||||
if (tentatives.length === 0) {
|
if (tentatives.length === 0) {
|
||||||
tbody.innerHTML = `
|
grid.innerHTML = `
|
||||||
<tr>
|
<div class="no-data">
|
||||||
<td colspan="${5 + NB_QUESTIONS}" class="no-data">
|
📭 Aucun élève n'a encore commencé cette évaluation
|
||||||
📭 Aucun élève n'a encore commencé cette évaluation
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = tentatives.map(t => `
|
grid.innerHTML = tentatives.map(t => createEleveCard(t)).join('');
|
||||||
<tr>
|
|
||||||
<td>
|
// Ajouter tooltips sur cellules questions
|
||||||
<span class="status-indicator status-${t.statut_connexion}"></span>
|
addQuestionTooltips();
|
||||||
</td>
|
}
|
||||||
<td>
|
|
||||||
<div class="eleve-name">${escapeHtml(t.nom)} ${escapeHtml(t.prenom)}</div>
|
function createEleveCard(tentative) {
|
||||||
<span class="eleve-classe">${escapeHtml(t.classe || 'Libre')}</span>
|
const statusLabels = {
|
||||||
</td>
|
'en-ligne': '🟢 En ligne',
|
||||||
<td>
|
'en-cours': '🟡 En cours',
|
||||||
|
'inactif': '🟠 Inactif',
|
||||||
|
'hors-ligne': '🔴 Hors ligne',
|
||||||
|
'termine': '⚫ Terminé'
|
||||||
|
};
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="eleve-card">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="eleve-header">
|
||||||
|
<div class="eleve-info">
|
||||||
|
<div class="eleve-name">${escapeHtml(tentative.nom)} ${escapeHtml(tentative.prenom)}</div>
|
||||||
|
<div class="eleve-classe">${escapeHtml(tentative.classe || 'Libre')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-badge status-${tentative.statut_connexion}">
|
||||||
|
${statusLabels[tentative.statut_connexion] || tentative.statut_connexion}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progression -->
|
||||||
|
<div class="progress-section">
|
||||||
<div class="progress-bar-container">
|
<div class="progress-bar-container">
|
||||||
<div class="progress-bar" style="width: ${t.pourcentage_progression}%">
|
<div class="progress-bar" style="width: ${tentative.pourcentage_progression}%">
|
||||||
${t.pourcentage_progression}%
|
${tentative.pourcentage_progression}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="progress-text">${t.nb_reponses}/${NB_QUESTIONS} questions</div>
|
<div class="progress-text">${tentative.nb_reponses}/${NB_QUESTIONS} questions répondues</div>
|
||||||
</td>
|
</div>
|
||||||
<td class="time-info">
|
|
||||||
⏱️ ${t.temps_ecoule}
|
<!-- Temps -->
|
||||||
</td>
|
<div class="time-info">
|
||||||
<td class="time-info">
|
<span>⏱️ Temps écoulé: ${tentative.temps_ecoule}</span>
|
||||||
${t.derniere_activite}
|
<span>🕐 Activité: ${tentative.derniere_activite}</span>
|
||||||
</td>
|
</div>
|
||||||
${t.reponses_details.map(r => `
|
|
||||||
<td class="question-cell" title="${r.tooltip}">
|
<!-- Matrice questions -->
|
||||||
${r.icone}
|
<div class="questions-matrix">
|
||||||
</td>
|
${createQuestionsMatrix(tentative.reponses_details)}
|
||||||
`).join('')}
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createQuestionsMatrix(reponses) {
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < NB_QUESTIONS; i++) {
|
||||||
|
const reponse = reponses[i] || {};
|
||||||
|
const classe = reponse.classe || '';
|
||||||
|
const icone = reponse.icone || '⬜';
|
||||||
|
const tooltip = reponse.tooltip || `Question ${i + 1}`;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="question-cell ${classe}"
|
||||||
|
data-question="${i + 1}"
|
||||||
|
data-tooltip="${escapeHtml(tooltip)}">
|
||||||
|
<span class="question-number">${i + 1}</span>
|
||||||
|
<span>${icone}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQuestionTooltips() {
|
||||||
|
const tooltip = document.getElementById('tooltip');
|
||||||
|
const cells = document.querySelectorAll('.question-cell');
|
||||||
|
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.addEventListener('mouseenter', (e) => {
|
||||||
|
const tooltipText = cell.dataset.tooltip;
|
||||||
|
tooltip.textContent = tooltipText;
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
|
||||||
|
const rect = cell.getBoundingClientRect();
|
||||||
|
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
|
||||||
|
tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
cell.addEventListener('mouseleave', () => {
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
|
|||||||
0
evaluations/.gitkeep
Normal file
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 380 KiB |
|
Before Width: | Height: | Size: 438 KiB |
|
Before Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 331 KiB |
|
Before Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 453 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
34
rollback_last.sh
Executable file
@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP_NAME="webval"
|
||||||
|
DST_DIR="/var/www/mathematiques"
|
||||||
|
BACKUP_ROOT="/var/backups/${APP_NAME}"
|
||||||
|
|
||||||
|
die() { echo "ERREUR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
[[ -d "${DST_DIR}" ]] || die "Destination inexistante: ${DST_DIR}"
|
||||||
|
[[ -d "${BACKUP_ROOT}" ]] || die "Aucun backup trouvé: ${BACKUP_ROOT}"
|
||||||
|
|
||||||
|
# Trouver le backup le plus récent (ordre lexicographique OK grâce au timestamp)
|
||||||
|
LAST_BACKUP="$(ls -1d "${BACKUP_ROOT}"/prev_* 2>/dev/null | sort | tail -n 1 || true)"
|
||||||
|
[[ -n "${LAST_BACKUP}" && -d "${LAST_BACKUP}" ]] || die "Aucun dossier prev_* valide dans ${BACKUP_ROOT}"
|
||||||
|
|
||||||
|
echo "=== ROLLBACK MANUEL (${APP_NAME}) ==="
|
||||||
|
echo "Restore depuis : ${LAST_BACKUP}"
|
||||||
|
echo "Vers : ${DST_DIR}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -r -p "Confirmer le rollback ? (oui/non) " ans
|
||||||
|
[[ "${ans}" == "oui" ]] || { echo "Annulé."; exit 0; }
|
||||||
|
|
||||||
|
sudo rsync -a --delete "${LAST_BACKUP}/" "${DST_DIR}/"
|
||||||
|
|
||||||
|
# Reload services (sans faire échouer si un service n'existe pas)
|
||||||
|
sudo systemctl reload php8.1-fpm >/dev/null 2>&1 || true
|
||||||
|
sudo systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
echo "OK: rollback terminé."
|
||||||
|
echo "Logs utiles si besoin :"
|
||||||
|
echo " sudo tail -n 80 /var/log/nginx/error.log"
|
||||||
|
echo " sudo journalctl -u php8.1-fpm -n 80 --no-pager"
|
||||||