- Limite de connexion par (e-mail, IP) et par IP : un tiers ne peut plus
bloquer le compte admin depuis une autre adresse ; mémoire bornée.
- Ids canoniques uniquement (« 07 » contournait la protection du propre
compte de l'admin).
- Export : un cadre réactivé entre deux requêtes ne fait plus planter.
- /auth/me lit le compte déjà chargé par requireAuth ; /auth/password et
e-mail non textuel ne renvoient plus 500.
- Planning : une écriture réussie n'est plus perdue quand un rechargement
du même mois répond avant elle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
Sécurité
- JWT : un JWT_SECRET d'exemple ou trop court est ignoré au profit d'un
secret aléatoire conservé en base (avant, la valeur publique de
docker-compose.yml permettait de forger un jeton admin).
- Le compte est relu en base à chaque requête : désactivation, changement
de rôle et réinitialisation du mot de passe prennent effet immédiatement.
- Connexion : 10 échecs max par e-mail / 15 min, temps constant que l'e-mail
existe ou non, mot de passe admin retiré des logs.
- Validation des entrées (ids, dates, mois, longueurs, e-mail) : 400 au
lieu de 500. Un admin ne peut plus se désactiver ni se rétrograder.
- Mots de passe : 8 caractères minimum ; changement en libre-service.
- Export : nom de fichier conforme RFC 5987 (un nom de société avec « — »
faisait planter l'export).
- esc() échappe aussi les guillemets (injection d'attributs HTML).
- En-têtes CSP / X-Frame-Options / nosniff (Nginx + API), API et Postgres
publiés sur 127.0.0.1 seulement, image backend non-root via npm ci.
Optimisations
- Export : une requête pour tout le mois au lieu d'une par cadre.
- Saisie groupée : un seul INSERT (unnest), doublons dédupliqués.
- Dates renvoyées en chaînes (plus de décalage d'un jour selon le TZ).
- Planning : chaque clic met à jour l'affichage localement au lieu de
recharger le mois ; les réponses de mois périmées sont ignorées.
- Recherche Utilisateurs / Plannings filtrée localement (plus une requête
par touche) ; statuts des sociétés chargés en parallèle.
UI/UX
- Mobile : toutes les destinations dans la barre basse (Sociétés et Ma
saisie étaient inaccessibles à l'admin) ; tableaux affichés en cartes
(les boutons d'action étaient hors écran).
- Boutons désactivés pendant l'envoi, focus et Échap dans les dialogues,
retour à l'écran de connexion quand la session est révoquée, erreurs
d'export affichées au lieu d'un fichier JSON téléchargé, actions
destructives signalées, confirmation avant désactivation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
Le filtre société et la recherche sont partagés avec l'écran Plannings :
une valeur restée en place masquait le compte créé, alors que le toast
annonçait « Utilisateur enregistré ». Après une création, on remet les
filtres à zéro pour que le nouveau compte soit visible.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5Bdayziw6tybgqETZoNSt
Symptôme observé en production : après mise à jour, l'application restait
vide et aucune requête /api/ n'était émise. Le navigateur avait rechargé
index.html et les feuilles de style (nouveaux noms de fichiers) mais
servait app.js depuis son cache — la version précédente, qui référence
des identifiants disparus du nouveau balisage. Le script levait
« Cannot read properties of null » à l'évaluation et s'arrêtait avant
d'enregistrer ses gestionnaires et d'appeler boot().
index.html, app.js et les CSS gardant le même nom d'une version à
l'autre, ils sont désormais servis avec Cache-Control: no-cache : le
navigateur peut toujours les mettre en cache mais doit les revalider, ce
qui coûte un 304 quand rien n'a changé. Les polices, stables et
volumineuses, gardent un cache long.
Vérifié en simulant un redéploiement dans un navigateur au cache chaud :
app.js est revalidé et la nouvelle version prise en compte. Suite E2E
toujours à 62/62.
Un échec pendant onLogin (chargement des sociétés) interrompait le rendu
avant la navigation et le tableau de bord : l'utilisateur arrivait sur une
interface vide, et le message d'erreur était écrit dans l'encart du
formulaire de connexion qui venait d'être masqué — donc invisible.
- onLogin isole le chargement des sociétés : la navigation et le tableau
de bord s'affichent toujours, l'échec est signalé par un toast.
- Les erreurs survenant après la bascule vers l'application sont
rapportées en toast et non plus dans l'encart de connexion masqué.
- boot() ne renvoie vers l'écran de connexion que si la session elle-même
est invalide, au lieu d'avaler toute erreur de chargement.
- Le tableau de bord admin tolère un backend plus ancien que la page : un
404 sur /validations/overview affiche « reconstruisez aussi l'image du
backend » et un tableau de bord réduit au lieu de rester vide.
Vérifié : les deux scénarios de panne rendus visibles, et la suite E2E
complète toujours à 62/62.
Reprend la maquette Presencia.dc.html sur l'application réelle : thème
clair chaud à accent vert forêt, navigation latérale groupée par rôle et
tableau de bord, en conservant l'intégralité du fonctionnel existant.
Frontend
- Écran de connexion en deux panneaux (argumentaire + formulaire).
- Tableau de bord par rôle : compteurs du mois et liste « à faire
maintenant » (sociétés prêtes à valider, cadres en retard, saisie à
compléter), plus l'état du mois par société côté admin.
- Planning : saisie rapide au pinceau, trois affichages (grille, compact,
liste), sélecteur avec raccourcis clavier (1–4, ⌫, ↵ pour appliquer à
la journée), barre d'état de verrouillage, « Tout effacer ».
- Nouvelle vue Historique pour les cadres (totaux par mois et statut).
- Validation & export : barre de progression et compteurs de
demi-journées par cadre.
- Mobile : liste verticale, navigation en barre basse, cibles 44 px.
Backend
- GET /api/validations/overview — état de toutes les sociétés d'un mois.
- GET /api/attendance/history — totaux par mois et par statut.
- DELETE /api/attendance/month — effacement d'un mois (mêmes verrous).
- Compteur de demi-journées saisies par cadre dans /validations/company.
Polices et icônes servies localement (Inter, sous-ensemble de 18 icônes
Phosphor) au lieu de Google Fonts et unpkg : l'application ne fait plus
aucune requête externe au chargement.
Vérifié de bout en bout via la vraie configuration nginx : 62 contrôles
Playwright (desktop et mobile), tous passants, sur base réinitialisée et
rejoués deux fois pour garantir le déterminisme.
Vérification de bout en bout via la vraie config nginx (21 checks
Playwright, tous passants) : parcours admin complet, saisie cadre,
double validation, verrouillages, exports, erreurs de connexion.
Bugs corrigés découverts par ce contrôle :
- La création d'utilisateur via l'interface était cassée depuis le
début : l'id "user-role" existait en double (badge de l'en-tête +
select de la modale), getElementById renvoyait le span du header et
le rôle n'était jamais envoyé (400 systématique). Le badge devient
user-role-badge.
- Les toasts de notification interceptaient les clics sur les boutons
situés dessous pendant 4 s (pointer-events: none ajouté).
Améliorations UI :
- « Absent » renommé « Non-présent » partout (légende, popover,
exports PDF et Excel — code NP au lieu de A).
- Récapitulatif mensuel : tuiles de compteurs par statut au-dessus du
calendrier, mises à jour à chaque saisie.
- Bouton « Remplir les jours ouvrés en Présent » : renseigne en un clic
toutes les demi-journées vides du lundi au vendredi (nouvel endpoint
PUT /api/attendance/bulk, transactionnel, mêmes contrôles de
verrouillage), sans toucher aux saisies existantes.
- Jour actuel mis en évidence dans le calendrier.
- Favicon.
Avec un proxy_pass entièrement basé sur une variable, nginx ne remplace
plus le préfixe de location par le reste de l'URI comme il le fait pour
une valeur statique : toutes les requêtes /api/* étaient réécrites en
exactement /api/ (sans /auth/login, /users, etc.), d'où les 404 sur
toutes les routes. Fix : $request_uri est maintenant explicitement
ajouté au proxy_pass pour transmettre le chemin et la query string
d'origine.
Reproduit et vérifié avec un vrai nginx local + un faux backend qui
échoue le chemin reçu : le bug était bien présent avec l'ancienne
config, et absent avec la nouvelle.
- Calendrier : sous 640px, la grille 7 colonnes devient une liste
verticale (une ligne pleine largeur par jour), boutons AM/PM en
cibles tactiles de 44px minimum.
- Popover de statut : positionnement recalculé pour rester dans
l'écran (bascule au-dessus de la cellule et se recale à gauche si
besoin) au lieu de pouvoir déborder hors viewport.
- Tableaux admin (sociétés, utilisateurs, validation) : scroll
horizontal indépendant pour ne pas casser la mise en page sur petit
écran.
- Nav admin et en-tête compactés sur mobile.
Testé avec Playwright en viewport mobile (375x812) : calendrier liste,
popover, connexion et vues admin.
- Renomme les services (db/backend/frontend -> presencia-db/
presencia-backend/presencia-frontend) : sur le réseau externe partagé
nginx_default, un nom générique comme "db" peut entrer en collision
avec un service du même nom dans une autre stack, et Docker résout
alors le nom en round-robin vers le mauvais container. Confirmé en
prod : le bootstrap réussissait puis une requête suivante échouait
avec une erreur d'authentification alors que le mot de passe était
correct.
- nginx résout maintenant le backend dynamiquement (resolver + variable)
au lieu de résoudre une seule fois au démarrage, pour survivre à une
recréation du container backend.
- Ajoute express-async-errors : une erreur dans une route async (ex. la
requête pg qui échoue) était une rejection non gérée et faisait
planter tout le process Node au lieu de renvoyer une 500 propre.
Testé en local : avant le fix, une erreur d'auth pg tuait le process ;
après le fix, il répond 500 et reste opérationnel.
Application complète : API Node/Express + PostgreSQL, frontend HTML/JS
mono-page, et déploiement Docker sur ports non standards (8781/4790/6543).
- Cadres : calendrier mensuel, saisie de présence par demi-journée
(présent/absent/congé/RTT), validation de fin de mois qui verrouille
leurs propres saisies.
- Admin : gestion des sociétés et utilisateurs (rattachement société <->
cadre), consultation de tous les plannings, validation d'une société
entière une fois tous ses cadres validés, réouverture en cas de
correction, export PDF et Excel par société/mois.