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
216 lines
9.2 KiB
Markdown
216 lines
9.2 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 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`
|