mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 22:19:42 +02:00
feat(scheduler): add Scheduled Jobs UI
- scheduler-ui.js: job list + create/edit form + Run Now/Enable/Disable/Delete, reacting to scheduler:* SSE events; same-agent Run Now warning - index.html: "⏰ Schedules" toolbar button + #schedulerModal + script include - constants.js / app.js: frontend SSE event constants + handler map entries - styles.css: scheduler row/badge/form styles Follows Codeman's vanilla-JS mixin + .modal/.form-row conventions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rp7JhmQXcYJhmxFMdZuuah
This commit is contained in:
@@ -165,6 +165,12 @@ const _SSE_HANDLER_MAP = [
|
||||
[SSE_EVENTS.SCHEDULED_COMPLETED, '_onScheduledCompleted'],
|
||||
[SSE_EVENTS.SCHEDULED_STOPPED, '_onScheduledStopped'],
|
||||
|
||||
// Scheduled jobs (cron-style scheduler)
|
||||
[SSE_EVENTS.SCHEDULER_JOBS_CHANGED, '_onSchedulerJobsChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_JOB_DELETED, '_onSchedulerJobsChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_RUN_CREATED, '_onSchedulerRunChanged'],
|
||||
[SSE_EVENTS.SCHEDULER_RUN_UPDATED, '_onSchedulerRunChanged'],
|
||||
|
||||
// Respawn
|
||||
[SSE_EVENTS.RESPAWN_STARTED, '_onRespawnStarted'],
|
||||
[SSE_EVENTS.RESPAWN_STOPPED, '_onRespawnStopped'],
|
||||
|
||||
@@ -276,6 +276,12 @@ const SSE_EVENTS = {
|
||||
SCHEDULED_LOG: 'scheduled:log',
|
||||
SCHEDULED_DELETED: 'scheduled:deleted',
|
||||
|
||||
// Scheduled jobs (cron-style scheduler)
|
||||
SCHEDULER_JOBS_CHANGED: 'scheduler:jobsChanged',
|
||||
SCHEDULER_JOB_DELETED: 'scheduler:jobDeleted',
|
||||
SCHEDULER_RUN_CREATED: 'scheduler:runCreated',
|
||||
SCHEDULER_RUN_UPDATED: 'scheduler:runUpdated',
|
||||
|
||||
// Respawn
|
||||
RESPAWN_STARTED: 'respawn:started',
|
||||
RESPAWN_STOPPED: 'respawn:stopped',
|
||||
|
||||
@@ -541,6 +541,7 @@
|
||||
<div class="toolbar-right">
|
||||
<!-- Orchestrator button hidden until feature is ready -->
|
||||
<!-- <button class="btn-toolbar btn-sm" onclick="app.toggleOrchestratorPanel()" title="Orchestrator Loop">⚙ Orchestrator</button> -->
|
||||
<button class="btn-toolbar btn-sm" onclick="app.openScheduler()" title="Scheduled Jobs">⏰ Schedules</button>
|
||||
<span class="version-display" id="versionDisplay" title="Codeman version">v0.0.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -570,6 +571,95 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Jobs Modal (cron-style scheduler) -->
|
||||
<div class="modal" id="schedulerModal">
|
||||
<div class="modal-backdrop" onclick="app.closeScheduler()"></div>
|
||||
<div class="modal-content modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Scheduled Jobs</h3>
|
||||
<button class="modal-close" onclick="app.closeScheduler()" aria-label="Close scheduler">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="form-hint" style="margin-bottom:10px;">Times use the server's local timezone.</p>
|
||||
<div style="margin-bottom:10px;">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.openSchedulerJobForm()">+ New Job</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.refreshScheduler()">Refresh</button>
|
||||
</div>
|
||||
<!-- Job list -->
|
||||
<div id="schedulerJobList" class="scheduler-job-list"></div>
|
||||
|
||||
<!-- Create/Edit form (hidden until New/Edit) -->
|
||||
<div id="schedulerJobForm" class="scheduler-job-form hidden">
|
||||
<div class="form-section-header" id="schedulerFormTitle">New Scheduled Job</div>
|
||||
<input type="hidden" id="schJobId">
|
||||
<div class="form-row"><label>Name</label><input type="text" id="schName" placeholder="My nightly job"></div>
|
||||
<div class="form-row"><label>Agent Type</label>
|
||||
<select id="schAgentType">
|
||||
<option value="claude">Claude</option>
|
||||
<option value="shell">Terminal / Shell</option>
|
||||
<option value="opencode">OpenCode</option>
|
||||
<option value="codex">Codex</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row"><label>Working Directory</label><input type="text" id="schWorkingDir" placeholder="/absolute/path"></div>
|
||||
<div class="form-row"><label>Prompt Source</label>
|
||||
<select id="schPromptMode" onchange="app.onSchedulerPromptModeChange()">
|
||||
<option value="inline_text">Inline text</option>
|
||||
<option value="prompt_file_path">Prompt file path</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" id="schPromptTextRow"><label>Prompt</label><textarea id="schPromptText" rows="4" placeholder="Prompt to send into the session"></textarea></div>
|
||||
<div class="form-row hidden" id="schPromptFileRow"><label>Prompt File Path</label><input type="text" id="schPromptFilePath" placeholder="/absolute/path/to/prompt.md"></div>
|
||||
<div class="form-row"><label>Input Mode</label>
|
||||
<select id="schInputMode">
|
||||
<option value="typed">Typed (via tmux)</option>
|
||||
<option value="paste">Paste (direct)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row"><label>Schedule Type</label>
|
||||
<select id="schScheduleType" onchange="app.onSchedulerScheduleTypeChange()">
|
||||
<option value="once">Once</option>
|
||||
<option value="interval">Interval</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" id="schRunAtRow"><label>Run At</label><input type="datetime-local" id="schRunAt"></div>
|
||||
<div class="form-row hidden" id="schIntervalRow"><label>Every (minutes)</label><input type="number" id="schIntervalMinutes" min="1" value="60"></div>
|
||||
<div class="form-row hidden" id="schDailyRow"><label>Daily Time (HH:MM)</label><input type="time" id="schDailyTime"></div>
|
||||
<div class="form-row hidden" id="schWeeklyDaysRow"><label>Weekdays</label>
|
||||
<span id="schWeeklyDays" class="scheduler-weekdays">
|
||||
<label><input type="checkbox" value="0">Sun</label>
|
||||
<label><input type="checkbox" value="1">Mon</label>
|
||||
<label><input type="checkbox" value="2">Tue</label>
|
||||
<label><input type="checkbox" value="3">Wed</label>
|
||||
<label><input type="checkbox" value="4">Thu</label>
|
||||
<label><input type="checkbox" value="5">Fri</label>
|
||||
<label><input type="checkbox" value="6">Sat</label>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-row hidden" id="schWeeklyTimeRow"><label>Weekly Time (HH:MM)</label><input type="time" id="schWeeklyTime"></div>
|
||||
<div class="form-row"><label>On auto-run, if same agent running</label>
|
||||
<select id="schConcurrencyPolicy">
|
||||
<option value="warn_only">Run anyway</option>
|
||||
<option value="skip_if_same_agent_running">Skip this run</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row form-row-switch"><label>Enabled</label>
|
||||
<label class="switch"><input type="checkbox" id="schEnabled" checked><span class="slider"></span></label>
|
||||
</div>
|
||||
<div class="form-row"><label>Notes</label><input type="text" id="schNotes" placeholder="Optional"></div>
|
||||
<div id="schedulerFormError" class="form-hint" style="color:var(--danger,#e55);"></div>
|
||||
<div style="margin-top:10px;">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.saveSchedulerJob()">Save</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.cancelSchedulerJobForm()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monitor Panel (Tmux Sessions + Background Tasks) -->
|
||||
<div class="monitor-panel" id="monitorPanel">
|
||||
<div class="monitor-panel-header" id="monitorPanelHeader">
|
||||
@@ -2056,6 +2146,7 @@
|
||||
<script defer src="respawn-ui.js"></script>
|
||||
<script defer src="ralph-panel.js"></script>
|
||||
<script defer src="orchestrator-panel.js"></script>
|
||||
<script defer src="scheduler-ui.js"></script>
|
||||
<script defer src="settings-ui.js"></script>
|
||||
<script defer src="panels-ui.js"></script>
|
||||
<script defer src="ultracode-panel.js"></script>
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* @fileoverview Scheduled Jobs UI (cron-style scheduler) mixed into
|
||||
* CodemanApp.prototype. Renders the job list + create/edit form in the
|
||||
* #schedulerModal, and reacts to scheduler:* SSE events.
|
||||
*
|
||||
* @mixin Extends CodemanApp.prototype via Object.assign
|
||||
* @dependency app.js, api-client.js, constants.js (escapeHtml)
|
||||
*/
|
||||
|
||||
Object.assign(CodemanApp.prototype, {
|
||||
// ── SSE handlers ──────────────────────────────────────────────────────────
|
||||
|
||||
_onSchedulerJobsChanged(data) {
|
||||
if (data && Array.isArray(data.jobs)) {
|
||||
this._schedulerJobs = data.jobs;
|
||||
if (this._isSchedulerOpen()) this.renderSchedulerJobs();
|
||||
} else if (this._isSchedulerOpen()) {
|
||||
this.refreshScheduler();
|
||||
}
|
||||
},
|
||||
|
||||
_onSchedulerRunChanged() {
|
||||
// A run's status changed — refresh the list so lastStatus stays current.
|
||||
if (this._isSchedulerOpen()) this.refreshScheduler();
|
||||
},
|
||||
|
||||
// ── Modal open/close ──────────────────────────────────────────────────────
|
||||
|
||||
_isSchedulerOpen() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
return !!el && el.classList.contains('active');
|
||||
},
|
||||
|
||||
openScheduler() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
if (!el) return;
|
||||
el.classList.add('active');
|
||||
this.cancelSchedulerJobForm();
|
||||
this.refreshScheduler();
|
||||
},
|
||||
|
||||
closeScheduler() {
|
||||
const el = document.getElementById('schedulerModal');
|
||||
if (el) el.classList.remove('active');
|
||||
},
|
||||
|
||||
async refreshScheduler() {
|
||||
const jobs = await this._apiJson('/api/scheduler/jobs');
|
||||
this._schedulerJobs = Array.isArray(jobs) ? jobs : [];
|
||||
this.renderSchedulerJobs();
|
||||
},
|
||||
|
||||
// ── List rendering ────────────────────────────────────────────────────────
|
||||
|
||||
renderSchedulerJobs() {
|
||||
const list = document.getElementById('schedulerJobList');
|
||||
if (!list) return;
|
||||
const jobs = this._schedulerJobs || [];
|
||||
if (jobs.length === 0) {
|
||||
list.innerHTML = '<div class="form-hint">No scheduled jobs yet. Click “+ New Job”.</div>';
|
||||
return;
|
||||
}
|
||||
const rows = jobs.map((j) => {
|
||||
const next = j.enabled ? this._fmtTime(j.nextRunAt) : '—';
|
||||
const last = this._fmtTime(j.lastRunAt);
|
||||
const status = j.lastStatus ? escapeHtml(j.lastStatus) : '—';
|
||||
return `
|
||||
<div class="scheduler-job-row">
|
||||
<div class="scheduler-job-main">
|
||||
<div class="scheduler-job-name">${escapeHtml(j.name || '(unnamed)')}
|
||||
<span class="scheduler-badge">${escapeHtml(j.agentType)}</span>
|
||||
<span class="scheduler-badge">${escapeHtml(this._fmtSchedule(j))}</span>
|
||||
${j.enabled ? '' : '<span class="scheduler-badge scheduler-badge-off">disabled</span>'}
|
||||
</div>
|
||||
<div class="scheduler-job-meta">
|
||||
<span title="${escapeHtml(j.workingDir || '')}">${escapeHtml(j.workingDir || '')}</span>
|
||||
· next: ${escapeHtml(next)} · last: ${escapeHtml(last)} · status: ${status}
|
||||
</div>
|
||||
</div>
|
||||
<div class="scheduler-job-actions">
|
||||
<button class="btn-toolbar btn-sm btn-primary" onclick="app.runSchedulerJob('${j.id}')">Run Now</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.toggleSchedulerJob('${j.id}', ${j.enabled ? 'false' : 'true'})">${j.enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn-toolbar btn-sm" onclick="app.editSchedulerJob('${j.id}')">Edit</button>
|
||||
<button class="btn-toolbar btn-sm btn-danger" onclick="app.deleteSchedulerJob('${j.id}')">Delete</button>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
list.innerHTML = rows.join('');
|
||||
},
|
||||
|
||||
_fmtTime(ts) {
|
||||
if (!ts) return '—';
|
||||
try {
|
||||
return new Date(ts).toLocaleString();
|
||||
} catch {
|
||||
return '—';
|
||||
}
|
||||
},
|
||||
|
||||
_fmtSchedule(j) {
|
||||
switch (j.scheduleType) {
|
||||
case 'once':
|
||||
return 'once';
|
||||
case 'interval':
|
||||
return `every ${j.intervalMinutes}m`;
|
||||
case 'daily':
|
||||
return `daily ${j.dailyTime || ''}`;
|
||||
case 'weekly': {
|
||||
const names = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
const days = (j.weeklyDays || []).map((d) => names[d] || d).join(',');
|
||||
return `weekly ${days} ${j.weeklyTime || ''}`;
|
||||
}
|
||||
default:
|
||||
return j.scheduleType || '';
|
||||
}
|
||||
},
|
||||
|
||||
// ── Create / edit form ────────────────────────────────────────────────────
|
||||
|
||||
openSchedulerJobForm(job) {
|
||||
const form = document.getElementById('schedulerJobForm');
|
||||
if (!form) return;
|
||||
document.getElementById('schedulerFormError').textContent = '';
|
||||
document.getElementById('schedulerFormTitle').textContent = job ? 'Edit Scheduled Job' : 'New Scheduled Job';
|
||||
document.getElementById('schJobId').value = job ? job.id : '';
|
||||
document.getElementById('schName').value = job ? job.name || '' : '';
|
||||
document.getElementById('schAgentType').value = job ? job.agentType || 'claude' : 'claude';
|
||||
document.getElementById('schWorkingDir').value = job ? job.workingDir || '' : '';
|
||||
document.getElementById('schPromptMode').value = job ? job.promptMode || 'inline_text' : 'inline_text';
|
||||
document.getElementById('schPromptText').value = job ? job.promptText || '' : '';
|
||||
document.getElementById('schPromptFilePath').value = job ? job.promptFilePath || '' : '';
|
||||
document.getElementById('schInputMode').value = job ? job.inputMode || 'typed' : 'typed';
|
||||
document.getElementById('schScheduleType').value = job ? job.scheduleType || 'once' : 'once';
|
||||
document.getElementById('schRunAt').value = job && job.runAt ? this._toLocalInput(job.runAt) : '';
|
||||
document.getElementById('schIntervalMinutes').value = job && job.intervalMinutes ? job.intervalMinutes : 60;
|
||||
document.getElementById('schDailyTime').value = job ? job.dailyTime || '' : '';
|
||||
document.getElementById('schWeeklyTime').value = job ? job.weeklyTime || '' : '';
|
||||
const weekly = (job && job.weeklyDays) || [];
|
||||
document.querySelectorAll('#schWeeklyDays input[type=checkbox]').forEach((cb) => {
|
||||
cb.checked = weekly.includes(Number(cb.value));
|
||||
});
|
||||
document.getElementById('schConcurrencyPolicy').value = job ? job.concurrencyPolicy || 'warn_only' : 'warn_only';
|
||||
document.getElementById('schEnabled').checked = job ? !!job.enabled : true;
|
||||
document.getElementById('schNotes').value = job ? job.notes || '' : '';
|
||||
|
||||
this.onSchedulerPromptModeChange();
|
||||
this.onSchedulerScheduleTypeChange();
|
||||
form.classList.remove('hidden');
|
||||
},
|
||||
|
||||
editSchedulerJob(id) {
|
||||
const job = (this._schedulerJobs || []).find((j) => j.id === id);
|
||||
if (job) this.openSchedulerJobForm(job);
|
||||
},
|
||||
|
||||
cancelSchedulerJobForm() {
|
||||
const form = document.getElementById('schedulerJobForm');
|
||||
if (form) form.classList.add('hidden');
|
||||
},
|
||||
|
||||
onSchedulerPromptModeChange() {
|
||||
const mode = document.getElementById('schPromptMode').value;
|
||||
document.getElementById('schPromptTextRow').classList.toggle('hidden', mode !== 'inline_text');
|
||||
document.getElementById('schPromptFileRow').classList.toggle('hidden', mode !== 'prompt_file_path');
|
||||
},
|
||||
|
||||
onSchedulerScheduleTypeChange() {
|
||||
const t = document.getElementById('schScheduleType').value;
|
||||
document.getElementById('schRunAtRow').classList.toggle('hidden', t !== 'once');
|
||||
document.getElementById('schIntervalRow').classList.toggle('hidden', t !== 'interval');
|
||||
document.getElementById('schDailyRow').classList.toggle('hidden', t !== 'daily');
|
||||
document.getElementById('schWeeklyDaysRow').classList.toggle('hidden', t !== 'weekly');
|
||||
document.getElementById('schWeeklyTimeRow').classList.toggle('hidden', t !== 'weekly');
|
||||
},
|
||||
|
||||
_toLocalInput(ts) {
|
||||
// epoch-ms → 'YYYY-MM-DDTHH:MM' in local time for <input datetime-local>.
|
||||
const d = new Date(ts);
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
},
|
||||
|
||||
_collectSchedulerForm() {
|
||||
const t = document.getElementById('schScheduleType').value;
|
||||
const promptMode = document.getElementById('schPromptMode').value;
|
||||
const body = {
|
||||
name: document.getElementById('schName').value.trim(),
|
||||
agentType: document.getElementById('schAgentType').value,
|
||||
workingDir: document.getElementById('schWorkingDir').value.trim(),
|
||||
promptMode,
|
||||
inputMode: document.getElementById('schInputMode').value,
|
||||
scheduleType: t,
|
||||
concurrencyPolicy: document.getElementById('schConcurrencyPolicy').value,
|
||||
enabled: document.getElementById('schEnabled').checked,
|
||||
notes: document.getElementById('schNotes').value.trim() || undefined,
|
||||
};
|
||||
if (promptMode === 'inline_text') body.promptText = document.getElementById('schPromptText').value;
|
||||
else body.promptFilePath = document.getElementById('schPromptFilePath').value.trim();
|
||||
|
||||
if (t === 'once') {
|
||||
const v = document.getElementById('schRunAt').value;
|
||||
body.runAt = v ? new Date(v).getTime() : undefined;
|
||||
} else if (t === 'interval') {
|
||||
body.intervalMinutes = Number(document.getElementById('schIntervalMinutes').value);
|
||||
} else if (t === 'daily') {
|
||||
body.dailyTime = document.getElementById('schDailyTime').value;
|
||||
} else if (t === 'weekly') {
|
||||
body.weeklyTime = document.getElementById('schWeeklyTime').value;
|
||||
body.weeklyDays = Array.from(document.querySelectorAll('#schWeeklyDays input:checked')).map((cb) =>
|
||||
Number(cb.value)
|
||||
);
|
||||
}
|
||||
return body;
|
||||
},
|
||||
|
||||
async saveSchedulerJob() {
|
||||
const errEl = document.getElementById('schedulerFormError');
|
||||
errEl.textContent = '';
|
||||
const body = this._collectSchedulerForm();
|
||||
if (!body.name) {
|
||||
errEl.textContent = 'Name is required.';
|
||||
return;
|
||||
}
|
||||
if (!body.workingDir) {
|
||||
errEl.textContent = 'Working directory is required.';
|
||||
return;
|
||||
}
|
||||
const id = document.getElementById('schJobId').value;
|
||||
const res = id
|
||||
? await this._apiPut(`/api/scheduler/jobs/${id}`, body)
|
||||
: await this._apiPost('/api/scheduler/jobs', body);
|
||||
if (!res || !res.ok) {
|
||||
let msg = 'Failed to save job.';
|
||||
try {
|
||||
const j = await res.json();
|
||||
if (j && j.error) msg = j.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
errEl.textContent = msg;
|
||||
return;
|
||||
}
|
||||
this.showToast?.(id ? 'Scheduled job updated' : 'Scheduled job created', 'success');
|
||||
this.cancelSchedulerJobForm();
|
||||
this.refreshScheduler();
|
||||
},
|
||||
|
||||
// ── Actions ───────────────────────────────────────────────────────────────
|
||||
|
||||
async runSchedulerJob(id) {
|
||||
const job = (this._schedulerJobs || []).find((j) => j.id === id);
|
||||
if (job) {
|
||||
const active = this._countActiveAgents(job.agentType);
|
||||
if (active > 0 && !confirm(`${active} ${job.agentType} session(s) already active. Run this job anyway?`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const res = await this._apiPost(`/api/scheduler/jobs/${id}/run`, {});
|
||||
if (res && res.ok) {
|
||||
this.showToast?.('Run started — opening session', 'success');
|
||||
let data = null;
|
||||
try {
|
||||
data = await res.json();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const run = data && (data.data ? data.data.run : data.run);
|
||||
if (run && run.sessionId) this._focusScheduledSession(run.sessionId);
|
||||
this.refreshScheduler();
|
||||
} else {
|
||||
this.showToast?.('Failed to run job', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
_focusScheduledSession(sessionId) {
|
||||
// Best-effort: switch to the created session tab if it exists.
|
||||
if (this.sessions && this.sessions.has(sessionId) && typeof this.switchSession === 'function') {
|
||||
this.closeScheduler();
|
||||
this.switchSession(sessionId);
|
||||
}
|
||||
},
|
||||
|
||||
_countActiveAgents(agentType) {
|
||||
if (!this.sessions) return 0;
|
||||
let n = 0;
|
||||
for (const s of this.sessions.values()) if (s && s.mode === agentType) n++;
|
||||
return n;
|
||||
},
|
||||
|
||||
async toggleSchedulerJob(id, enabled) {
|
||||
const res = await this._apiPut(`/api/scheduler/jobs/${id}/enabled`, { enabled });
|
||||
if (res && res.ok) this.refreshScheduler();
|
||||
else this.showToast?.('Failed to update job', 'error');
|
||||
},
|
||||
|
||||
async deleteSchedulerJob(id) {
|
||||
if (!confirm('Delete this scheduled job and its run history?')) return;
|
||||
const res = await this._apiDelete(`/api/scheduler/jobs/${id}`);
|
||||
if (res && res.ok) {
|
||||
this.showToast?.('Scheduled job deleted', 'success');
|
||||
this.refreshScheduler();
|
||||
} else {
|
||||
this.showToast?.('Failed to delete job', 'error');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -11071,3 +11071,25 @@ html[data-skin="daylight-blue"] .welcome-btn-tunnel.active:hover {
|
||||
background: linear-gradient(135deg, #7c3aed, #8b5cf6);
|
||||
box-shadow: 0 0 28px -4px rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* ── Scheduled Jobs (cron-style scheduler) ───────────────────────────────── */
|
||||
.scheduler-job-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.scheduler-job-row {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||||
padding: 10px 12px; border: 1px solid var(--border, #333); border-radius: 8px;
|
||||
background: var(--panel-bg, rgba(255,255,255,0.02));
|
||||
}
|
||||
.scheduler-job-main { min-width: 0; flex: 1; }
|
||||
.scheduler-job-name { font-weight: 600; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.scheduler-job-meta { font-size: 12px; opacity: 0.7; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.scheduler-job-actions { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.scheduler-badge {
|
||||
font-size: 11px; padding: 1px 6px; border-radius: 10px;
|
||||
background: var(--accent-bg, rgba(120,160,255,0.15)); opacity: 0.9;
|
||||
}
|
||||
.scheduler-badge-off { background: rgba(200,80,80,0.18); }
|
||||
.scheduler-weekdays { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
.scheduler-weekdays label { display: inline-flex; align-items: center; gap: 3px; font-weight: 400; }
|
||||
.scheduler-job-form {
|
||||
margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border, #333);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user