aipackage/cezen-portal/schedules.html
2026-06-30 10:51:41 +05:30

723 lines
34 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Scheduled Jobs — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
/* ── Layout ── */
.sj-layout { display:grid; grid-template-columns:280px 1fr; min-height:calc(100vh - 64px); }
@media(max-width:900px){ .sj-layout { grid-template-columns:1fr; } }
/* ── Sidebar ── */
.sj-sidebar { border-right:1px solid var(--bdr); background:var(--navy2); display:flex; flex-direction:column; }
.sj-sidebar-header { padding:16px 14px 10px; border-bottom:1px solid var(--bdr); }
.sj-sidebar-header h3 { font-size:12px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.5px; margin:0 0 10px; }
.sj-new-btn { display:flex; align-items:center; gap:8px; width:100%; padding:9px 12px; border-radius:8px; border:1.5px dashed var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:13px; font-weight:600; color:var(--med); transition:.15s; }
.sj-new-btn:hover { border-color:var(--teal); color:var(--teal); }
.sj-job-list { flex:1; overflow-y:auto; padding:8px; }
.sj-job-item { padding:10px 12px; border-radius:8px; cursor:pointer; transition:.1s; border:1px solid transparent; margin-bottom:4px; }
.sj-job-item:hover { background:rgba(255,255,255,.03); }
.sj-job-item.active { background:rgba(13,148,136,.12); border-color:rgba(13,148,136,.4); }
.sj-job-name { font-size:13px; font-weight:600; color:var(--ink); display:flex; align-items:center; gap:6px; }
.sj-job-meta { font-size:11px; color:var(--lt); margin-top:3px; }
.sj-job-next { font-size:10px; color:var(--teal); margin-top:2px; }
/* ── Status dot ── */
.sj-dot { width:7px; height:7px; border-radius:50%; display:inline-block; flex-shrink:0; }
.sj-dot.active { background:#22C55E; }
.sj-dot.paused { background:#F59E0B; }
/* ── Main ── */
.sj-main { background:rgba(255,255,255,.03); padding:28px; overflow-y:auto; }
/* ── Empty state ── */
.sj-empty { text-align:center; padding:80px 20px; color:var(--lt); }
.sj-empty-icon { font-size:56px; margin-bottom:16px; }
.sj-empty-title { font-size:20px; font-weight:700; color:var(--ink); margin-bottom:8px; }
.sj-empty-sub { font-size:14px; max-width:420px; margin:0 auto; line-height:1.6; }
/* ── Cards ── */
.sj-card { background:var(--navy2); border:1px solid var(--bdr); border-radius:14px; padding:24px 28px; margin-bottom:20px; }
.sj-card-title { font-size:15px; font-weight:700; color:var(--ink); margin-bottom:18px; display:flex; align-items:center; gap:10px; }
.sj-field { display:flex; flex-direction:column; gap:5px; margin-bottom:14px; }
.sj-field label { font-size:11px; font-weight:700; color:var(--lt); text-transform:uppercase; letter-spacing:.4px; }
.sj-field input, .sj-field textarea, .sj-field select {
padding:9px 12px; border:1.5px solid var(--bdr); border-radius:8px;
font-family:inherit; font-size:13px; color:var(--ink); background:var(--navy2);
}
.sj-field input:focus, .sj-field textarea:focus, .sj-field select:focus { outline:none; border-color:var(--teal); }
.sj-field textarea { resize:vertical; min-height:80px; }
.sj-grid2 { display:grid; grid-template-columns:1fr 1fr; gap:16px; }
@media(max-width:640px){ .sj-grid2 { grid-template-columns:1fr; } }
/* ── Schedule type selector ── */
.sj-sched-tabs { display:flex; gap:8px; margin-bottom:14px; }
.sj-sched-tab { padding:7px 16px; border-radius:8px; border:1.5px solid var(--bdr); background:var(--navy2); cursor:pointer; font-family:inherit; font-size:12px; font-weight:700; color:var(--med); transition:.15s; }
.sj-sched-tab.active { border-color:var(--teal); background:rgba(13,148,136,.12); color:var(--teal); }
/* ── Cron presets ── */
.sj-presets { display:flex; gap:6px; flex-wrap:wrap; margin-top:8px; }
.sj-preset { padding:4px 10px; border-radius:20px; background:rgba(255,255,255,.03); border:1px solid var(--bdr); font-size:11px; font-weight:600; color:var(--med); cursor:pointer; transition:.1s; }
.sj-preset:hover { border-color:var(--teal); color:var(--teal); }
/* ── Actions ── */
.sj-actions { display:flex; gap:10px; flex-wrap:wrap; }
/* ── Run history ── */
.sj-hist-wrap { border:1px solid var(--bdr); border-radius:10px; overflow:hidden; }
table.sj-hist { width:100%; border-collapse:collapse; }
.sj-hist th { background:rgba(255,255,255,.03); padding:9px 14px; text-align:left; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; color:var(--lt); border-bottom:1px solid var(--bdr); }
.sj-hist td { padding:10px 14px; border-bottom:1px solid var(--bdr); font-size:13px; color:var(--ink); vertical-align:top; }
.sj-hist tr:last-child td { border-bottom:none; }
.sj-hist tr:hover td { background:rgba(255,255,255,.03); }
.sj-status { display:inline-block; padding:2px 9px; border-radius:20px; font-size:11px; font-weight:700; }
.sj-status.done { background:rgba(34,197,94,.15); color:#15803D; }
.sj-status.error { background:#FEE2E2; color:#B91C1C; }
.sj-status.running { background:#DBEAFE; color:#1D4ED8; }
.sj-status.pending { background:rgba(234,179,8,.15); color:#A16207; }
.sj-output-cell { max-width:420px; white-space:pre-wrap; word-break:break-word; font-size:12px; color:var(--med); max-height:80px; overflow:hidden; position:relative; cursor:pointer; }
.sj-empty-hist { text-align:center; padding:32px; color:var(--lt); font-size:13px; }
/* ── Toggle switch ── */
.sj-toggle-row { display:flex; align-items:center; gap:12px; margin-bottom:14px; }
.sj-toggle-label { font-size:13px; font-weight:600; color:var(--ink); }
.sj-toggle { position:relative; width:42px; height:24px; }
.sj-toggle input { opacity:0; width:0; height:0; }
.sj-toggle-slider { position:absolute; inset:0; background:#CBD5E1; border-radius:24px; cursor:pointer; transition:.2s; }
.sj-toggle-slider:before { content:''; position:absolute; width:18px; height:18px; border-radius:50%; background:var(--navy2); top:3px; left:3px; transition:.2s; }
.sj-toggle input:checked + .sj-toggle-slider { background:var(--teal); }
.sj-toggle input:checked + .sj-toggle-slider:before { transform:translateX(18px); }
/* ── Stats row ── */
.sj-stats { display:flex; gap:16px; margin-bottom:20px; flex-wrap:wrap; }
.sj-stat { background:var(--navy2); border:1px solid var(--bdr); border-radius:10px; padding:14px 18px; flex:1; min-width:120px; }
.sj-stat-val { font-size:22px; font-weight:800; color:var(--ink); }
.sj-stat-lbl { font-size:11px; color:var(--lt); margin-top:2px; }
/* ── Output modal ── */
.sj-modal-bg { display:none; position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:500; }
.sj-modal-bg.open { display:flex; align-items:center; justify-content:center; }
.sj-modal { background:var(--navy2); border-radius:14px; padding:28px; max-width:680px; width:90%; max-height:80vh; display:flex; flex-direction:column; }
.sj-modal-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:16px; }
.sj-modal-title { font-size:15px; font-weight:700; color:var(--ink); }
.sj-modal-close { background:none; border:none; font-size:20px; cursor:pointer; color:var(--lt); }
.sj-modal-body { overflow-y:auto; flex:1; font-size:13px; color:var(--ink); line-height:1.7; white-space:pre-wrap; word-break:break-word; }
</style>
</head>
<body>
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security & Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Audit Log</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Manager</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">Multimodal Chat</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="sj-layout">
<!-- Sidebar -->
<aside class="sj-sidebar">
<div class="sj-sidebar-header">
<h3>Scheduled Jobs</h3>
<button class="sj-new-btn" onclick="newJob()"> New Job</button>
</div>
<div class="sj-job-list" id="job-list">
<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No jobs yet</div>
</div>
</aside>
<!-- Main -->
<main class="sj-main" id="sj-main">
<!-- Empty state -->
<div class="sj-empty" id="sj-empty">
<div class="sj-empty-icon"></div>
<div class="sj-empty-title">Scheduled AI Jobs</div>
<div class="sj-empty-sub">
Run prompts or agent pipelines automatically on a schedule — every hour, every morning, or on a custom cron. Results are logged and viewable here.
</div>
<div style="margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
<button class="btn btn-primary" onclick="newJob('daily')">📅 Daily Summary</button>
<button class="btn btn-ghost" onclick="newJob('hourly')">⏱ Hourly Monitor</button>
<button class="btn btn-ghost" onclick="newJob('weekly')">📊 Weekly Report</button>
</div>
</div>
<!-- Builder (hidden until a job is selected/created) -->
<div id="sj-builder" style="display:none">
<!-- Stats row -->
<div class="sj-stats" id="sj-stats" style="display:none">
<div class="sj-stat"><div class="sj-stat-val" id="stat-runs">0</div><div class="sj-stat-lbl">Total Runs</div></div>
<div class="sj-stat"><div class="sj-stat-val" id="stat-ok">0</div><div class="sj-stat-lbl">Successful</div></div>
<div class="sj-stat"><div class="sj-stat-val" id="stat-err">0</div><div class="sj-stat-lbl">Errors</div></div>
<div class="sj-stat"><div class="sj-stat-val" id="stat-next"></div><div class="sj-stat-lbl">Next Run</div></div>
</div>
<!-- Job config card -->
<div class="sj-card">
<div class="sj-card-title">⚙️ Job Configuration</div>
<div class="sj-toggle-row">
<label class="sj-toggle">
<input type="checkbox" id="job-active" checked>
<span class="sj-toggle-slider"></span>
</label>
<span class="sj-toggle-label" id="toggle-lbl">Active — job will run on schedule</span>
</div>
<div class="sj-grid2">
<div class="sj-field">
<label>Job Name</label>
<input type="text" id="job-name" placeholder="e.g. Daily Status Summary">
</div>
<div class="sj-field">
<label>Job Type</label>
<select id="job-type" onchange="onTypeChange()">
<option value="prompt">💬 Run a Prompt</option>
<option value="agent">🤖 Run an Agent</option>
</select>
</div>
</div>
<div class="sj-field">
<label>Description (optional)</label>
<input type="text" id="job-desc" placeholder="What this job does">
</div>
<!-- Schedule -->
<div class="sj-field">
<label>Schedule</label>
<div class="sj-sched-tabs">
<button class="sj-sched-tab active" id="tab-interval" onclick="setSchedType('interval')">⏱ Interval</button>
<button class="sj-sched-tab" id="tab-cron" onclick="setSchedType('cron')">🗓 Cron</button>
</div>
<!-- Interval -->
<div id="sched-interval">
<div style="display:flex;align-items:center;gap:10px">
<span style="font-size:13px;color:var(--med)">Every</span>
<input type="number" id="interval-mins" value="60" min="1" max="10080"
style="width:90px;padding:8px 10px;border:1.5px solid var(--bdr);border-radius:8px;font-family:inherit;font-size:13px">
<span style="font-size:13px;color:var(--med)">minutes</span>
</div>
<div class="sj-presets">
<span class="sj-preset" onclick="setInterval_(15)">Every 15 min</span>
<span class="sj-preset" onclick="setInterval_(30)">Every 30 min</span>
<span class="sj-preset" onclick="setInterval_(60)">Every hour</span>
<span class="sj-preset" onclick="setInterval_(360)">Every 6 hrs</span>
<span class="sj-preset" onclick="setInterval_(1440)">Every day</span>
<span class="sj-preset" onclick="setInterval_(10080)">Every week</span>
</div>
</div>
<!-- Cron -->
<div id="sched-cron" style="display:none">
<input type="text" id="cron-expr" placeholder="e.g. 0 8 * * 1-5 (weekdays at 8am)"
style="width:100%;box-sizing:border-box;padding:9px 12px;border:1.5px solid var(--bdr);border-radius:8px;font-family:monospace;font-size:13px">
<div class="sj-presets">
<span class="sj-preset" onclick="setCron('0 8 * * *')">Daily 8am</span>
<span class="sj-preset" onclick="setCron('0 8 * * 1-5')">Weekdays 8am</span>
<span class="sj-preset" onclick="setCron('0 9 * * 1')">Mondays 9am</span>
<span class="sj-preset" onclick="setCron('0 */2 * * *')">Every 2 hrs</span>
<span class="sj-preset" onclick="setCron('0 0 * * *')">Midnight</span>
<span class="sj-preset" onclick="setCron('0 12 1 * *')">1st of month</span>
</div>
</div>
</div>
<!-- Prompt job fields -->
<div id="prompt-fields">
<div class="sj-grid2">
<div class="sj-field">
<label>Model</label>
<select id="job-model">
<option value="">Auto (default)</option>
</select>
</div>
</div>
<div class="sj-field">
<label>Prompt</label>
<textarea id="job-prompt" rows="5" placeholder="What should the AI do on each run? e.g. Summarise the system status and flag any anomalies…"></textarea>
</div>
</div>
<!-- Agent job fields -->
<div id="agent-fields" style="display:none">
<div class="sj-field">
<label>Agent</label>
<select id="job-agent" onchange="onAgentChange()">
<option value="">— Select an agent —</option>
</select>
</div>
<div class="sj-field">
<label>Input / Context for this run</label>
<textarea id="job-agent-input" rows="4" placeholder="Initial input passed to the agent on each run…"></textarea>
</div>
</div>
<div class="sj-actions">
<button class="btn btn-primary" onclick="saveJob()">💾 Save Job</button>
<button class="btn btn-ghost" onclick="triggerNow()" id="trigger-btn" style="display:none">▶ Run Now</button>
<button class="btn btn-ghost" onclick="deleteJob()" id="delete-btn" style="display:none;color:#B91C1C;border-color:#FCA5A5">🗑 Delete</button>
</div>
</div>
<!-- Run history -->
<div class="sj-card" id="hist-card" style="display:none">
<div class="sj-card-title">📋 Run History <span id="hist-count" style="font-size:12px;font-weight:400;color:var(--lt)"></span></div>
<div id="hist-body"><div class="sj-empty-hist">No runs yet</div></div>
</div>
</div>
</main>
</div>
<!-- Output modal -->
<div class="sj-modal-bg" id="output-modal" onclick="closeModal(event)">
<div class="sj-modal">
<div class="sj-modal-header">
<span class="sj-modal-title">Run Output</span>
<button class="sj-modal-close" onclick="document.getElementById('output-modal').classList.remove('open')"></button>
</div>
<div class="sj-modal-body" id="modal-content"></div>
</div>
</div>
<script>
const _API = '/api';
let jobs = [];
let agents = [];
let models = [];
let currentJobId = null;
let schedType = 'interval';
let pollTimer = null;
// ── Sidebar ───────────────────────────────────────────────────────────────────
async function loadJobList() {
try {
const res = await fetch(`${_API}/schedules`, { credentials:'include' });
jobs = await res.json();
renderSidebar();
} catch(e) {}
}
function renderSidebar() {
const el = document.getElementById('job-list');
if (!jobs.length) {
el.innerHTML = '<div style="padding:16px;font-size:12px;color:var(--lt);text-align:center">No jobs yet</div>';
return;
}
el.innerHTML = jobs.map(j => {
const active = j.is_active ? 'active' : 'paused';
const nextTxt = j.next_run_at ? `Next: ${fmtRelative(j.next_run_at)}` : '';
return `<div class="sj-job-item${j.id === currentJobId ? ' active':''}" onclick="selectJob(${j.id})">
<div class="sj-job-name">
<span class="sj-dot ${active}"></span>
${esc(j.name)}
</div>
<div class="sj-job-meta">${j.job_type === 'agent' ? '🤖 Agent' : '💬 Prompt'} · ${fmtSched(j)}</div>
${nextTxt ? `<div class="sj-job-next">${nextTxt}</div>` : ''}
</div>`;
}).join('');
}
// ── Select / New ──────────────────────────────────────────────────────────────
function newJob(template) {
currentJobId = null;
document.getElementById('sj-empty').style.display = 'none';
document.getElementById('sj-builder').style.display = '';
document.getElementById('sj-stats').style.display = 'none';
document.getElementById('hist-card').style.display = 'none';
document.getElementById('trigger-btn').style.display = 'none';
document.getElementById('delete-btn').style.display = 'none';
clearForm();
if (template === 'daily') {
document.getElementById('job-name').value = 'Daily Summary';
document.getElementById('job-desc').value = 'Runs every morning at 8am';
setCron('0 8 * * *');
setSchedType('cron');
document.getElementById('job-prompt').value = 'Write a brief status summary for today. Note any system alerts or items needing attention.';
} else if (template === 'hourly') {
document.getElementById('job-name').value = 'Hourly Monitor';
setInterval_(60);
document.getElementById('job-prompt').value = 'Briefly check: are there any anomalies or issues that need immediate attention?';
} else if (template === 'weekly') {
document.getElementById('job-name').value = 'Weekly Report';
document.getElementById('job-desc').value = 'Generates a weekly summary every Monday';
setCron('0 9 * * 1');
setSchedType('cron');
document.getElementById('job-prompt').value = 'Generate a weekly AI usage report. Summarise activity, highlight any trends, and suggest improvements.';
}
renderSidebar();
}
async function selectJob(id) {
currentJobId = id;
const job = jobs.find(j => j.id === id);
if (!job) return;
document.getElementById('sj-empty').style.display = 'none';
document.getElementById('sj-builder').style.display = '';
document.getElementById('sj-stats').style.display = '';
document.getElementById('hist-card').style.display = '';
document.getElementById('trigger-btn').style.display = '';
document.getElementById('delete-btn').style.display = '';
renderSidebar();
// Fill form
document.getElementById('job-name').value = job.name;
document.getElementById('job-desc').value = job.description || '';
document.getElementById('job-active').checked = !!job.is_active;
updateToggleLabel();
document.getElementById('job-type').value = job.job_type;
onTypeChange();
if (job.schedule_type === 'cron') {
setSchedType('cron');
document.getElementById('cron-expr').value = job.schedule_val;
} else {
setSchedType('interval');
document.getElementById('interval-mins').value = job.schedule_val;
}
if (job.job_type === 'agent') {
document.getElementById('job-agent').value = job.agent_id || '';
document.getElementById('job-agent-input').value = job.prompt_text;
} else {
document.getElementById('job-model').value = job.model || '';
document.getElementById('job-prompt').value = job.prompt_text;
}
// Stats
document.getElementById('stat-runs').textContent = job.run_count || 0;
document.getElementById('stat-next').textContent = job.next_run_at ? fmtRelative(job.next_run_at) : '—';
await loadHistory(id);
}
function clearForm() {
document.getElementById('job-name').value = '';
document.getElementById('job-desc').value = '';
document.getElementById('job-active').checked = true;
document.getElementById('job-type').value = 'prompt';
document.getElementById('job-model').value = '';
document.getElementById('job-prompt').value = '';
document.getElementById('job-agent').value = '';
document.getElementById('job-agent-input').value = '';
document.getElementById('interval-mins').value = '60';
document.getElementById('cron-expr').value = '';
setSchedType('interval');
onTypeChange();
updateToggleLabel();
}
// ── Schedule type ─────────────────────────────────────────────────────────────
function setSchedType(type) {
schedType = type;
document.getElementById('sched-interval').style.display = type === 'interval' ? '' : 'none';
document.getElementById('sched-cron').style.display = type === 'cron' ? '' : 'none';
document.getElementById('tab-interval').className = 'sj-sched-tab' + (type === 'interval' ? ' active' : '');
document.getElementById('tab-cron').className = 'sj-sched-tab' + (type === 'cron' ? ' active' : '');
}
function setInterval_(mins) { document.getElementById('interval-mins').value = mins; }
function setCron(expr) {
setSchedType('cron');
document.getElementById('cron-expr').value = expr;
}
// ── Job type ──────────────────────────────────────────────────────────────────
function onTypeChange() {
const type = document.getElementById('job-type').value;
document.getElementById('prompt-fields').style.display = type === 'prompt' ? '' : 'none';
document.getElementById('agent-fields').style.display = type === 'agent' ? '' : 'none';
}
function onAgentChange() {
const sel = document.getElementById('job-agent');
const ag = agents.find(a => a.id == sel.value);
if (ag && !document.getElementById('job-agent-input').value) {
document.getElementById('job-agent-input').value = `Run agent: ${ag.name}`;
}
}
// ── Toggle ────────────────────────────────────────────────────────────────────
document.getElementById('job-active').addEventListener('change', updateToggleLabel);
function updateToggleLabel() {
const on = document.getElementById('job-active').checked;
document.getElementById('toggle-lbl').textContent = on
? 'Active — job will run on schedule'
: 'Paused — job is disabled';
}
// ── Save ──────────────────────────────────────────────────────────────────────
async function saveJob() {
const name = document.getElementById('job-name').value.trim();
if (!name) { alert('Please enter a job name.'); return; }
const jobType = document.getElementById('job-type').value;
const schedVal = schedType === 'cron'
? document.getElementById('cron-expr').value.trim()
: document.getElementById('interval-mins').value.trim();
if (!schedVal) { alert('Please set a schedule.'); return; }
const body = {
name,
description: document.getElementById('job-desc').value.trim(),
job_type: jobType,
schedule_type: schedType,
schedule_val: schedVal,
prompt_text: jobType === 'agent'
? document.getElementById('job-agent-input').value.trim()
: document.getElementById('job-prompt').value.trim(),
agent_id: jobType === 'agent' ? (parseInt(document.getElementById('job-agent').value) || null) : null,
agent_name: jobType === 'agent'
? (document.getElementById('job-agent').options[document.getElementById('job-agent').selectedIndex]?.text || '')
: '',
model: document.getElementById('job-model').value,
is_active: document.getElementById('job-active').checked ? 1 : 0,
};
try {
let res;
if (currentJobId) {
res = await fetch(`${_API}/schedules/${currentJobId}`, {
method:'PUT', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
});
} else {
res = await fetch(`${_API}/schedules`, {
method:'POST', credentials:'include',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body)
});
}
if (!res.ok) throw new Error((await res.json()).detail || 'Save failed');
const saved = await res.json();
currentJobId = saved.id;
await loadJobList();
renderSidebar();
document.getElementById('trigger-btn').style.display = '';
document.getElementById('delete-btn').style.display = '';
document.getElementById('sj-stats').style.display = '';
document.getElementById('stat-next').textContent = saved.next_run_at ? fmtRelative(saved.next_run_at) : '—';
} catch(e) {
alert('Save failed: ' + e.message);
}
}
// ── Trigger Now ───────────────────────────────────────────────────────────────
async function triggerNow() {
if (!currentJobId) return;
const btn = document.getElementById('trigger-btn');
btn.disabled = true;
btn.textContent = '⏳ Running…';
try {
const res = await fetch(`${_API}/schedules/${currentJobId}/run`, {
method:'POST', credentials:'include'
});
if (!res.ok) throw new Error((await res.json()).detail || 'Failed');
// Poll for new run to appear
await new Promise(r => setTimeout(r, 1500));
await loadHistory(currentJobId);
await loadJobList();
} catch(e) {
alert('Trigger failed: ' + e.message);
}
btn.disabled = false;
btn.textContent = '▶ Run Now';
}
// ── Delete ────────────────────────────────────────────────────────────────────
async function deleteJob() {
if (!currentJobId) return;
if (!confirm('Delete this scheduled job and all its run history?')) return;
try {
await fetch(`${_API}/schedules/${currentJobId}`, { method:'DELETE', credentials:'include' });
currentJobId = null;
document.getElementById('sj-empty').style.display = '';
document.getElementById('sj-builder').style.display = 'none';
await loadJobList();
} catch(e) { alert('Delete failed'); }
}
// ── History ───────────────────────────────────────────────────────────────────
async function loadHistory(jobId) {
try {
const res = await fetch(`${_API}/schedules/${jobId}/runs`, { credentials:'include' });
const runs = await res.json();
const el = document.getElementById('hist-body');
const countEl = document.getElementById('hist-count');
const ok = runs.filter(r => r.status === 'done').length;
const err = runs.filter(r => r.status === 'error').length;
document.getElementById('stat-ok').textContent = ok;
document.getElementById('stat-err').textContent = err;
countEl.textContent = runs.length ? `(${runs.length} runs)` : '';
if (!runs.length) {
el.innerHTML = '<div class="sj-empty-hist">No runs yet — save the job and click ▶ Run Now to test it</div>';
return;
}
el.innerHTML = `<div class="sj-hist-wrap"><table class="sj-hist">
<thead><tr>
<th>When</th><th>Status</th><th>Duration</th><th>Output</th>
</tr></thead>
<tbody>
${runs.map(r => {
const dur = r.finished_at
? Math.round((new Date(r.finished_at) - new Date(r.started_at)) / 1000) + 's'
: '—';
const preview = (r.output || r.error_msg || '').slice(0, 120);
return `<tr>
<td style="color:var(--lt);font-size:12px;white-space:nowrap">${new Date(r.started_at).toLocaleString()}</td>
<td><span class="sj-status ${r.status}">${r.status}</span></td>
<td style="color:var(--lt);font-size:12px">${dur}</td>
<td class="sj-output-cell" onclick="showOutput(${JSON.stringify(esc(r.output || r.error_msg || '')).replace(/\\\\/g, '\\\\\\\\')})">${esc(preview)}${preview.length >= 120 ? '<span style="color:var(--teal)">… see more</span>' : ''}</td>
</tr>`;
}).join('')}
</tbody>
</table></div>`;
} catch(e) {}
}
// ── Output modal ──────────────────────────────────────────────────────────────
function showOutput(text) {
document.getElementById('modal-content').textContent = text;
document.getElementById('output-modal').classList.add('open');
}
function closeModal(e) {
if (e.target.id === 'output-modal') document.getElementById('output-modal').classList.remove('open');
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function esc(s) {
return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function fmtSched(j) {
if (j.schedule_type === 'cron') return `cron: ${j.schedule_val}`;
const mins = parseInt(j.schedule_val);
if (mins >= 1440) return `every ${mins/1440|0}d`;
if (mins >= 60) return `every ${mins/60|0}h`;
return `every ${mins}m`;
}
function fmtRelative(iso) {
const diff = new Date(iso) - Date.now();
if (diff < 0) return 'overdue';
const m = Math.round(diff / 60000);
if (m < 1) return 'in <1 min';
if (m < 60) return `in ${m} min`;
const h = Math.round(m / 60);
if (h < 24) return `in ${h}h`;
return `in ${Math.round(h/24)}d`;
}
async function loadModels() {
try {
const res = await fetch(`${_API}/models/list`, { credentials:'include' });
const data = await res.json();
models = (data.models || []).map(m => m.name);
const sel = document.getElementById('job-model');
sel.innerHTML = '<option value="">Auto (default)</option>' +
models.map(m => `<option value="${esc(m)}">${esc(m)}</option>`).join('');
} catch(e) {}
}
async function loadAgents() {
try {
const res = await fetch(`${_API}/agents`, { credentials:'include' });
agents = await res.json();
const sel = document.getElementById('job-agent');
sel.innerHTML = '<option value="">— Select an agent —</option>' +
agents.map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
} catch(e) {}
}
// ── Boot ──────────────────────────────────────────────────────────────────────
(async () => {
await Promise.all([loadModels(), loadAgents(), loadJobList()]);
})();
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>