Files
pointeuse-optimisator/README.md
Jacquin Antoine 065d73e15c Ajoute les astreintes : saisie de la fin de dernière activité, retour au bureau décalé de 11h de repos
Un nouveau bouton AS marque un jour d'astreinte. Ce jour et son lendemain,
une saisie « Fin activité » enregistre l'heure de fin de la dernière
intervention hors horaires (elle peut être le matin si la nuit a dépassé
minuit). L'app en déduit l'heure de retour au bureau (fin + 11h de repos
consécutif) et retire du décompte de la semaine toute la présence avant
cette heure : heures dues réduites, présence pointée avant le retour non
comptée, préconisations Fin cible démarrées à l'heure de retour sans
avertissement de retard, demi-journée entièrement en repos considérée
comme remplie. Les exports .ods gagnent les colonnes Astreinte et Retour,
et les stats n'ont plus les retards liés au repos.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-09-08 22:05:10 +02:00

9.2 KiB

pointeuse-optimisator

Saisie des horaires de travail avec calcul en temps réel de l'heure de sortie optimale pour finir la semaine à exactement 0h d'heures supplémentaires.

Interface principale


Démarrage rapide

Avant le premier lancement, copie et adapte le fichier de config (voir Configuration plus bas) :

cp data/config.example.json data/config.json

