Un jour d'astreinte déclaré couvre désormais 18:00 → 18:00 le lendemain :
les interventions hors fenêtre ne comptent ni au décompte ni pour les
11h de repos, et deux astreintes consécutives se recollent sans trou.
La fin de la dernière intervention est arrondie au quart d'heure entamé
avant de poser le retour. Samedi et dimanche apparaissent dans la
semaine (heures dues nulles) pour pouvoir y déclarer des astreintes,
dans les exports aussi. Un réglage par utilisateur ajoute une durée
forfaitaire au temps travaillé de chaque jour d'astreinte déclaré.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
243 lines
11 KiB
Markdown
243 lines
11 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.
|
||
|
||
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
|
||
|
||
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 : la période couverte va de **18:00
|
||
ce jour-là à 18:00 le lendemain** (deux astreintes consécutives se recollent sans
|
||
trou). 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 ajoutée au temps travaillé de
|
||
chaque jour d'astreinte déclaré (page **Réglages**, « Forfait par jour
|
||
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 »). Tout quart d'heure entamé est travaillé : la fin est arrondie au quart
|
||
d'heure supérieur (fin 23:37 → comptée 23:45) ; les interventions hors fenêtre
|
||
18:00→18:00 ne sont pas de l'astreinte. Fin 23:30 → retour 10:30 le lendemain ;
|
||
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, par pas de 15 minutes (toute tranche entamée compte en
|
||
totalité), 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. Ces ratios sont affichés sur la page **Réglages**. Les jours
|
||
fériés français (fixes + Pâques, Ascension, Pentecôte)
|
||
sont calculés automatiquement.
|
||
|
||
Ces règles sont 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 tranche 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`
|