723 lines
34 KiB
HTML
723 lines
34 KiB
HTML
<!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 & 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
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>
|