mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
fix: allow no-agent cron edits without prompt
This commit is contained in:
committed by
nesquena-hermes
parent
0ed63968b6
commit
48773e8ff7
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
+16
-5
@@ -422,6 +422,9 @@ function _renderCronDetail(job){
|
||||
const schedule = job.schedule_display || (job.schedule && job.schedule.expression) || '';
|
||||
const skills = Array.isArray(job.skills) && job.skills.length ? job.skills.join(', ') : '—';
|
||||
const deliver = job.deliver || 'local';
|
||||
const isNoAgent = !!job.no_agent;
|
||||
const cronJobMode = isNoAgent ? 'no-agent' : 'agent';
|
||||
const script = job.script || '';
|
||||
const profileLabel = _cronProfileLabel(job.profile);
|
||||
const profileTitle = _cronProfileTitle(job.profile);
|
||||
const lastError = job.last_error ? `<div class="detail-row"><div class="detail-row-label">${esc(t('error_prefix').replace(/:\s*$/,''))}</div><div class="detail-row-value" style="color:var(--accent-text)">${esc(job.last_error)}</div></div>` : '';
|
||||
@@ -450,6 +453,8 @@ function _renderCronDetail(job){
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_next'))}</div><div class="detail-row-value">${esc(nextRun)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_last'))}</div><div class="detail-row-value">${esc(lastRun)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Deliver</div><div class="detail-row-value">${esc(deliver)}</div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Mode</div><div class="detail-row-value"><span class="detail-badge" id="cronJobMode">${esc(cronJobMode)}</span></div></div>
|
||||
${isNoAgent ? `<div class="detail-row"><div class="detail-row-label">No-agent script</div><div class="detail-row-value"><code>${esc(script || '—')}</code></div></div>` : ''}
|
||||
<div class="detail-row"><div class="detail-row-label">${esc(t('cron_profile_label') || 'Profile')}</div><div class="detail-row-value"><span class="detail-badge active" title="${esc(profileTitle)}">${esc(profileLabel)}</span></div></div>
|
||||
<div class="detail-row"><div class="detail-row-label">Skills</div><div class="detail-row-value">${esc(skills)}</div></div>
|
||||
${lastError}
|
||||
@@ -685,6 +690,8 @@ function openCronEdit(job){
|
||||
prompt: job.prompt || '',
|
||||
deliver: job.deliver || 'local',
|
||||
profile: job.profile || '',
|
||||
no_agent: !!job.no_agent,
|
||||
script: job.script || '',
|
||||
isEdit: true,
|
||||
});
|
||||
if (!_cronSkillsCache) {
|
||||
@@ -695,11 +702,12 @@ function openCronEdit(job){
|
||||
loadCronProfiles().then(()=>_refreshCronProfileSelect(job.profile || '')).catch(()=>{});
|
||||
}
|
||||
|
||||
function _renderCronForm({ name, schedule, prompt, deliver, profile, isEdit }){
|
||||
function _renderCronForm({ name, schedule, prompt, deliver, profile, no_agent=false, script='', isEdit }){
|
||||
const title = $('taskDetailTitle');
|
||||
const body = $('taskDetailBody');
|
||||
const empty = $('taskDetailEmpty');
|
||||
if (!body || !title) return;
|
||||
const isNoAgent = !!no_agent;
|
||||
title.textContent = isEdit ? (t('edit') + ' · ' + (name || schedule || t('scheduled_jobs'))) : t('new_job');
|
||||
const deliverOpt = (v,l) => `<option value="${v}"${deliver===v?' selected':''}>${esc(l)}</option>`;
|
||||
body.innerHTML = `
|
||||
@@ -714,9 +722,10 @@ function _renderCronForm({ name, schedule, prompt, deliver, profile, isEdit }){
|
||||
<input type="text" id="cronFormSchedule" value="${esc(schedule || '')}" placeholder="0 9 * * * — every 1h — @daily" autocomplete="off" required>
|
||||
<div class="detail-form-hint">${esc(t('cron_schedule_hint') || "Cron expression or shorthand like 'every 1h'.")}</div>
|
||||
</div>
|
||||
<div class="detail-form-row">
|
||||
<div class="detail-form-row ${isNoAgent ? 'cron-no-agent-prompt-row' : ''}">
|
||||
<label for="cronFormPrompt">${esc(t('cron_prompt_label') || 'Prompt')}</label>
|
||||
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}" required>${esc(prompt || '')}</textarea>
|
||||
<textarea id="cronFormPrompt" rows="6" placeholder="${esc(t('cron_prompt_placeholder') || 'Must be self-contained')}"${isNoAgent ? ' disabled' : ' required'}>${esc(prompt || '')}</textarea>
|
||||
${isNoAgent ? `<div class="detail-form-hint cron-no-agent-hint">No-agent mode runs the configured script directly; Prompt is unused. No-agent script: <code>${esc(script || '—')}</code></div>` : ''}
|
||||
</div>
|
||||
<div class="detail-form-row">
|
||||
<label for="cronFormDeliver">${esc(t('cron_deliver_label') || 'Deliver output to')}</label>
|
||||
@@ -825,12 +834,14 @@ async function saveCronForm(){
|
||||
const prompt=promptEl.value.trim();
|
||||
const deliver=delivEl?delivEl.value:'local';
|
||||
const profile=profileEl?profileEl.value:'';
|
||||
const isNoAgent = !!(_currentCronDetail && _currentCronDetail.no_agent);
|
||||
errEl.style.display='none';
|
||||
if(!schedule){errEl.textContent=t('cron_schedule_required_example');errEl.style.display='';return;}
|
||||
if(!prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;}
|
||||
if(!isNoAgent && !prompt){errEl.textContent=t('cron_prompt_required');errEl.style.display='';return;}
|
||||
try{
|
||||
if (_editingCronId) {
|
||||
const updates = {job_id: _editingCronId, schedule, prompt, profile: profile};
|
||||
const updates = {job_id: _editingCronId, schedule, profile: profile};
|
||||
if (!isNoAgent) updates.prompt = prompt;
|
||||
if (name) updates.name = name;
|
||||
await api('/api/crons/update', {method:'POST', body: JSON.stringify(updates)});
|
||||
const editedId = _editingCronId;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""Regression coverage for issue #1820: no-agent cron edits do not require prompts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
PANELS_JS = (ROOT / "static" / "panels.js").read_text()
|
||||
|
||||
|
||||
def _function_body(name: str) -> str:
|
||||
marker = f"function {name}("
|
||||
start = PANELS_JS.find(marker)
|
||||
assert start != -1, f"{name} not found"
|
||||
paren = PANELS_JS.find("(", start)
|
||||
assert paren != -1, f"{name} params not found"
|
||||
depth = 0
|
||||
for idx in range(paren, len(PANELS_JS)):
|
||||
ch = PANELS_JS[idx]
|
||||
if ch == "(":
|
||||
depth += 1
|
||||
elif ch == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
brace = PANELS_JS.find("{", idx)
|
||||
break
|
||||
else:
|
||||
raise AssertionError(f"{name} params did not terminate")
|
||||
assert brace != -1, f"{name} body not found"
|
||||
depth = 0
|
||||
for idx in range(brace, len(PANELS_JS)):
|
||||
ch = PANELS_JS[idx]
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return PANELS_JS[brace + 1 : idx]
|
||||
raise AssertionError(f"{name} body did not terminate")
|
||||
|
||||
|
||||
def test_open_cron_edit_plumbs_no_agent_and_script_to_form():
|
||||
body = _function_body("openCronEdit")
|
||||
assert "no_agent: !!job.no_agent" in body
|
||||
assert "script: job.script || ''" in body
|
||||
|
||||
|
||||
def test_no_agent_form_drops_prompt_required_attribute_and_shows_script_context():
|
||||
body = _function_body("_renderCronForm")
|
||||
assert "no_agent" in body and "script" in body
|
||||
assert "const isNoAgent = !!no_agent;" in body
|
||||
assert "cron-no-agent-hint" in body
|
||||
assert "No-agent script" in body
|
||||
assert "${isNoAgent ? ' disabled' : ' required'}" in body
|
||||
|
||||
|
||||
def test_save_cron_form_keeps_agent_prompt_required_but_skips_no_agent_edits():
|
||||
body = _function_body("saveCronForm")
|
||||
assert "const isNoAgent = !!(_currentCronDetail && _currentCronDetail.no_agent);" in body
|
||||
assert "if(!isNoAgent && !prompt)" in body
|
||||
assert "cron_prompt_required" in body
|
||||
assert "if (!isNoAgent) updates.prompt = prompt;" in body
|
||||
|
||||
|
||||
def test_no_agent_detail_displays_mode_and_script():
|
||||
body = _function_body("_renderCronDetail")
|
||||
assert "const isNoAgent = !!job.no_agent;" in body
|
||||
assert "No-agent script" in body
|
||||
assert "cronJobMode" in body
|
||||
assert "job.script" in body
|
||||
Reference in New Issue
Block a user