diff --git a/CHANGELOG.md b/CHANGELOG.md index c75a2334..d3be963b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Hermes Web UI -- Changelog +## [Unreleased] + +### Fixed + +- **bug(cron): clarify one-shot schedule deletion semantics** ([#2031](https://github.com/nesquena/hermes-webui/issues/2031)). The Scheduled Jobs form now makes the Hermes Agent cron contract visible: recurring jobs should use `every 30m` or a cron expression, while bare durations/dates such as `30m`, `2h`, or `2026-05-11T08:00` create one-shot jobs that are removed after they run. Adds a live warning under the Schedule input when the entered value matches the one-shot forms. + ## [v0.51.39] — 2026-05-10 — Release O (4-PR contributor batch — Railway docker fix + Stop-button race + provider resolver + live context tracking) ### Fixed diff --git a/static/i18n.js b/static/i18n.js index a625dcd9..2dec4953 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -1011,7 +1011,8 @@ const LOCALES = { cron_name_label: 'Name', cron_name_placeholder: 'Optional', cron_schedule_label: 'Schedule', - cron_schedule_hint: "Cron expression or shorthand like 'every 1h'.", + cron_schedule_hint: "Use 'every 1h' or a cron expression for recurring jobs. Bare durations like '30m' run once.", + cron_schedule_once_warning: "Duration forms like '30m' run once and are removed after running. Use 'every 30m' to keep a recurring job.", cron_prompt_label: 'Prompt', cron_deliver_label: 'Deliver output to', cron_deliver_local: 'Local (save output only)', @@ -2095,7 +2096,8 @@ const LOCALES = { cron_name_label: '名前', cron_name_placeholder: '任意', cron_schedule_label: 'スケジュール', - cron_schedule_hint: "Cron 式または 'every 1h' のような短縮形。", + cron_schedule_hint: "繰り返し実行には 'every 1h' または Cron 式を使います。'30m' のような期間だけの指定は 1 回だけ実行されます。", + cron_schedule_once_warning: "'30m' のような期間指定は 1 回だけ実行され、実行後に削除されます。繰り返すには 'every 30m' を使ってください。", cron_prompt_label: 'プロンプト', cron_deliver_label: '出力先', cron_deliver_local: 'ローカル (出力を保存のみ)', @@ -2985,7 +2987,8 @@ const LOCALES = { cron_name_label: 'Имя', cron_name_placeholder: 'Необязательно', cron_schedule_label: 'Расписание', - cron_schedule_hint: "Cron-выражение или сокращение, например 'every 1h'.", + cron_schedule_hint: "Для повторяющихся заданий используйте 'every 1h' или cron-выражение. Простые интервалы вроде '30m' выполняются один раз.", + cron_schedule_once_warning: "Интервалы вроде '30m' выполняются один раз и удаляются после запуска. Используйте 'every 30m' для повторяющегося задания.", cron_prompt_label: 'Запрос', cron_deliver_label: 'Доставлять вывод', cron_deliver_local: 'Локально (только сохранение)', @@ -3990,7 +3993,8 @@ const LOCALES = { cron_name_label: 'Nombre', cron_name_placeholder: 'Opcional', cron_schedule_label: 'Programación', - cron_schedule_hint: "Expresión cron o abreviatura como 'every 1h'.", + cron_schedule_hint: "Usa 'every 1h' o una expresión cron para trabajos recurrentes. Duraciones como '30m' se ejecutan una sola vez.", + cron_schedule_once_warning: "Las duraciones como '30m' se ejecutan una vez y se eliminan después de correr. Usa 'every 30m' para mantener un trabajo recurrente.", cron_prompt_label: 'Prompt', cron_deliver_label: 'Entregar salida a', cron_deliver_local: 'Local (solo guardar salida)', @@ -4741,7 +4745,8 @@ const LOCALES = { cron_duplicated: 'Aufgabe dupliziert (pausiert)', cron_name_placeholder: 'Optional', cron_schedule_label: 'Zeitplan', - cron_schedule_hint: "Cron-Ausdruck oder Kurzform wie 'every 1h'.", + cron_schedule_hint: "Für wiederkehrende Aufgaben 'every 1h' oder einen Cron-Ausdruck verwenden. Reine Dauern wie '30m' laufen einmal.", + cron_schedule_once_warning: "Dauerangaben wie '30m' laufen einmal und werden nach der Ausführung entfernt. Verwende 'every 30m' für eine wiederkehrende Aufgabe.", cron_prompt_label: 'Prompt', cron_deliver_label: 'Ausgabe senden an', cron_deliver_local: 'Lokal (nur speichern)', @@ -6022,7 +6027,8 @@ const LOCALES = { cron_name_label: '名称', cron_name_placeholder: '可选', cron_schedule_label: '计划', - cron_schedule_hint: "Cron 表达式或简写,例如 'every 1h'。", + cron_schedule_hint: "循环任务请用 'every 1h' 或 Cron 表达式。像 '30m' 这样的裸时长只会运行一次。", + cron_schedule_once_warning: "像 '30m' 这样的时长写法只会运行一次,并在运行后移除。要保留循环任务,请使用 'every 30m'。", cron_prompt_label: '提示词', cron_deliver_label: '输出位置', cron_deliver_local: '本地(仅保存输出)', @@ -7236,7 +7242,8 @@ const LOCALES = { // Cron labels cron_name_label: '任務名稱', cron_schedule_label: '排程', - cron_schedule_hint: '例如: 0 9 * * *, every 2h, 30m', + cron_schedule_hint: "循環任務請用 'every 1h' 或 Cron 表達式。像 '30m' 這樣的裸時長只會執行一次。", + cron_schedule_once_warning: "像 '30m' 這樣的時長寫法只會執行一次,並在執行後移除。要保留循環任務,請使用 'every 30m'。", cron_prompt_label: '提示', cron_deliver_label: '發送至', cron_deliver_local: '僅本地儲存', @@ -8142,7 +8149,8 @@ const LOCALES = { cron_name_label: 'Nome', cron_name_placeholder: 'Opcional', cron_schedule_label: 'Agendamento', - cron_schedule_hint: "Expressão Cron ou shorthand como 'every 1h'.", + cron_schedule_hint: "Use 'every 1h' ou uma expressão Cron para tarefas recorrentes. Durações como '30m' rodam uma vez.", + cron_schedule_once_warning: "Durações como '30m' rodam uma vez e são removidas após executar. Use 'every 30m' para manter uma tarefa recorrente.", cron_prompt_label: 'Prompt', cron_deliver_label: 'Entregar output para', cron_deliver_local: 'Local (salvar output apenas)', @@ -9192,7 +9200,8 @@ const LOCALES = { cron_name_label: 'Name', cron_name_placeholder: 'Optional', cron_schedule_label: 'Schedule', - cron_schedule_hint: "Cron expression or shorthand like 'every 1h'.", + cron_schedule_hint: "Use 'every 1h' or a cron expression for recurring jobs. Bare durations like '30m' run once.", + cron_schedule_once_warning: "Duration forms like '30m' run once and are removed after running. Use 'every 30m' to keep a recurring job.", cron_prompt_label: 'Prompt', cron_deliver_label: 'Deliver output to', cron_deliver_local: 'Local (save output only)', diff --git a/static/panels.js b/static/panels.js index 7f73e503..86075f1b 100644 --- a/static/panels.js +++ b/static/panels.js @@ -231,6 +231,26 @@ function _isRecurringCronJob(job) { return kind === 'cron' || kind === 'interval'; } +function _cronScheduleKindForInput(value) { + const schedule = String(value || '').trim(); + if (!schedule) return ''; + const lower = schedule.toLowerCase(); + if (lower.startsWith('every ')) return 'interval'; + if (lower.startsWith('@')) return 'cron'; + const parts = schedule.split(/\s+/); + if (parts.length >= 5 && parts.slice(0, 5).every(p => /^[\d*\-,/]+$/.test(p))) return 'cron'; + if (schedule.includes('T') || /^\d{4}-\d{2}-\d{2}/.test(schedule)) return 'once'; + if (/^\d+\s*(m|min|mins|minute|minutes|h|hr|hrs|hour|hours|d|day|days)$/i.test(schedule)) return 'once'; + return ''; +} + +function _syncCronScheduleWarning() { + const input = $('cronFormSchedule'); + const warning = $('cronFormScheduleOnceWarning'); + if (!input || !warning) return; + warning.style.display = _cronScheduleKindForInput(input.value) === 'once' ? '' : 'none'; +} + function _hasUnlimitedRepeat(job) { return !!(job && job.repeat && job.repeat.times == null); } @@ -722,6 +742,7 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, no_agent=fa