- La gestion des astreintes devient un réglage par compte (Réglages →
« Gestion des astreintes », actif par défaut) : désactivée, le bouton AS,
les interventions, forfaits, repos de 11h, récapitulatif, ratios, stats
et exports n'en parlent plus — sans rien supprimer, réactiver retrouve tout
- L'heure de retour au bureau tombe désormais toujours sur un quart d'heure
d'horloge fixe (:00, :15, :30, :45), arrondie au supérieur après les 11h
de repos (ex. fin comptée 23:54 → retour 11:00)
- Corrigé : enregistrer ses plages horaires écrasait le forfait d'astreinte
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
274 lines
13 KiB
Markdown
274 lines
13 KiB
Markdown
# 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**.
|
||
|
||

|
||
|
||
---
|
||
|
||
## Démarrage rapide
|
||
|
||
Avant le premier lancement, copie et adapte le fichier de config (voir
|
||
[Configuration](#configuration) plus bas) :
|
||
|
||
```bash
|
||
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](#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.
|
||
|
||
**Enregistrement automatique** : chaque heure validée (sortie du champ, Entrée ou
|
||
sélecteur) enregistre le jour ; un témoin « ✓ enregistré » s'affiche un instant
|
||
dans la colonne Jour. Plus de bouton à cliquer.
|
||
|
||
Quand la semaine affichée contient le jour courant, un **panneau Aujourd'hui**
|
||
s'invite en tête de page : heure de sortie cible en grand et barre de progression
|
||
travaillé/dû de la semaine, actualisés à chaque 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](#configuration) et [Présence & notifications](#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](#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
|
||
|
||
```bash
|
||
docker compose up -d
|
||
```
|
||
|
||
Ouvre [http://localhost:8000](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`) :
|
||
|
||
```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](#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](https://ntfy.sh) 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
|
||
|
||
```bash
|
||
docker compose run --rm pointeuse pytest -v
|
||
```
|
||
|
||
---
|
||
|
||
## Congés & jours fériés
|
||
|
||
Chaque ligne porte un bouton d'état unique (colonne **État**) qui résume la
|
||
situation par des pastilles colorées et ouvre un menu : **MA** (congé matin), **AM**
|
||
(congé après-midi), **J** (journée entière) et **F** (jour férié). Les trois premiers réduisent les heures
|
||
dues et recalculent les préconisations **Fin cible** pour les demi-journées
|
||
restantes. **F** marque un jour férié (fête locale non calculée, pont...) : heures
|
||
dues nulles, aucune présence obligatoire, ligne grisée avec badge « férié » — sans
|
||
consommer de congé. Un jour marqué **F** compte aussi comme férié pour les ratios
|
||
d'astreinte (×2 en journée par défaut) et pour la fenêtre d'astreinte ci-dessous.
|
||
|
||
---
|
||
|
||
## Astreinte & repos de 11h
|
||
|
||
La gestion des astreintes est une **préférence personnelle** (Réglages →
|
||
« Gestion des astreintes », active par défaut). Désactivée, tout ce qui suit
|
||
disparaît pour ce compte : bouton **AS**, saisie d'interventions, forfaits,
|
||
repos de 11h, récapitulatif et ratios — sans rien supprimer, une réactivation
|
||
retrouve les données telles quelles.
|
||
|
||
Le bouton **AS** du menu d'état marque un jour d'astreinte : la période couverte va de **18:00
|
||
ce jour-là à 18:00 le lendemain** (deux astreintes consécutives se recollent sans
|
||
trou) — sauf **samedi, dimanche et jour férié**, qui comptent de **0h00 à 0h00**
|
||
(journée entière ; un jour AS déclaré sam/dim/férié garde sa borne du lendemain
|
||
18:00 pour ne pas tronquer les interventions nocturnes). Ce jour et son lendemain, une sous-ligne dédiée apparaît sous la ligne du
|
||
jour, pleine largeur : un menu dépliant fermé par défaut (seuls l'heure de retour
|
||
et le total restent visibles). Déplié, il offre autant de lignes que nécessaire
|
||
(**+ intervention**) sur trois colonnes : **début**, **fin** et description
|
||
libre. Une fin antérieure au début (fin < début) désigne une intervention à
|
||
cheval sur minuit. Une durée forfaitaire peut être attribuée à chaque jour
|
||
d'astreinte déclaré (page **Réglages**, « Forfait par jour d'astreinte ») :
|
||
elle ne compte jamais dans les heures travaillées, elle est suivie à part dans
|
||
le récapitulatif d'astreinte.
|
||
|
||
La semaine affiche aussi **samedi et dimanche** (heures dues nulles, lignes
|
||
grisées) : on peut y déclarer des astreintes, y saisir les interventions et les
|
||
pointer si besoin — les exports `.ods` suivent.
|
||
|
||
L'app en déduit l'heure de retour au bureau : **fin du dernier événement + 11h de
|
||
repos consécutif** (exigées par période de 24h), affichée sous la liste (« retour
|
||
≥ 13:00 »). Comme pour le décompte, la durée réelle de l'intervention est arrondie
|
||
au quart d'heure supérieur (début 21:00, fin 23:37 → durée comptée 2h45) ; les
|
||
interventions hors fenêtre 18:00→18:00 ne sont pas de l'astreinte. **L'heure de
|
||
retour tombe toujours sur un quart d'heure d'horloge fixe** (:00, :15, :30 ou :45),
|
||
arrondie au quart supérieur : fin 23:30 → retour 10:30 le lendemain ; 22:39 →
|
||
23:49 (fin comptée 23:54) → retour 11:00 ; fin 02:00 → retour 13:00 le jour même.
|
||
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.
|
||
|
||
Les heures d'astreinte ne comptent **jamais** dans les heures de semaine : elles
|
||
sont décomptées à part — la **durée réelle** de chaque intervention est **arrondie
|
||
au quart d'heure supérieur** (2, 5 ou 12 min réelles → 15 min ; 22 ou 28 → 30 min),
|
||
même si l'arrondi ne tombe pas juste sur un quart d'heure de l'horloge (:00, :15,
|
||
:30, :45), et les minutes réelles communes à deux interventions qui se suivent ou
|
||
se chevauchent ne sont comptées **qu'une fois** (attribuées à la première, les
|
||
lignes d'événements somment au total du jour) — dans des compteurs pondérés selon
|
||
le jour et l'heure. Sous le bandeau de la semaine s'affiche le détail par
|
||
événement, les totaux par compteur et le total pondéré.
|
||
|
||
Par défaut : **Nuit** (20:00→08:00, tous types de jours) ×1,5 ; **Journée** (semaine
|
||
Lun-Ven, 08:00→20:00) ×1 ; **Samedi** (08:00→20:00) ×1,5 ; **Dimanche / férié**
|
||
(08:00→20:00) ×2. Les jours fériés français (fixes + Pâques, Ascension,
|
||
Pentecôte) sont calculés automatiquement, et s'ajoutent les jours marqués **F**.
|
||
|
||
Ces règles sont **éditables à la main** sur la page **Réglages** (section « Ratios
|
||
des heures d'astreinte ») : nom, types de jours, fenêtre horaire et ratio de chaque
|
||
règle, ajout et suppression de lignes. La première règle qui couvre une minute
|
||
gagne — place les fenêtres spécifiques avant les générales. Elles restent aussi
|
||
ajustables dans `data/config.json`, clé `astreinte_facteurs` : chaque règle porte
|
||
un `nom`, les `jours` concernés (`semaine`, `sam`, `dim`, `ferie`), un `debut`,
|
||
une `fin` et un `facteur`. Une minute qui ne correspond à aucune règle est rangée
|
||
dans « Autre » avec un facteur ×1.
|
||
|
||
Les événements ne s'appliquent que les jours qualifiés (astreinte ou lendemain) :
|
||
partout ailleurs ils sont ignorés. Les colonnes **Astreinte** (brut), **Astreinte
|
||
pondérée** et **Retour** (calculé) apparaissent dans les exports `.ods`.
|
||
|
||
---
|
||
|
||
## Stack
|
||
|
||
`FastAPI` · `HTMX` · `Jinja2` · fichiers JSON · `Docker`
|