/** * Schedules — Cron Job Management Surface (v0.1.0) * * Wraps the kernel /api/v1/schedules API. No Starlark backend — * this surface talks directly to the core scheduled-tasks endpoints. * * SDK usage: * sw.shell.topbar — shell topbar API * sw.ui.* — primitive components * sw.api.* — raw REST calls to /api/v1/schedules */ (async function () { 'use strict'; var mount = document.getElementById('extension-mount'); if (!mount) return; var base = window.__BASE__ || ''; var ver = window.__VERSION__ || '0'; // ── Boot SDK ─────────────────────────────── try { if (!window.preact) { var { h, render } = await import(base + '/js/sw/vendor/preact.module.js'); var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js'); var htmModule = await import(base + '/js/sw/vendor/htm.module.js'); window.preact = { h, render }; window.hooks = hooksModule; window.html = htmModule.default.bind(h); } var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver); await sdk.boot(); } catch (e) { mount.innerHTML = '
SDK boot failed: ' + e.message + '
'; return; } var { html } = window; var { useState, useEffect, useCallback } = hooks; var { render } = preact; var { Button, FormField, Dialog, Spinner, Banner } = sw.ui; // ── API helpers (core kernel endpoints) ──── var API = '/api/v1/schedules'; function apiList() { return sw.api.get(API); } function apiCreate(data) { return sw.api.post(API, data); } function apiUpdate(id, d) { return sw.api.put(API + '/' + id, d); } function apiDelete(id) { return sw.api.del(API + '/' + id); } function apiRun(id) { return sw.api.post(API + '/' + id + '/run', {}); } function apiLogs(id) { return sw.api.get(API + '/' + id + '/logs'); } // ═══════════════════════════════════════════ // Cron description helper // ═══════════════════════════════════════════ var DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; function describeCron(expr) { if (!expr) return ''; var parts = expr.trim().split(/\s+/); if (parts.length !== 5) return expr; var min = parts[0], hr = parts[1], dom = parts[2], mon = parts[3], dow = parts[4]; // Format time var timeStr = ''; if (min !== '*' && hr !== '*') { var h = parseInt(hr, 10); var m = parseInt(min, 10); var ampm = h >= 12 ? 'PM' : 'AM'; var h12 = h === 0 ? 12 : h > 12 ? h - 12 : h; timeStr = h12 + ':' + String(m).padStart(2, '0') + ' ' + ampm; } // Every N minutes if (min.startsWith('*/') && hr === '*' && dom === '*' && mon === '*' && dow === '*') { return 'Every ' + min.slice(2) + ' minutes'; } // Every hour at :MM if (min !== '*' && hr === '*' && dom === '*' && mon === '*' && dow === '*') { return 'Every hour at :' + String(parseInt(min, 10)).padStart(2, '0'); } // Daily if (timeStr && dom === '*' && mon === '*' && dow === '*') { return 'Daily at ' + timeStr; } // Weekdays if (timeStr && dom === '*' && mon === '*' && dow === '1-5') { return 'Weekdays at ' + timeStr; } // Specific days if (timeStr && dom === '*' && mon === '*' && dow !== '*') { var dayNames = dow.split(',').map(function(d) { var n = parseInt(d, 10); return DAYS[n] || d; }); return dayNames.join(', ') + ' at ' + timeStr; } return expr; } // ═══════════════════════════════════════════ // ScheduleForm — create / edit dialog // ═══════════════════════════════════════════ function ScheduleForm({ schedule, onSave, onClose }) { var [name, setName] = useState(schedule ? schedule.name : ''); var [desc, setDesc] = useState(schedule ? schedule.description : ''); var [cron, setCron] = useState(schedule ? schedule.cron_expr : ''); var [script, setScript] = useState(schedule ? schedule.script : ''); var [enabled, setEnabled] = useState(schedule ? schedule.enabled : true); var [saving, setSaving] = useState(false); var [error, setError] = useState(''); var preview = describeCron(cron); async function handleSubmit(e) { e.preventDefault(); if (!name.trim()) { setError('Name is required'); return; } if (!cron.trim()) { setError('Cron expression is required'); return; } setSaving(true); setError(''); try { var data = { name: name.trim(), description: desc, cron_expr: cron.trim(), script: script, enabled: enabled }; if (schedule) { await apiUpdate(schedule.id, data); } else { await apiCreate(data); } onSave(); } catch (err) { var msg = err.message || 'Save failed'; if (err.body?.error) msg = err.body.error; setError(msg); } finally { setSaving(false); } } return html` <${Dialog} open onClose=${onClose} title=${schedule ? 'Edit Schedule' : 'New Schedule'}> /> `; } // ═══════════════════════════════════════════ // ScheduleLogsPanel — execution history // ═══════════════════════════════════════════ function ScheduleLogsPanel({ scheduleId, onClose }) { var [logs, setLogs] = useState([]); var [loading, setLoading] = useState(true); useEffect(function () { if (!scheduleId) return; setLoading(true); apiLogs(scheduleId).then(function (res) { setLogs(res.logs || []); }).catch(function () { setLogs([]); }).finally(function () { setLoading(false); }); }, [scheduleId]); return html`| Name | Schedule | Next Run | Enabled | Actions |
|---|---|---|---|---|
|
${s.description && html` ${s.description} `}
|
${s.cron_expr}
${describeCron(s.cron_expr)}
|
${s.next_fire_at ? _formatTime(s.next_fire_at) : '—'} |