aipackage/cezen-portal/schedules.html
Jino Jose b3eec34158 Sync cezen-portal/ from live working copy (V1.0 GA polish + search-icon ordering fix)
Brings the ISO-packaged portal copy current with the live cezen-portal/
working directory, which had drifted across the entire V1.0 GA polish
initiative (Phases 1-10) plus this session's topnav icon-order bugfix.

- 52 existing files updated, 2 new pages added (about.html, health.html)
- cgit/cezen-portal/ is now byte-identical to cezen-portal/
- Backend files (rag_ingest.py, cezen_license.py) already in sync;
  main.py intentionally left as-is (cgit is ahead there with
  workstation-category commercial-fit logic not yet backported to the
  live working copy)

Next: rebuild the ISO (bash autoinstall/build-iso.sh) to bake this in.
2026-07-09 11:11:38 +05:30

737 lines
38 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=12">
<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 { width:56px; height:56px; margin:0 auto 16px; color:var(--teal); }
.sj-empty-icon svg { width:100%; height:100%; }
.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; cursor:pointer; color:var(--lt); width:20px; height:20px; padding:0; }
.sj-modal-close svg { width:100%; height:100%; }
.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">Governance & Audit</a>
<a href="health.html">Health Center</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 Catalog</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">MCP & Enterprise 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">AI Workspace</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 Studio</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Designer</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" class="nav-icon-btn" style="position:relative" aria-label="Notifications">
<span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg></span>
</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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg></span><span>New Job</span></span></button>
</div>
<div class="sj-job-list" id="job-list">
<div class="cezen-list-empty">No jobs yet &mdash; <a onclick="newJob()">create your first one</a>.</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 icon-svg" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l3 2"/><path d="M8 3h8"/></svg></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')"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></svg></span><span>Daily Summary</span></span></button>
<button class="btn btn-ghost" onclick="newJob('hourly')"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="13" r="8"/><path d="M12 9v4l3 2"/><path d="M8 3h8"/></svg></span><span>Hourly Monitor</span></span></button>
<button class="btn btn-ghost" onclick="newJob('weekly')"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 19h16"/><path d="M7 16V8"/><path d="M12 16V5"/><path d="M17 16v-3"/></svg></span><span>Weekly Report</span></span></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"><span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01A1.65 1.65 0 0 0 10.5 3.1V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>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()"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M5 21h14"/><path d="M19 21V7l-3-3H5v17"/><path d="M9 10h6"/><path d="M9 14h6"/></svg></span><span>Save Job</span></span></button>
<button class="btn btn-ghost" onclick="triggerNow()" id="trigger-btn" style="display:none"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg></span><span>Run Now</span></span></button>
<button class="btn btn-ghost" onclick="deleteJob()" id="delete-btn" style="display:none;color:#B91C1C;border-color:#FCA5A5"><span class="icon-label"><span class="icon-svg icon-sm" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/></svg></span><span>Delete</span></span></button>
</div>
</div>
<!-- Run history -->
<div class="sj-card" id="hist-card" style="display:none">
<div class="sj-card-title"><span class="icon-svg icon-md" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M9 3h6"/><path d="M10 7h4"/><rect x="5" y="5" width="14" height="16" rx="2"/></svg></span>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')" aria-label="Close"><svg viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></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;
function sjIconSvg(name, sizeClass = 'icon-sm') {
const icons = {
robot:'<svg viewBox="0 0 24 24"><rect x="7" y="8" width="10" height="8" rx="2"/><path d="M12 4v4M9 16v2M15 16v2M6 10H4M20 10h-2"/><circle cx="10" cy="12" r="1"/><circle cx="14" cy="12" r="1"/></svg>',
chat:'<svg viewBox="0 0 24 24"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
play:'<svg viewBox="0 0 24 24"><path d="m8 5 11 7-11 7z"/></svg>',
spinner:'<svg viewBox="0 0 24 24"><path d="M21 12a9 9 0 1 1-2.64-6.36"/></svg>'
};
return `<span class="icon-svg ${sizeClass}" aria-hidden="true">${icons[name] || icons.chat}</span>`;
}
// ── 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 class="cezen-list-empty">No jobs yet &mdash; <a onclick="newJob()">create your first one</a>.</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.innerHTML = `<span class="icon-label">${sjIconSvg('spinner')}<span>Running...</span></span>`;
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.innerHTML = `<span class="icon-label">${sjIconSvg('play')}<span>Run Now</span></span>`;
}
// ── 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?v=3"></script>
</body>
</html>