From b47bf61386ef0030726e086d490cb2910cd70f27 Mon Sep 17 00:00:00 2001
From: Bruno Charest
Date: Fri, 14 Aug 2026 23:37:23 -0400
Subject: [PATCH] =?UTF-8?q?feat(web):=20=C3=A9diteur=20de=20p=C3=A9riodes?=
=?UTF-8?q?=20modifiables=20dans=20les=20r=C3=A9glages?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
La page Réglages permet maintenant de créer, modifier et supprimer ses propres périodes (profils) : nom, travail, pause courte, pause longue et nombre de cycles, avec bouton radio ★ pour choisir la période active. Enregistrement via profiles_json + active_profile (index). Repli automatique sur la première période si l'active est supprimée ; noms vides/doublons ignorés. La page Aide affiche le tableau des périodes réelles de la configuration. Tests : parsing, édition, repli du profil actif et intégration HTTP.
---
README.md | 5 +-
src/webserver.nim | 172 ++++++++++++++++++++++++++++++++-------
tests/test_webserver.nim | 50 ++++++++++--
3 files changed, 189 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
index a06ce7b..368e9db 100644
--- a/README.md
+++ b/README.md
@@ -182,8 +182,9 @@ des pages HTML **servies localement** par l'application elle-même :
- **Statistiques** affiche les pomodoros du jour / semaine, la série de jours
consécutifs, le total et l'historique des 7 derniers jours.
- **Réglages** présente un formulaire complet (durées, objectif, son, langue,
- profil actif, rappels santé) : l'enregistrement est appliqué immédiatement
- par l'application.
+ rappels santé) avec un **éditeur de périodes** : créez, modifiez ou supprimez
+ vos propres profils (nom, travail, pauses, cycles) et choisissez la période
+ active. L'enregistrement est appliqué immédiatement par l'application.
L'application lance un petit serveur sur `127.0.0.1` (premier port libre à
partir de `38943`), uniquement accessible en local. Aucune donnée ne quitte
diff --git a/src/webserver.nim b/src/webserver.nim
index 33a7135..ff26dfe 100644
--- a/src/webserver.nim
+++ b/src/webserver.nim
@@ -256,6 +256,16 @@ input[type=number]:focus,select:focus{outline:none;border-color:#14b8a6;box-shad
.mini:hover{background:#f0fdfa;border-color:#5eead4}
.mini.danger{color:#b91c1c}
.mini.danger:hover{background:#fef2f2;border-color:#fca5a5}
+.proflist{display:flex;flex-direction:column;gap:.8rem}
+.profilrow{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:.9rem 1rem;box-shadow:0 2px 8px rgba(15,118,110,.05)}
+.profilrow.active{border-color:#5eead4;box-shadow:0 0 0 2px rgba(20,184,166,.15)}
+.phead{display:flex;align-items:center;gap:.7rem;margin-bottom:.6rem}
+.pname{flex:1;min-width:0;padding:.45rem .6rem;border:1.5px solid #cbd5e1;border-radius:9px;font-size:.98rem;font-family:inherit;color:#1e293b;background:#fff}
+.pfields{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:.6rem}
+.pcell{display:flex;flex-direction:column;gap:.25rem;font-size:.85rem;font-weight:600;color:#64748b}
+.pcell input{width:100%;padding:.4rem .5rem;border:1.5px solid #cbd5e1;border-radius:9px;font-size:.95rem;font-family:inherit;color:#1e293b;background:#fff}
+.pcell input:focus,.pname:focus{outline:none;border-color:#14b8a6;box-shadow:0 0 0 3px rgba(20,184,166,.18)}
+.activepick{display:flex;align-items:center;gap:.35rem;font-weight:700;color:#0f766e;cursor:pointer;white-space:nowrap}
.saved{text-align:center;padding:2.5rem 1.5rem}
.savedemoji{font-size:3rem;margin-bottom:.4rem}
footer{margin-top:2.8rem;padding-top:1.2rem;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:.9rem;text-align:center}
@@ -281,6 +291,9 @@ footer{margin-top:2.8rem;padding-top:1.2rem;border-top:1px solid #e2e8f0;color:#
.progress{background:#1e293b}
.empty{color:#94a3b8;border-color:#334155}
.remrow.off{opacity:.55}
+ .profilrow{background:#0f172a;border-color:#1e293b}
+ .pname,.pcell input{background:#0b1220;color:#e2e8f0;border-color:#334155}
+ .activepick{color:#5eead4}
}
@@ -333,17 +346,19 @@ proc renderHelp*(cfg: Config): string =
b.add "
⏱️ " & t(lang, "Les périodes", "The periods") & "
"
b.add "
" & t(lang,
- "Un cycle classique : travail (25 min), pause courte (5 min) puis, après plusieurs cycles, une pause longue (15 min). Tout est configurable via les profils :",
- "A classic cycle: work (25 min), short break (5 min) then, after several cycles, a long break (15 min). Everything is configurable via the profiles:") & "
"
+ "Un cycle enchaîne travail, pause courte puis, après plusieurs cycles, une pause longue. Créez et modifiez vos propres périodes depuis la page ⚙️ Réglages :",
+ "A cycle chains work, short break then, after several cycles, a long break. Create and edit your own profiles from the ⚙️ Settings page:") & ""
+ b.add "
"
+ for p in cfg.profiles:
+ let star = if p.name == cfg.activeProfile: "⭐" else: ""
+ b.add "
" & star & "
" & htmlEscape(p.name) & "
" & $p.workMin &
+ "
" & $p.shortBreakMin & "
" & $p.longBreakMin & "
" & $p.cycles & "
"
+ b.add "
"
b.add "
" & t(lang,
- "Le profil actif se choisit dans le menu de l'icône (« Profils »).",
- "The active profile is chosen from the icon menu (“Profiles”).") & "
"
+ "La période active est choisie dans le menu de l'icône (« Profils ») ou dans les réglages.",
+ "The active profile is chosen from the icon menu (“Profiles”) or in settings.") & "
"
b.add "
☕ " & t(lang, "Pendant une pause", "During a break") & "
"
b.add "
"
@@ -474,14 +489,24 @@ proc remindersJson(rs: seq[HealthReminder]): string =
items.add "{\"text\":" & $(%r.text) & ",\"enabled\":" & $(%r.enabled) & "}"
"[" & items.join(",") & "]"
+proc profilesJson(ps: seq[Profile]): string =
+ ## Sérialise les périodes (profils) en JSON (compact) pour l'éditeur.
+ var items: seq[string]
+ for p in ps:
+ items.add "{\"name\":" & $(%p.name) &
+ ",\"workMin\":" & $(%p.workMin) &
+ ",\"shortBreakMin\":" & $(%p.shortBreakMin) &
+ ",\"longBreakMin\":" & $(%p.longBreakMin) &
+ ",\"cycles\":" & $(%p.cycles) & "}"
+ "[" & items.join(",") & "]"
+
proc renderSettings*(cfg: Config): string =
let lang = resolveLang(cfg)
- var profileOpts = ""
- for p in cfg.profiles:
- let sel = if p.name == cfg.activeProfile: " selected" else: ""
- profileOpts.add ""
+ var activeIdx = 0
+ for i, p in cfg.profiles:
+ if p.name == cfg.activeProfile:
+ activeIdx = i
let langOpts =
""
var b = ""
- b.add "