keepsafe rework & alert ajustments
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-07-26 17:48:14 +02:00
parent 68005ef918
commit 2e6d8a2c0c
11 changed files with 129 additions and 45 deletions

View file

@ -11,6 +11,7 @@ import {
getDailyTargetMs,
getNowMs,
getTimePatternTargetMs,
getTimePatternRecurrenceMs,
timePartsToDurationMs,
timePartsToString
} from "./timerUtils.js";
@ -27,6 +28,17 @@ const ALERT_MODES = [
{ mode: "site", icon: "sound-max" }
];
const ALERT_MIN_DELAY_MS = 5 * 60 * 1000;
const AUTO_REFRESH_COUNTDOWN_TYPES = new Set(["time_pattern", "interval"]);
function isAlertGuarded(countdown) {
if (countdown?.type === "interval") return Number(countdown.intervalMs) < ALERT_MIN_DELAY_MS;
if (countdown?.type === "time_pattern") return getTimePatternRecurrenceMs(countdown.pattern) < ALERT_MIN_DELAY_MS;
return false;
}
function getAlertGuardMessage(textContent) {
return textContent.alertTooSoonTitle || "Alerte indisponible pour une répétition inférieure à 5 minutes";
}
function TimerValue({ ms, compact = false, showCentiseconds = true }) {
if (!showCentiseconds) return <span className="timer-value">{formatDuration(ms)}</span>;
@ -204,9 +216,8 @@ export function TimerModule({ toolboxId, moduleId, context }) {
function setCountdownAlertMode(countdownId, alertMode) {
const countdown = data.countdowns.find((item) => item.id === countdownId);
const targetMs = countdown ? getCountdownTargetMs(countdown, nowMs) : 0;
if (alertMode !== "off" && targetMs && targetMs - nowMs < ALERT_MIN_DELAY_MS) {
context.notify?.(textContent.alertTooSoonTitle || "Alerte indisponible à moins de 5 minutes");
if (alertMode !== "off" && AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown?.type) && isAlertGuarded(countdown)) {
context.notify?.(getAlertGuardMessage(textContent));
return;
}
save({
@ -303,7 +314,7 @@ function TimerAlertModeSwitch({ mode, blocked, textContent, onChange }) {
visible: textContent.alertVisibleTitle || "Alerte quand la toolbox est affichée",
site: textContent.alertSiteTitle || "Alerte sur tout le site"
};
const blockedTitle = textContent.alertTooSoonTitle || "Alerte indisponible à moins de 5 minutes";
const blockedTitle = getAlertGuardMessage(textContent);
return (
<div
@ -316,11 +327,11 @@ function TimerAlertModeSwitch({ mode, blocked, textContent, onChange }) {
>
{ALERT_MODES.map((option) => {
const label = labels[option.mode];
const disabled = blocked && option.mode !== "off";
const disabled = Boolean(blocked);
return (
<button
key={option.mode}
className={`timer-alert-mode-button ${mode === option.mode ? "active" : ""}`}
className={`timer-alert-mode-button ${!blocked && mode === option.mode ? "active" : ""}`}
type="button"
onClick={() => onChange(option.mode)}
disabled={disabled}
@ -368,31 +379,31 @@ function CountdownControls({ textContent, form, onChange, onSubmit }) {
<select value={form.type} onChange={(event) => onChange({ type: event.target.value })}>
<option value="duration">{textContent.durationType || "Durée"}</option>
<option value="daily_time">{textContent.dailyTimeType || "Heure précise"}</option>
<option value="time_pattern">{textContent.timePatternType || "Horaire spécifique"}</option>
<option value="time_pattern">{textContent.timePatternType || "Pattern horaire"}</option>
<option value="interval">{textContent.intervalType || "Intervalle"}</option>
</select>
</label>
{form.type === "duration" && (
<label>
<span>{textContent.durationLabel || "Durée"}</span>
<span className="sr-only">{textContent.durationLabel || "Durée"}</span>
<TimePartsInput value={form.duration} onChange={(duration) => onChange({ duration })} />
</label>
)}
{form.type === "daily_time" && (
<label>
<span>{textContent.dailyTimeLabel || "Heure"}</span>
<span className="sr-only">{textContent.dailyTimeLabel || "Heure"}</span>
<TimePartsInput value={form.time} onChange={(time) => onChange({ time })} />
</label>
)}
{form.type === "time_pattern" && (
<label>
<span>{textContent.timePatternLabel || "Heure / minute / seconde"}</span>
<span className="sr-only">{textContent.timePatternLabel || "Heure / minute / seconde"}</span>
<TimePartsInput value={form.pattern} onChange={(pattern) => onChange({ pattern })} allowWildcard />
</label>
)}
{form.type === "interval" && (
<label>
<span>{textContent.intervalLabel || "Toutes les"}</span>
<span className="sr-only">{textContent.intervalLabel || "Toutes les"}</span>
<TimePartsInput value={form.interval} onChange={(interval) => onChange({ interval })} />
</label>
)}
@ -512,7 +523,7 @@ function CountdownList({ countdowns, nowMs, textContent, onRename, onAlertModeCh
)}
<TimerAlertModeSwitch
mode={countdown.alertMode}
blocked={countdown.targetMs && countdown.targetMs - nowMs < ALERT_MIN_DELAY_MS}
blocked={AUTO_REFRESH_COUNTDOWN_TYPES.has(countdown.type) && isAlertGuarded(countdown)}
textContent={textContent}
onChange={(alertMode) => onAlertModeChange(countdown.id, alertMode)}
/>

View file

@ -81,17 +81,31 @@ export function getTimePatternTargetMs(pattern, nowMs) {
const now = new Date(nowMs);
const candidate = new Date(nowMs);
candidate.setMilliseconds(0);
const hasHour = parsed.hours !== "X";
const hasMinute = parsed.minutes !== "X";
for (let offset = 1; offset <= 86400; offset += 1) {
candidate.setTime(now.getTime() + offset * 1000);
const hours = parsed.hours === "X" || Number(parsed.hours) === candidate.getHours();
const minutes = parsed.minutes === "X" || Number(parsed.minutes) === candidate.getMinutes();
const seconds = parsed.seconds === "X" || Number(parsed.seconds) === candidate.getSeconds();
const minutes = parsed.minutes === "X"
? (!hasHour || candidate.getMinutes() === 0)
: Number(parsed.minutes) === candidate.getMinutes();
const seconds = parsed.seconds === "X"
? (!hasHour && !hasMinute) || candidate.getSeconds() === 0
: Number(parsed.seconds) === candidate.getSeconds();
if (hours && minutes && seconds) return candidate.getTime();
}
return 0;
}
export function getTimePatternRecurrenceMs(pattern) {
const baseMs = new Date(2026, 0, 1, 0, 0, 0, 0).getTime();
const firstTargetMs = getTimePatternTargetMs(pattern, baseMs);
if (!firstTargetMs) return 0;
const secondTargetMs = getTimePatternTargetMs(pattern, firstTargetMs);
return secondTargetMs ? secondTargetMs - firstTargetMs : 0;
}
export function getIntervalTargetMs(intervalMs, anchorAt, nowMs) {
const safeIntervalMs = Math.max(1000, Number(intervalMs) || 0);
const safeAnchor = Number.isFinite(anchorAt) && anchorAt > 0 ? anchorAt : nowMs;