Pour surcharger compose.yml localement (ports, variables d'env, volumes...) sans modifier le fichier versionné, crée un compose.override.yml — il est chargé automatiquement par docker compose et déjà ignoré par git.


Principe

La semaine cible est 39h (7h48 / jour). Les directives imposent des plages de présence minimum :

Demi-journée Plage minimale
Matin 09:00 → 12:00
Après-midi 14:00 → 17:00

Ces horaires sont réglables par utilisateur depuis la page Réglages de l'app (voir Configuration).

Le matin n'est jamais touché : sa durée nominale est fixe, et si elle ne rentre pas avant la pause déjeuner, le surplus est reporté sur l'après-midi du même jour. Quand un écart hebdomadaire apparaît, il est réparti uniquement sur les heures de départ du soir des demi-journées restantes (répartition égale, recalculée à chaque fois qu'un départ atteint sa limite) :

  • un retard repousse les départs — d'abord dans la marge de confort depart_vise, puis au-delà si besoin (jusqu'à minuit) ;
  • une avance avance les départs, mais jamais en dessous du minimum de présence de l'après-midi.

Si le rythme normal suffit déjà à rentrer dans les clous, aucune préconisation n'est affichée.


Ce que tu vois dans le tableau

Colonne Ce qu'elle dit
Travaillé Heures effectivement saisies ce jour
Δ jour Écart par rapport aux 7h48 dus (affiché dès qu'une heure est enregistrée)
Δ sem. Solde cumulé depuis lundi — visible uniquement quand la journée est complète et sans jour manquant
Fin cible Préconisation de sortie M (matin) et/ou A (après-midi) pour cette demi-journée — vide si le rythme normal suffit

Les plages qui ne respectent pas les minimums sont signalées en rouge dès la saisie.

Le bandeau au-dessus du tableau ajoute, quand il reste du retard à rattraper, une ligne Reste à faire : le total encore dû, le nombre de demi-journées restantes, et la part supplémentaire moyenne à ajouter sur chacune.


Pages

Page Contenu
Semaine courante (/) Le tableau de saisie ci-dessus, pour la semaine en cours ou une autre semaine choisie via le calendrier.
Statistiques (/stats) Conformité aux plages obligatoires, ponctualité moyenne par demi-journée, distribution des horaires, graphique des soldes hebdomadaires.
Journaux (/logs) Historique par utilisateur : changements de réglages, notifications ntfy envoyées, pointages de présence reçus.
Réglages (/settings) Plages horaires personnelles, notifications ntfy, URLs des webhooks de présence — voir Configuration et Présence & notifications.
Aide (/aide) Guide de configuration de ntfy et de l'app de présence (Automation/Tasker).
Export Boutons ⇩ Exporter (semaine, dans l'en-tête) et ⇩ {année} (dans l'historique) : téléchargent un fichier .ods.

Comptes & connexion

Chaque personne a son propre compte, ses propres pointages, congés, réglages et jetons de présence — tout est stocké séparément dans data/users/<identifiant>/.

Connexion par email + mot de passe, restreinte au domaine allowed_email_domain (voir Configuration). Pour un compte qui n'a pas encore de mot de passe, se connecter avec son adresse envoie un lien (valable 24h) pour en définir un — c'est aussi le seul moyen d'en obtenir un : il n'y a pas de "mot de passe oublié" en libre-service une fois un mot de passe défini. La session est un cookie signé (HMAC), sans base de données de sessions côté serveur.


Lancement

docker compose up -d

Ouvre http://localhost:8000.

Les données (pointages, congés, comptes) sont stockées dans data/ et ignorées par git.
Seul data/config.example.json est versionné.


Configuration

data/config.json (voir data/config.example.json) :

{
  "heures_semaine": 39.0,
  "jours_par_semaine": 5,
  "log_level": "DEBUG",
  "allowed_email_domain": "exemple.fr",
  "base_url": "https://pointeuse.exemple.fr",
  "mail_from": "no-reply@exemple.fr",
  "smtp": {
    "host": "smtp.exemple.fr",
    "port": 587,
    "user": "no-reply@exemple.fr",
    "password": "change-moi",
    "use_tls": true
  }
}
  • heures_semaine — heures hebdomadaires visées (ex: 39.0).
  • jours_par_semaine — nombre de jours travaillés dans la semaine (ex: 5). Le quota quotidien est calculé : heures_semaine ÷ jours_par_semaine (39h / 5 = 7h48/jour).
  • log_level — DEBUG, INFO, WARNING ou ERROR. Contrôle la verbosité des logs (docker logs).
  • allowed_email_domain — seules les adresses de ce domaine peuvent se connecter.
  • base_url — schéma + domaine (ex: https://pointeuse.exemple.fr, sans / final) utilisés pour construire les liens envoyés par email (magic link) et les URLs Tasker/Automation affichées dans Réglages. Si vide, ces URLs sont dérivées de la requête entrante (Host header) quand celui-ci correspond à localhost ou à allowed_email_domain ; sinon l'envoi d'email est refusé par sécurité (anti open-redirect). Toujours renseigner base_url en production.
  • mail_from — adresse expéditrice des emails de connexion.
  • smtp — serveur relais utilisé pour l'envoi (host, port, identifiants, TLS).
  • force_secure_cookies (optionnel, false par défaut) — force le flag Secure sur le cookie de session même si la requête entrante n'est pas vue comme HTTPS par l'app (utile derrière un reverse proxy qui termine le TLS).

Les rappels ntfy (topic, serveur, plage horaire) et les plages horaires (bornes de présence obligatoire matin/après-midi, voir Principe, et heure de reprise par défaut de l'après-midi) se règlent par utilisateur depuis la page Réglages de l'app, pas dans config.json. Modifier ses plages change, pour ce compte uniquement, les avertissements de saisie, les stats de conformité et les préconisations de rattrapage.


Présence & notifications

La page Réglages fournit, par utilisateur, deux URLs à jeton unique (/presence/{token}/arrivee et /presence/{token}/depart) — sans autre authentification, la sécurité vient de l'imprévisibilité du jeton. Une app comme Automation (alternative libre à Tasker) les appelle depuis le téléphone en entrant/sortant d'une zone (wifi, géofence...) pour signaler l'arrivée ou le départ.

Une fois configuré, l'app peut envoyer un rappel ntfy si quelqu'un est détecté présent sans avoir pointé son arrivée, ou parti sans avoir pointé son départ, uniquement pendant les jours/heures de travail. Guide complet sur la page Aide.


Tests

docker compose run --rm pointeuse pytest -v

Congés

Trois boutons par ligne : MA (matin), AM (après-midi), J (journée entière).
Ils réduisent les heures dues et recalculent les préconisations Fin cible pour les demi-journées restantes.


Astreinte & repos de 11h

Le bouton AS marque un jour d'astreinte. Ce jour et son lendemain (la nuit qui suit), une ligne de saisie supplémentaire apparaît sous l'après-midi :

  • Fin activité : l'heure de fin de la dernière activité hors horaires de travail. Elle peut tomber le matin (intervention ayant dépassé minuit) : saisie sur le jour d'astreinte, une fin matinale (ex: 06:00) désigne la fin de sa nuit ; saisie sur le lendemain, une fin matinale désigne la nuit de la veille.

L'app en déduit l'heure de retour au bureau : fin + 11h de repos consécutif (exigées par période de 24h), affichée sous le champ (« retour ≥ 13:00 »). Fin 23:30 → retour 10:30 le lendemain ; fin 02:00 → retour 13:00 ; fin 06:00 → retour 17:00. Un repos qui se termine avant l'ouverture ne contraint rien.

À partir de cette heure de retour, tout ce qui la précède sur la journée quitte le décompte de la semaine :

  • la présence obligatoire avant l'heure de retour est retirée des heures dues (ex: retour 13:00 → la plage 09:00-12:00 ne compte plus) ;
  • une présence pointée avant l'heure de retour ne compte pas dans le travaillé ;
  • les préconisations Fin cible démarrent à l'heure de retour (arrivée du matin décalée d'autant, sans avertissement de retard) ;
  • une demi-journée entièrement couverte par le repos est considérée comme remplie.

La fin d'activité ne s'applique que les jours qualifiés (astreinte ou lendemain) : partout ailleurs elle est ignorée. Les colonnes Astreinte (fin saisie) et Retour (calculé) apparaissent dans les exports .ods.


Stack

FastAPI · HTMX · Jinja2 · fichiers JSON · Docker