Corrige le rendu de la page mot de passe, masque la nav si non connecté,

réduit le score spam des emails, et rend le niveau de log configurable

- Les styles .login-* étaient définis uniquement dans login.html, donc
  set_password.html s'affichait sans aucun style. Déplacé dans base.html.
- La nav affichait les liens même sans session active.
- mail.py ajoute Message-ID/Date et encode en quoted-printable (au lieu
  de base64) pour réduire le score spam (MISSING_MID, MISSING_DATE,
  MIME_BASE64_TEXT).
- log_level devient une directive de config.json plutôt qu'une variable
  d'environnement.
- README: section démarrage rapide (copie de config.example.json,
  compose.override.yml pour les surcharges locales).
This commit is contained in:
Antoine
2026-07-17 22:16:48 +02:00
parent 2d4179169b
commit 0b47e4a625
7 changed files with 104 additions and 84 deletions

View File

@ -6,6 +6,21 @@ Saisie des horaires de travail avec calcul en temps réel de l'heure de sortie o
--- ---
## 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 ## Principe
La semaine cible est **39h** (7h48 / jour). Les directives imposent des plages de présence minimum : La semaine cible est **39h** (7h48 / jour). Les directives imposent des plages de présence minimum :
@ -35,14 +50,13 @@ Les plages qui ne respectent pas les minimums sont signalées en rouge dès la s
## Lancement ## Lancement
```bash ```bash
cp data/config.example.json data/config.json # à adapter à ton domaine
docker compose up -d docker compose up -d
``` ```
Ouvre [http://localhost:8000](http://localhost:8000). Ouvre [http://localhost:8000](http://localhost:8000).
Les données (pointages, congés, comptes) sont stockées dans `data/` et ignorées par git.\ Les données (pointages, congés, comptes) sont stockées dans `data/` et ignorées par git.\
Seul `data/config.example.json` est versionné — copie-le en `data/config.json` et adapte-le. Seul `data/config.example.json` est versionné.
--- ---
@ -53,6 +67,7 @@ Seul `data/config.example.json` est versionné — copie-le en `data/config.json
```json ```json
{ {
"heures_jour": 7.8, "heures_jour": 7.8,
"log_level": "DEBUG",
"allowed_email_domain": "exemple.fr", "allowed_email_domain": "exemple.fr",
"mail_from": "no-reply@exemple.fr", "mail_from": "no-reply@exemple.fr",
"smtp": { "smtp": {
@ -66,6 +81,7 @@ Seul `data/config.example.json` est versionné — copie-le en `data/config.json
``` ```
- `heures_jour``7.8` = 7h48 = 39h ÷ 5 jours. Modifier cette valeur recentre tous les calculs. - `heures_jour``7.8` = 7h48 = 39h ÷ 5 jours. Modifier cette valeur recentre tous les calculs.
- `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. - `allowed_email_domain` — seules les adresses de ce domaine peuvent se connecter.
- `mail_from` — adresse expéditrice des emails de connexion. - `mail_from` — adresse expéditrice des emails de connexion.
- `smtp` — serveur relais utilisé pour l'envoi (host, port, identifiants, TLS). - `smtp` — serveur relais utilisé pour l'envoi (host, port, identifiants, TLS).

View File

@ -1,7 +1,8 @@
"""Mail delivery via a configured SMTP relay (host/port/credentials in config.json).""" """Mail delivery via a configured SMTP relay (host/port/credentials in config.json)."""
import logging import logging
import smtplib import smtplib
from email.mime.text import MIMEText from email.message import EmailMessage
from email.utils import make_msgid, formatdate
logger = logging.getLogger("pointeuse.mail") logger = logging.getLogger("pointeuse.mail")
@ -16,10 +17,13 @@ def send_mail(to_addr: str, subject: str, body: str, from_addr: str, smtp_cfg: d
password = smtp_cfg.get("password") password = smtp_cfg.get("password")
use_tls = smtp_cfg.get("use_tls", True) use_tls = smtp_cfg.get("use_tls", True)
msg = MIMEText(body) msg = EmailMessage()
msg.set_content(body, cte="quoted-printable") # évite MIME_BASE64_TEXT
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = from_addr msg["From"] = from_addr
msg["To"] = to_addr msg["To"] = to_addr
msg["Message-ID"] = make_msgid(domain=from_addr.split("@", 1)[1])
msg["Date"] = formatdate(localtime=True)
logger.info( logger.info(
"envoi mail: to=%r from=%r subject=%r via smtp=%s:%s user=%r tls=%r", "envoi mail: to=%r from=%r subject=%r via smtp=%s:%s user=%r tls=%r",

View File

@ -1,7 +1,6 @@
import asyncio import asyncio
import json import json
import logging import logging
import os
import re import re
import secrets import secrets
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -26,7 +25,7 @@ from classement import compute_classement
from notifications import reminder_loop from notifications import reminder_loop
logging.basicConfig( logging.basicConfig(
level=getattr(logging, os.environ.get("LOG_LEVEL", "DEBUG").upper(), logging.DEBUG), level=getattr(logging, load_config().get("log_level", "DEBUG").upper(), logging.DEBUG),
format="%(asctime)s %(levelname)s %(name)s: %(message)s", format="%(asctime)s %(levelname)s %(name)s: %(message)s",
) )
logger = logging.getLogger("pointeuse") logger = logging.getLogger("pointeuse")

View File

@ -8,6 +8,7 @@ CONFIG_FILE = DATA_DIR / "config.json"
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"heures_jour": 7.8, # 7h48 = 39h/semaine "heures_jour": 7.8, # 7h48 = 39h/semaine
"log_level": "DEBUG", # DEBUG, INFO, WARNING ou ERROR
"allowed_email_domain": "", # ex: "sdv.fr" — seules ces adresses peuvent se connecter "allowed_email_domain": "", # ex: "sdv.fr" — seules ces adresses peuvent se connecter
"mail_from": "", # ex: "no-reply@sdv.fr" — expéditeur des emails de connexion "mail_from": "", # ex: "no-reply@sdv.fr" — expéditeur des emails de connexion
"smtp": { "smtp": {

View File

@ -840,6 +840,82 @@
50% { transform: scale(1.25); } 50% { transform: scale(1.25); }
} }
/* ── Login / set-password ── */
.login-wrap {
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 4rem;
}
.login-card {
background: var(--surface);
border: 1.5px solid var(--rule);
padding: 2.5rem 3rem;
width: 380px;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.login-header { display: flex; flex-direction: column; gap: .35rem; }
.login-eyebrow {
font-family: var(--mono);
font-size: 10px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
}
.login-title {
font-family: var(--mono);
font-size: 22px;
font-weight: 500;
color: var(--text);
letter-spacing: -.01em;
}
.login-error {
background: var(--neg-bg);
border-left: 3px solid var(--neg);
color: var(--neg);
font-size: 12px;
padding: .6rem .9rem;
font-family: var(--mono);
}
.login-form { display: flex; flex-direction: column; gap: .75rem; }
.login-label {
font-size: 11px;
font-weight: 500;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--muted);
}
.login-input {
font-family: var(--mono);
font-size: 15px;
padding: .6rem .75rem;
border: 1.5px solid var(--rule);
background: var(--ground);
color: var(--text);
outline: none;
transition: border-color .15s;
}
.login-input:focus { border-color: var(--accent); }
.login-hint { font-size: 11px; color: var(--muted); }
.login-btn {
margin-top: .5rem;
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
padding: .7rem 1.5rem;
background: var(--text);
color: var(--surface);
border: none;
cursor: pointer;
transition: background .15s;
}
.login-btn:hover { background: var(--accent); }
/* ── Chart zoom modal ── */ /* ── Chart zoom modal ── */
.chart-modal { .chart-modal {
position: fixed; position: fixed;
@ -928,11 +1004,11 @@
<nav> <nav>
<a href="/" class="nav-brand"><span class="nav-dot"></span>Décompte Horaire</a> <a href="/" class="nav-brand"><span class="nav-dot"></span>Décompte Horaire</a>
<span class="spacer"></span> <span class="spacer"></span>
{% if current_user is defined and current_user %}
<a href="/" class="nav-link">Semaine courante</a> <a href="/" class="nav-link">Semaine courante</a>
<a href="/stats" class="nav-link">Statistiques</a> <a href="/stats" class="nav-link">Statistiques</a>
<a href="/classement" class="nav-link">Classement</a> <a href="/classement" class="nav-link">Classement</a>
<a href="/settings" class="nav-link">Réglages</a> <a href="/settings" class="nav-link">Réglages</a>
{% if current_user is defined and current_user %}
<span class="nav-user">{{ current_user }}</span> <span class="nav-user">{{ current_user }}</span>
<a href="/logout" class="nav-link nav-logout">Déconnexion</a> <a href="/logout" class="nav-link nav-logout">Déconnexion</a>
{% endif %} {% endif %}

View File

@ -53,81 +53,4 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<style>
.login-wrap {
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 4rem;
}
.login-card {
background: var(--surface);
border: 1.5px solid var(--rule);
padding: 2.5rem 3rem;
width: 380px;
max-width: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.login-header { display: flex; flex-direction: column; gap: .35rem; }
.login-eyebrow {
font-family: var(--mono);
font-size: 10px;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
}
.login-title {
font-family: var(--mono);
font-size: 22px;
font-weight: 500;
color: var(--text);
letter-spacing: -.01em;
}
.login-error {
background: var(--neg-bg);
border-left: 3px solid var(--neg);
color: var(--neg);
font-size: 12px;
padding: .6rem .9rem;
font-family: var(--mono);
}
.login-form { display: flex; flex-direction: column; gap: .75rem; }
.login-label {
font-size: 11px;
font-weight: 500;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--muted);
}
.login-input {
font-family: var(--mono);
font-size: 15px;
padding: .6rem .75rem;
border: 1.5px solid var(--rule);
background: var(--ground);
color: var(--text);
outline: none;
transition: border-color .15s;
}
.login-input:focus { border-color: var(--accent); }
.login-hint { font-size: 11px; color: var(--muted); }
.login-btn {
margin-top: .5rem;
font-family: var(--mono);
font-size: 13px;
font-weight: 500;
letter-spacing: .08em;
text-transform: uppercase;
padding: .7rem 1.5rem;
background: var(--text);
color: var(--surface);
border: none;
cursor: pointer;
transition: background .15s;
}
.login-btn:hover { background: var(--accent); }
</style>
{% endblock %} {% endblock %}

View File

@ -1,5 +1,6 @@
{ {
"heures_jour": 7.8, "heures_jour": 7.8,
"log_level": "DEBUG",
"allowed_email_domain": "exemple.fr", "allowed_email_domain": "exemple.fr",
"mail_from": "no-reply@exemple.fr", "mail_from": "no-reply@exemple.fr",
"smtp": { "smtp": {