feat(web): éditeur de périodes modifiables dans les réglages

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.
This commit is contained in:
2026-08-14 23:37:23 -04:00
parent 162d574ff4
commit b47bf61386
3 changed files with 189 additions and 38 deletions
+3 -2
View File
@@ -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 - **Statistiques** affiche les pomodoros du jour / semaine, la série de jours
consécutifs, le total et l'historique des 7 derniers jours. consécutifs, le total et l'historique des 7 derniers jours.
- **Réglages** présente un formulaire complet (durées, objectif, son, langue, - **Réglages** présente un formulaire complet (durées, objectif, son, langue,
profil actif, rappels santé) : l'enregistrement est appliqué immédiatement rappels santé) avec un **éditeur de périodes** : créez, modifiez ou supprimez
par l'application. 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 à 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 partir de `38943`), uniquement accessible en local. Aucune donnée ne quitte
+142 -30
View File
@@ -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:hover{background:#f0fdfa;border-color:#5eead4}
.mini.danger{color:#b91c1c} .mini.danger{color:#b91c1c}
.mini.danger:hover{background:#fef2f2;border-color:#fca5a5} .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} .saved{text-align:center;padding:2.5rem 1.5rem}
.savedemoji{font-size:3rem;margin-bottom:.4rem} .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} 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} .progress{background:#1e293b}
.empty{color:#94a3b8;border-color:#334155} .empty{color:#94a3b8;border-color:#334155}
.remrow.off{opacity:.55} .remrow.off{opacity:.55}
.profilrow{background:#0f172a;border-color:#1e293b}
.pname,.pcell input{background:#0b1220;color:#e2e8f0;border-color:#334155}
.activepick{color:#5eead4}
} }
</style> </style>
</head> </head>
@@ -333,17 +346,19 @@ proc renderHelp*(cfg: Config): string =
b.add "<h2>⏱️ " & t(lang, "Les périodes", "The periods") & "</h2>" b.add "<h2>⏱️ " & t(lang, "Les périodes", "The periods") & "</h2>"
b.add "<div class='card'><p>" & t(lang, b.add "<div class='card'><p>" & 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 :", "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 classic cycle: work (25 min), short break (5 min) then, after several cycles, a long break (15 min). Everything is configurable via the profiles:") & "</p>" "A cycle chains work, short break then, after several cycles, a long break. Create and edit your own profiles from the ⚙️ Settings page:") & "</p>"
b.add "<table><tr><th>👤 " & t(lang, "Profil", "Profile") & "</th><th>💼 " & t(lang, "Travail", "Work") & b.add "<table><tr><th>⭐ " & t(lang, "Active", "Active") & "</th><th>👤 " & t(lang, "Période", "Profile") &
"</th><th>☕ " & t(lang, "Pause courte", "Short break") & "</th><th>🛋️ " & t(lang, "Pause longue", "Long break") & "</th><th>💼 " & t(lang, "Travail", "Work") & "</th><th>☕ " & t(lang, "Pause courte", "Short break") &
"</th><th>🔁 " & t(lang, "Cycles", "Cycles") & "</th></tr>" "</th><th>🛋️ " & t(lang, "Pause longue", "Long break") & "</th><th>🔁 " & t(lang, "Cycles", "Cycles") & "</th></tr>"
b.add "<tr><td>" & t(lang, "Classique", "Classic") & "</td><td>25 min</td><td>5 min</td><td>15 min</td><td>4</td></tr>" for p in cfg.profiles:
b.add "<tr><td>Deep Work</td><td>50 min</td><td>10 min</td><td>20 min</td><td>4</td></tr>" let star = if p.name == cfg.activeProfile: "⭐" else: ""
b.add "<tr><td>Sprint</td><td>15 min</td><td>3 min</td><td>10 min</td><td>6</td></tr></table>" b.add "<tr><td>" & star & "</td><td><b>" & htmlEscape(p.name) & "</b></td><td>" & $p.workMin &
"</td><td>" & $p.shortBreakMin & "</td><td>" & $p.longBreakMin & "</td><td>" & $p.cycles & "</td></tr>"
b.add "</table>"
b.add "<p class='hint'>" & t(lang, b.add "<p class='hint'>" & t(lang,
"Le profil actif se choisit dans le menu de l'icône (« Profils »).", "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”).") & "</p></div>" "The active profile is chosen from the icon menu (“Profiles”) or in settings.") & "</p></div>"
b.add "<h2>☕ " & t(lang, "Pendant une pause", "During a break") & "</h2>" b.add "<h2>☕ " & t(lang, "Pendant une pause", "During a break") & "</h2>"
b.add "<div class='card'><ul class='clean'>" b.add "<div class='card'><ul class='clean'>"
@@ -474,14 +489,24 @@ proc remindersJson(rs: seq[HealthReminder]): string =
items.add "{\"text\":" & $(%r.text) & ",\"enabled\":" & $(%r.enabled) & "}" items.add "{\"text\":" & $(%r.text) & ",\"enabled\":" & $(%r.enabled) & "}"
"[" & items.join(",") & "]" "[" & 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 = proc renderSettings*(cfg: Config): string =
let lang = resolveLang(cfg) let lang = resolveLang(cfg)
var profileOpts = "" var activeIdx = 0
for p in cfg.profiles: for i, p in cfg.profiles:
let sel = if p.name == cfg.activeProfile: " selected" else: "" if p.name == cfg.activeProfile:
profileOpts.add "<option value='" & htmlEscape(p.name) & "'" & sel & ">" & activeIdx = i
htmlEscape(p.name) & "</option>"
let langOpts = let langOpts =
"<option value='auto'" & (if cfg.lang == "auto": " selected" else: "") & ">🌐 " & "<option value='auto'" & (if cfg.lang == "auto": " selected" else: "") & ">🌐 " &
@@ -490,12 +515,12 @@ proc renderSettings*(cfg: Config): string =
"<option value='en'" & (if cfg.lang == "en": " selected" else: "") & ">🇬🇧 English</option>" "<option value='en'" & (if cfg.lang == "en": " selected" else: "") & ">🇬🇧 English</option>"
var b = "" var b = ""
b.add "<form method='post' action='/settings' onsubmit='syncReminders()'>" b.add "<form method='post' action='/settings' onsubmit='syncReminders(); syncProfiles()'>"
# --- Minuteur --- # --- Minuteur ---
b.add "<div class='card'><h2>⏱️ " & t(lang, "Minuteur", "Timer") & "</h2>" b.add "<div class='card'><h2>⏱️ " & t(lang, "Minuteur", "Timer") & "</h2>"
b.add "<p class='hint'>" & t(lang, b.add "<p class='hint'>" & t(lang,
"Valeurs de base utilisées quand aucun profil ne correspond.", "Valeurs de base utilisées quand aucune période ne correspond.",
"Base values used when no profile matches.") & "</p>" "Base values used when no profile matches.") & "</p>"
b.add "<div class='grid2'>" b.add "<div class='grid2'>"
b.add numField("💼", t(lang, "Travail (min)", "Work (min)"), "workMin", cfg.workMin, 1) b.add numField("💼", t(lang, "Travail (min)", "Work (min)"), "workMin", cfg.workMin, 1)
@@ -514,13 +539,15 @@ proc renderSettings*(cfg: Config): string =
"<select name='lang'>" & langOpts & "</select></label>" "<select name='lang'>" & langOpts & "</select></label>"
b.add "</div></div>" b.add "</div></div>"
# --- Profil actif --- # --- Périodes (éditeur client) ---
b.add "<div class='card'><h2>👤 " & t(lang, "Profil actif", "Active profile") & "</h2>" b.add "<div class='card'><h2>📚 " & t(lang, "Périodes", "Profiles") & "</h2>"
b.add "<p class='hint'>" & t(lang, b.add "<p class='hint'>" & t(lang,
"Détermine les durées utilisées pour les prochaines périodes.", "Créez et modifiez vos périodes : nom, travail, pauses courtes et longues, cycles. Cochez ★ pour choisir la période active.",
"Determines the durations used for the next periods.") & "</p>" "Create and edit your profiles: name, work, short and long breaks, cycles. Check ★ to pick the active profile.") & "</p>"
b.add "<label class='field'><span class='flabel'>📦 " & t(lang, "Profil", "Profile") & "</span>" & b.add "<div id='profiles'></div>"
"<select name='activeProfile'>" & profileOpts & "</select></label>" b.add "<button type='button' class='btn ghost' onclick='addProfile()' style='margin-top:.8rem'>➕ " &
t(lang, "Ajouter une période", "Add a profile") & "</button>"
b.add "<input type='hidden' name='profiles_json' id='profiles_json'>"
b.add "</div>" b.add "</div>"
# --- Notifications et système --- # --- Notifications et système ---
@@ -548,6 +575,8 @@ proc renderSettings*(cfg: Config): string =
b.add "<script>" b.add "<script>"
b.add "var reminders = " & remindersJson(cfg.healthReminders) & ";" b.add "var reminders = " & remindersJson(cfg.healthReminders) & ";"
b.add "var profiles = " & profilesJson(cfg.profiles) & ";"
b.add "var activeIdx = " & $activeIdx & ";"
b.add """var R = function(fr, en){ return document.documentElement.lang === 'fr' ? fr : en; }; b.add """var R = function(fr, en){ return document.documentElement.lang === 'fr' ? fr : en; };
function esc(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); } function esc(s){ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function renderReminders(){ function renderReminders(){
@@ -580,7 +609,57 @@ function editReminder(i){
function removeReminder(i){ reminders.splice(i, 1); renderReminders(); } function removeReminder(i){ reminders.splice(i, 1); renderReminders(); }
function toggleReminder(i){ reminders[i].enabled = !reminders[i].enabled; } function toggleReminder(i){ reminders[i].enabled = !reminders[i].enabled; }
function syncReminders(){ document.getElementById('reminders_json').value = JSON.stringify(reminders); } function syncReminders(){ document.getElementById('reminders_json').value = JSON.stringify(reminders); }
renderReminders(); function renderProfiles(){
var el = document.getElementById('profiles');
if (profiles.length === 0){
el.innerHTML = '<div class="empty">' + R('🌱 Aucune période.','🌱 No profiles yet.') + '</div>';
return;
}
var html = '<div class="proflist">';
for (var i = 0; i < profiles.length; i++){
var p = profiles[i];
html += '<div class="profilrow' + (i === activeIdx ? ' active' : '') + '">' +
'<div class="phead">' +
'<label class="activepick"><input type="radio" name="active_profile" value="' + i + '" ' + (i === activeIdx ? 'checked' : '') + ' onchange="setActive(' + i + ')"> <span>' + R('★ Active','★ Active') + '</span></label>' +
'<input class="pname" type="text" value="' + esc(p.name) + '" placeholder="' + R('Nom de la période','Profile name') + '" data-key="name" oninput="updateProfile(this)">' +
'<button type="button" class="mini danger" onclick="removeProfile(' + i + ')">🗑️</button>' +
'</div>' +
'<div class="pfields">' +
'<label class="pcell"><span>💼 ' + R('Travail','Work') + '</span><input type="number" min="1" value="' + p.workMin + '" data-key="workMin" oninput="updateProfile(this)"></label>' +
'<label class="pcell"><span>☕ ' + R('Pause courte','Short break') + '</span><input type="number" min="1" value="' + p.shortBreakMin + '" data-key="shortBreakMin" oninput="updateProfile(this)"></label>' +
'<label class="pcell"><span>🛋️ ' + R('Pause longue','Long break') + '</span><input type="number" min="1" value="' + p.longBreakMin + '" data-key="longBreakMin" oninput="updateProfile(this)"></label>' +
'<label class="pcell"><span>🔁 ' + R('Cycles','Cycles') + '</span><input type="number" min="1" value="' + p.cycles + '" data-key="cycles" oninput="updateProfile(this)"></label>' +
'</div></div>';
}
html += '</div>';
el.innerHTML = html;
}
function updateProfile(el){
var row = el.closest('.profilrow');
var idx = Array.prototype.indexOf.call(document.querySelectorAll('.profilrow'), row);
if (idx < 0 || idx >= profiles.length) return;
var key = el.getAttribute('data-key');
if (key === 'name'){ profiles[idx].name = el.value; }
else { profiles[idx][key] = parseInt(el.value) || 1; }
}
function setActive(i){ activeIdx = i; renderProfiles(); }
function addProfile(){
var base = R('Nouvelle période','New profile');
var name = base;
var n = 1;
while (profiles.some(function(p){ return p.name === name; })){ n++; name = base + ' ' + n; }
profiles.push({name: name, workMin: 25, shortBreakMin: 5, longBreakMin: 15, cycles: 4});
renderProfiles();
}
function removeProfile(i){
if (profiles.length <= 1){ alert(R('Au moins une période est requise.','At least one profile is required.')); return; }
profiles.splice(i, 1);
if (i === activeIdx){ activeIdx = 0; }
else if (i < activeIdx){ activeIdx--; }
renderProfiles();
}
function syncProfiles(){ document.getElementById('profiles_json').value = JSON.stringify(profiles); }
renderProfiles();
""" """
b.add "</script>" b.add "</script>"
@@ -623,11 +702,47 @@ proc applySettings*(cfg: Config, form: Table[string, string]): Config =
let lang = form.getOrDefault("lang") let lang = form.getOrDefault("lang")
if lang in ["auto", "fr", "en"]: if lang in ["auto", "fr", "en"]:
result.lang = lang result.lang = lang
let prof = form.getOrDefault("activeProfile")
# --- Périodes (profils) modifiables ---
let profJson = form.getOrDefault("profiles_json")
if profJson.len > 0:
try:
let n = parseJson(profJson)
if n.kind == JArray:
var newProfiles: seq[Profile]
var seen: seq[string]
for p in n:
let name = p.getOrDefault("name").getStr().strip()
if name.len == 0 or name in seen:
continue # noms vides ou doublons ignorés
seen.add name
newProfiles.add Profile(
name: name,
workMin: p.getOrDefault("workMin").getInt(25),
shortBreakMin: p.getOrDefault("shortBreakMin").getInt(5),
longBreakMin: p.getOrDefault("longBreakMin").getInt(15),
cycles: p.getOrDefault("cycles").getInt(4))
if newProfiles.len > 0:
result.profiles = newProfiles
except:
discard
# --- Profil actif (index envoyé par l'éditeur) ---
let activeIdxStr = form.getOrDefault("active_profile")
if activeIdxStr.len > 0:
let idx = parseIntSafe(activeIdxStr, -1)
if idx >= 0 and idx < result.profiles.len:
result.activeProfile = result.profiles[idx].name
# Sécurité : le profil actif doit exister, sinon premier profil.
var activeExists = false
for p in result.profiles: for p in result.profiles:
if p.name == prof: if p.name == result.activeProfile:
result.activeProfile = prof activeExists = true
break break
if not activeExists and result.profiles.len > 0:
result.activeProfile = result.profiles[0].name
# --- Rappels santé ---
let remJson = form.getOrDefault("reminders_json") let remJson = form.getOrDefault("reminders_json")
if remJson.len > 0: if remJson.len > 0:
try: try:
@@ -644,9 +759,6 @@ proc applySettings*(cfg: Config, form: Table[string, string]): Config =
except: except:
discard discard
# ---------------------------------------------------------------------------
# Routage
# ---------------------------------------------------------------------------
proc handleRequest(req: HttpRequest): string {.gcsafe.} = proc handleRequest(req: HttpRequest): string {.gcsafe.} =
case req.path case req.path
+44 -6
View File
@@ -43,8 +43,11 @@ suite "applySettings":
test "applies a full form": test "applies a full form":
let base = defaultConfig() let base = defaultConfig()
let form = parseForm("workMin=40&shortBreakMin=8&longBreakMin=20&" & let form = parseForm("workMin=40&shortBreakMin=8&longBreakMin=20&" &
"cyclesBeforeLongBreak=5&dailyGoal=6&lang=en&activeProfile=Sprint&" & "cyclesBeforeLongBreak=5&dailyGoal=6&lang=en&" &
"soundEnabled=on&doNotDisturb=on&" & "active_profile=1&" &
"profiles_json=" & encodeUrl(
"""[{"name":"Focus","workMin":30,"shortBreakMin":6,"longBreakMin":18,"cycles":4},{"name":"Rapide","workMin":10,"shortBreakMin":2,"longBreakMin":5,"cycles":3}]""") &
"&soundEnabled=on&doNotDisturb=on&" &
"reminders_json=" & encodeUrl("""[{"text":"Boire de l'eau","enabled":true}]""")) "reminders_json=" & encodeUrl("""[{"text":"Boire de l'eau","enabled":true}]"""))
let cfg = applySettings(base, form) let cfg = applySettings(base, form)
check cfg.workMin == 40 check cfg.workMin == 40
@@ -53,16 +56,43 @@ suite "applySettings":
check cfg.cyclesBeforeLongBreak == 5 check cfg.cyclesBeforeLongBreak == 5
check cfg.dailyGoal == 6 check cfg.dailyGoal == 6
check cfg.lang == "en" check cfg.lang == "en"
check cfg.activeProfile == "Sprint" check cfg.profiles.len == 2
check cfg.profiles[0].name == "Focus"
check cfg.profiles[1].workMin == 10
check cfg.activeProfile == "Rapide"
check cfg.soundEnabled check cfg.soundEnabled
check cfg.doNotDisturb check cfg.doNotDisturb
check not cfg.autoStart check not cfg.autoStart
check cfg.healthReminders.len == 1 check cfg.healthReminders.len == 1
check cfg.healthReminders[0].text == "Boire de l'eau" check cfg.healthReminders[0].text == "Boire de l'eau"
test "profiles are edited and active profile falls back safely":
let base = defaultConfig()
# Deux périodes, active = index 1.
let f1 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"A","workMin":25,"shortBreakMin":5,"longBreakMin":15,"cycles":4},{"name":"B","workMin":50,"shortBreakMin":10,"longBreakMin":20,"cycles":5}]""") &
"&active_profile=1")
let c1 = applySettings(base, f1)
check c1.profiles.len == 2
check c1.activeProfile == "B"
# Index hors limites (période active supprimée) : repli sur la première.
let f2 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"C","workMin":20,"shortBreakMin":4,"longBreakMin":10,"cycles":3}]""") &
"&active_profile=5")
let c2 = applySettings(base, f2)
check c2.profiles.len == 1
check c2.activeProfile == "C"
# Noms vides et doublons ignorés.
let f3 = parseForm("profiles_json=" & encodeUrl(
"""[{"name":"","workMin":1,"shortBreakMin":1,"longBreakMin":1,"cycles":1},{"name":"D","workMin":33,"shortBreakMin":3,"longBreakMin":9,"cycles":2},{"name":"D","workMin":99,"shortBreakMin":9,"longBreakMin":27,"cycles":9}]"""))
let c3 = applySettings(base, f3)
check c3.profiles.len == 1
check c3.profiles[0].name == "D"
check c3.profiles[0].workMin == 33
test "invalid values fall back to current ones": test "invalid values fall back to current ones":
let base = defaultConfig() let base = defaultConfig()
let form = parseForm("workMin=abc&lang=zz&activeProfile=Inexistant") let form = parseForm("workMin=abc&lang=zz&active_profile=99")
let cfg = applySettings(base, form) let cfg = applySettings(base, form)
check cfg.workMin == base.workMin check cfg.workMin == base.workMin
check cfg.lang == "auto" check cfg.lang == "auto"
@@ -78,6 +108,8 @@ suite "rendering":
check "Réglages" in renderSettings(cfg) check "Réglages" in renderSettings(cfg)
check "name='workMin'" in renderSettings(cfg) check "name='workMin'" in renderSettings(cfg)
check "reminders_json" in renderSettings(cfg) check "reminders_json" in renderSettings(cfg)
check "profiles_json" in renderSettings(cfg)
check "active_profile" in renderSettings(cfg)
suite "integration (local HTTP server)": suite "integration (local HTTP server)":
test "GET routes work and settings POST persists": test "GET routes work and settings POST persists":
@@ -109,8 +141,10 @@ suite "integration (local HTTP server)":
# Sauvegarde via POST (comme le ferait le navigateur). # Sauvegarde via POST (comme le ferait le navigateur).
let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" & let body = "workMin=33&shortBreakMin=6&longBreakMin=18&cyclesBeforeLongBreak=4&" &
"dailyGoal=5&lang=fr&activeProfile=Classique&" & "dailyGoal=5&lang=fr&active_profile=1&" &
"reminders_json=" & encodeUrl( "profiles_json=" & encodeUrl(
"""[{"name":"Zen","workMin":35,"shortBreakMin":7,"longBreakMin":20,"cycles":4},{"name":"Bolt","workMin":5,"shortBreakMin":1,"longBreakMin":3,"cycles":2}]""") &
"&reminders_json=" & encodeUrl(
"""[{"text":"Étire-toi","enabled":true},{"text":"Bois de l'eau","enabled":false}]""") """[{"text":"Étire-toi","enabled":true},{"text":"Bois de l'eau","enabled":false}]""")
let resp = client.postContent(base & "/settings", body) let resp = client.postContent(base & "/settings", body)
check "Réglages enregistrés" in resp check "Réglages enregistrés" in resp
@@ -120,6 +154,10 @@ suite "integration (local HTTP server)":
check saved.workMin == 33 check saved.workMin == 33
check saved.dailyGoal == 5 check saved.dailyGoal == 5
check saved.lang == "fr" check saved.lang == "fr"
check saved.profiles.len == 2
check saved.profiles[0].name == "Zen"
check saved.profiles[1].workMin == 5
check saved.activeProfile == "Bolt"
check saved.healthReminders.len == 2 check saved.healthReminders.len == 2
check saved.healthReminders[1].enabled == false check saved.healthReminders[1].enabled == false