Le bloc astreinte quitte la colonne après-midi (qui retrouvait une
hauteur normale quel que soit le contenu) pour une sous-ligne dédiée
sur toute la largeur du tableau, collée à la carte du jour sur mobile.
La sous-ligne se masque automatiquement quand le jour n'est pas
qualifié, y compris après les rafraîchissements HTMX.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
232 lines
10 KiB
Markdown
232 lines
10 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. Ce jour et son lendemain (la nuit qui
|
||
suit), 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.
|
||
|
||
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 »). 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. 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`
|