roboticus-api 0.11.4

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
App._calMonth = new Date().getMonth();
App._calYear = new Date().getFullYear();
App._calSelected = null;
App._calEditJob = null;
App._calModalMode = null;
App._JOB_COLORS = ['#c180ff','#22c55e','#06b6d4','#f59e0b','#ec4899','#8b5cf6','#ef4444','#14b8a6','#f97316','#a855f7'];
App._colorForJob = function(jobId) {
      if (!jobId) return this._JOB_COLORS[0];
      var h = 0;
      for (var i = 0; i < jobId.length; i++) h = ((h << 5) - h) + jobId.charCodeAt(i);
      return this._JOB_COLORS[Math.abs(h) % this._JOB_COLORS.length];
};
App._parseCronToSched = function(kind, expr) {
      var s = { freq: 'daily', interval: 5, intervalUnit: 'minutes', hour: '02', minute: '00', days: [1,2,3,4,5,6,0] };
      if (kind === 'interval' || kind === 'every') {
        var m = expr.match(/^(\d+)(s|m|h)$/);
        if (m) { s.freq = 'interval'; s.interval = parseInt(m[1]); s.intervalUnit = m[2] === 's' ? 'seconds' : m[2] === 'h' ? 'hours' : 'minutes'; }
        return s;
      }
      var parts = (expr || '* * * * *').split(/\s+/);
      var min = parts[0] || '*', hr = parts[1] || '*';
      if (min.indexOf('/') !== -1) { s.freq = 'interval'; s.intervalUnit = 'minutes'; s.interval = parseInt(min.split('/')[1]) || 5; }
      else if (hr.indexOf('/') !== -1) { s.freq = 'interval'; s.intervalUnit = 'hours'; s.interval = parseInt(hr.split('/')[1]) || 1; }
      else if (hr !== '*' && min !== '*') { s.hour = String(parseInt(hr)).padStart(2, '0'); s.minute = String(parseInt(min)).padStart(2, '0'); var dow = parts[4] || '*'; if (dow === '*') { s.freq = 'daily'; s.days = [0,1,2,3,4,5,6]; } else { s.freq = 'weekly'; s.days = dow.split(',').map(function(d) { return parseInt(d); }); } }
      else if (min !== '*') { s.freq = 'hourly'; s.minute = String(parseInt(min)).padStart(2, '0'); }
      else { s.freq = 'interval'; s.interval = 1; s.intervalUnit = 'minutes'; }
      return s;
};
App._schedToCron = function(s) {
      if (s.freq === 'interval') { if (s.intervalUnit === 'seconds') return { kind: 'interval', expr: s.interval + 's' }; if (s.intervalUnit === 'hours') return { kind: 'cron', expr: '0 */' + s.interval + ' * * *' }; return { kind: 'cron', expr: '*/' + s.interval + ' * * * *' }; }
      if (s.freq === 'hourly') return { kind: 'cron', expr: s.minute + ' * * * *' };
      if (s.freq === 'weekly') return { kind: 'cron', expr: s.minute + ' ' + s.hour + ' * * ' + s.days.join(',') };
      return { kind: 'cron', expr: s.minute + ' ' + s.hour + ' * * *' };
};
App._readSchedFromUI = function() {
      var freqEl = document.getElementById('cal-sched-freq'); var freq = freqEl ? freqEl.value : 'daily';
      var s = { freq: freq, interval: 5, intervalUnit: 'minutes', hour: '02', minute: '00', days: [0,1,2,3,4,5,6] };
      if (freq === 'interval') { var intEl = document.getElementById('cal-sched-interval'); var unitEl = document.getElementById('cal-sched-interval-unit'); s.interval = intEl ? parseInt(intEl.value) || 1 : 5; s.intervalUnit = unitEl ? unitEl.value : 'minutes'; }
      else if (freq === 'hourly') { var minEl = document.getElementById('cal-sched-minute'); s.minute = String(minEl ? parseInt(minEl.value) || 0 : 0).padStart(2, '0'); }
      else { var hrEl = document.getElementById('cal-sched-hour'); var minEl = document.getElementById('cal-sched-minute'); s.hour = String(hrEl ? parseInt(hrEl.value) || 0 : 0).padStart(2, '0'); s.minute = String(minEl ? parseInt(minEl.value) || 0 : 0).padStart(2, '0'); if (freq === 'weekly') { s.days = []; document.querySelectorAll('.cal-day-btn.active').forEach(function(b) { s.days.push(parseInt(b.getAttribute('data-cal-day'))); }); if (s.days.length === 0) s.days = [1]; } }
      return s;
};
App._schedSummary = function(s) {
      if (s.freq === 'interval') return 'Every ' + s.interval + ' ' + s.intervalUnit;
      if (s.freq === 'hourly') return 'Every hour at :' + s.minute;
      var dayNames = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; var time = s.hour + ':' + s.minute;
      if (s.freq === 'weekly') return 'Weekly on ' + s.days.map(function(i) { return dayNames[i]; }).join(', ') + ' at ' + time;
      return 'Daily at ' + time;
};
App._cronIntentFromJob = function(job) {
      if (!job || !job.payload_json) return '';
      try {
        var payload = JSON.parse(job.payload_json);
        return String(payload.task || payload.prompt || payload.message || '').trim();
      } catch (_) {
        return '';
      }
};
App._projectCronDaysInMonth = function(job, year, month) {
      var days = [];
      if (!job || !job.schedule_kind || !job.schedule_expr) return days;
      var maxDay = new Date(year, month + 1, 0).getDate();
      var sched = this._parseCronToSched(job.schedule_kind, job.schedule_expr);
      // Interval/hourly jobs are frequent: show as "scheduled" every day.
      if (sched.freq === 'interval' || sched.freq === 'hourly') {
        for (var d = 1; d <= maxDay; d++) {
          days.push(year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0'));
        }
        return days;
      }
      if (sched.freq === 'daily') {
        for (var d2 = 1; d2 <= maxDay; d2++) {
          days.push(year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d2).padStart(2, '0'));
        }
        return days;
      }
      if (sched.freq === 'weekly') {
        var allowed = {};
        (sched.days || []).forEach(function(x) { allowed[String(x)] = true; });
        for (var d3 = 1; d3 <= maxDay; d3++) {
          var jsDay = new Date(year, month, d3).getDay(); // 0=Sun
          if (allowed[String(jsDay)]) {
            days.push(year + '-' + String(month + 1).padStart(2, '0') + '-' + String(d3).padStart(2, '0'));
          }
        }
      }
      return days;
};
App.renderScheduler = function() {
      var self = this;
      var year = self._calYear, month = self._calMonth;
      var monthStart = year + '-' + String(month + 1).padStart(2, '0') + '-01 00:00:00';
      var monthEnd = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(new Date(year, month + 1, 0).getDate()).padStart(2, '0') + ' 23:59:59';
      return Promise.all([
        api('/api/cron/jobs'),
        api('/api/cron/runs?from=' + encodeURIComponent(monthStart) + '&to=' + encodeURIComponent(monthEnd) + '&limit=5000').catch(function() { return { runs: [] }; })
      ]).then(function(arr) {
        var jobs = (arr[0] && arr[0].jobs) || [];
        var runs = (arr[1] && arr[1].runs) || [];
        jobs.forEach(function(j) { j.color = self._colorForJob(j.id || j.name || 'job'); });
        _cachedCronJobs = jobs;
        var now = new Date();
        var todayKey = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0');
        var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
        var dows = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];

        var firstDay = new Date(year, month, 1);
        var startDow = (firstDay.getDay() + 6) % 7;
        var daysInMonth = new Date(year, month + 1, 0).getDate();
        var prevDays = new Date(year, month, 0).getDate();
        var today = now.getDate();

        var cronRuns = {};
        runs.forEach(function(r) {
          var day = r.day || (r.created_at ? r.created_at.split(' ')[0] : '');
          if (!day) return;
          if (!cronRuns[day]) cronRuns[day] = [];
          cronRuns[day].push({
            job: r.job_name,
            job_id: r.job_id,
            status: (r.status || 'unknown').toLowerCase()
          });
        });
        Object.keys(cronRuns).forEach(function(day) {
          var grouped = {};
          cronRuns[day].forEach(function(r) {
            var key = r.job_id || r.job;
            if (!grouped[key]) grouped[key] = { job: r.job, job_id: r.job_id, count: 0, success_count: 0, error_count: 0, status: 'success', scheduled_only: false };
            grouped[key].count += 1;
            if (r.status === 'error') grouped[key].error_count += 1;
            else grouped[key].success_count += 1;
          });
          Object.keys(grouped).forEach(function(k) {
            var g = grouped[k];
            if (g.error_count > 0 && g.success_count > 0) g.status = 'mixed';
            else if (g.error_count > 0) g.status = 'error';
            else g.status = 'success';
          });
          cronRuns[day] = Object.keys(grouped).map(function(k) { return grouped[k]; });
        });

        // Project scheduled occurrences into the month grid so newly created jobs
        // are visible before first execution.
        jobs.forEach(function(j) {
          var projected = self._projectCronDaysInMonth(j, year, month);
          projected.forEach(function(day) {
            if (!cronRuns[day]) cronRuns[day] = [];
            var exists = cronRuns[day].some(function(r) {
              return (r.job_id && j.id && r.job_id === j.id) || r.job === j.name;
            });
            if (!exists) {
              cronRuns[day].push({
                job: j.name,
                job_id: j.id,
                count: 0,
                success_count: 0,
                error_count: 0,
                status: 'scheduled',
                scheduled_only: true
              });
            }
          });
        });

        var jobMap = {}; jobs.forEach(function(j) { jobMap[j.name] = j; if (j.id) jobMap[j.id] = j; });
        var calCells = '';
        var totalCells = Math.ceil((startDow + daysInMonth) / 7) * 7;
        for (var i = 0; i < totalCells; i++) {
          var dayNum, isOther = false, dateKey;
          if (i < startDow) { dayNum = prevDays - startDow + i + 1; isOther = true; var pm = month === 0 ? 12 : month; var py = month === 0 ? year - 1 : year; dateKey = py + '-' + String(pm).padStart(2, '0') + '-' + String(dayNum).padStart(2, '0'); }
          else if (i >= startDow + daysInMonth) { dayNum = i - startDow - daysInMonth + 1; isOther = true; var nm = month + 2 > 12 ? 1 : month + 2; var ny = month + 2 > 12 ? year + 1 : year; dateKey = ny + '-' + String(nm).padStart(2, '0') + '-' + String(dayNum).padStart(2, '0'); }
          else { dayNum = i - startDow + 1; dateKey = year + '-' + String(month + 1).padStart(2, '0') + '-' + String(dayNum).padStart(2, '0'); }
          var isToday = dateKey === todayKey; var isSelected = dateKey === self._calSelected;
          var cls = 'cal-day' + (isOther ? ' other-month' : '') + (isToday ? ' today' : '') + (isSelected ? ' selected' : '');
          var dayRuns = cronRuns[dateKey] || [];
          var isPast = dateKey < todayKey;
          var evtHtml = '';
          if (dayRuns.length > 0) {
            evtHtml = '<div class="cal-events">';
            dayRuns.forEach(function(r) { var job = jobMap[r.job_id || r.job]; var color = job ? job.color : '#9baad6'; var failCls = r.status === 'error' ? ' fail' : ''; var scheduledCls = r.status === 'scheduled' ? ' past' : ''; var pastCls = isPast ? ' past' : ''; var pastAttr = isPast ? ' data-past="true"' : ''; var statusText = r.status === 'scheduled' ? 'scheduled' : r.status; evtHtml += '<div class="cal-evt' + failCls + scheduledCls + pastCls + '" style="background:' + color + '" title="' + esc(r.job) + ' (' + r.count + ' runs, status: ' + statusText + ')' + '"' + pastAttr + ' data-evt-job="' + esc(r.job) + '">' + esc(r.job) + '</div>'; });
            evtHtml += '</div>';
          }
          calCells += '<div class="' + cls + '" data-date="' + dateKey + '"><div class="cal-day-num">' + dayNum + '</div>' + evtHtml + '</div>';
        }

        var dowHeaders = dows.map(function(d) { return '<div class="cal-dow">' + d + '</div>'; }).join('');
        var legend = jobs.map(function(j, idx) {
          var ls = (j.last_status || '').toLowerCase(); var stBadge = ls === 'success' ? '<span class="badge success" style="font-size:0.5625rem">ok</span>' : (ls === 'error' ? '<span class="badge error" style="font-size:0.5625rem">err</span>' : '<span class="badge" style="font-size:0.5625rem;background:rgba(113,113,122,0.2);color:#a1a1aa">idle</span>');
          return '<div class="cal-legend-item"><div class="cal-legend-dot" style="background:' + j.color + '"></div><div class="cal-legend-info"><div class="cal-legend-name">' + esc(j.name) + '</div><div class="cal-legend-sched">' + esc(j.schedule_expr && j.schedule_expr.toLowerCase().indexOf((j.schedule_kind || '').toLowerCase()) === 0 ? j.schedule_expr : ((j.schedule_kind || '') + ' ' + (j.schedule_expr || '')).trim()) + '</div></div><div class="cal-legend-actions">' + stBadge + '<button class="cal-legend-btn" data-cal-edit="' + idx + '" title="Edit">\u270e</button><button class="cal-legend-btn danger" data-cal-delete="' + idx + '" title="Delete">\u2715</button></div></div>';
        }).join('');

        var detail = '';
        if (self._calSelected) {
          var selIsPast = self._calSelected < todayKey;
          var selRuns = cronRuns[self._calSelected] || [];
          if (selRuns.length > 0) {
            var histLabel = selIsPast ? '<div style="font-size:0.625rem;color:var(--muted);margin-bottom:0.375rem;text-transform:uppercase;letter-spacing:0.06em">Historical runs</div>' : '';
            var runItems = selRuns.map(function(r) { var job = jobMap[r.job_id || r.job]; var color = job ? job.color : '#9baad6'; var stCls = r.status === 'error' ? 'error' : (r.status === 'scheduled' ? 'muted' : 'success'); var countLabel = r.status === 'scheduled' ? 'planned' : (r.count + 'x'); return '<div class="cal-detail-run"><div class="cal-detail-dot" style="background:' + color + '"></div><span style="flex:1">' + esc(r.job) + '</span><span class="card-mono" style="color:var(--muted)">' + countLabel + '</span><span class="badge ' + stCls + '" style="font-size:0.5625rem">' + r.status + '</span></div>'; }).join('');
            detail = '<div class="cal-detail"><div class="cal-detail-title">' + esc(self._calSelected) + '</div>' + histLabel + runItems + '</div>';
          } else { detail = '<div class="cal-detail"><div class="cal-detail-title">' + esc(self._calSelected) + '</div><div style="color:var(--muted);font-size:0.75rem">No runs on this day</div></div>'; }
        }

        var modal = '';
        if (self._calModalMode) {
          var job = self._calEditJob || { name: '', description: '', schedule_kind: 'cron', schedule_expr: '' };
          var titleText = self._calModalMode === 'add' ? 'New Scheduled Job' : 'Edit Job';
          var sched = self._parseCronToSched(job.schedule_kind, job.schedule_expr);
          var freqOpts = [['interval','Repeating'],['hourly','Hourly'],['daily','Daily'],['weekly','Weekly']];
          var freqSelect = '<select class="cal-modal-select" id="cal-sched-freq">' + freqOpts.map(function(f) { return '<option value="' + f[0] + '"' + (sched.freq === f[0] ? ' selected' : '') + '>' + f[1] + '</option>'; }).join('') + '</select>';
          var intervalRow = '';
          if (sched.freq === 'interval') { var unitOpts = [['seconds','Seconds'],['minutes','Minutes'],['hours','Hours']]; intervalRow = '<div class="cal-sched-row"><span class="cal-sched-inline">Every</span><input class="cal-sched-input-sm" type="number" id="cal-sched-interval" value="' + sched.interval + '" min="1" max="999"><select class="cal-modal-select" id="cal-sched-interval-unit" style="min-width:80px">' + unitOpts.map(function(u) { return '<option value="' + u[0] + '"' + (sched.intervalUnit === u[0] ? ' selected' : '') + '>' + u[1] + '</option>'; }).join('') + '</select></div>'; }
          var timeRow = '';
          if (sched.freq === 'hourly') { timeRow = '<div class="cal-sched-row"><span class="cal-sched-inline">At minute</span><input class="cal-sched-input-sm" type="number" id="cal-sched-minute" value="' + parseInt(sched.minute) + '" min="0" max="59"><span class="cal-sched-inline">of every hour</span></div>'; }
          else if (sched.freq === 'daily' || sched.freq === 'weekly') { timeRow = '<div class="cal-sched-row"><span class="cal-sched-inline">At</span><input class="cal-sched-input-sm" type="number" id="cal-sched-hour" value="' + parseInt(sched.hour) + '" min="0" max="23"><span class="cal-sched-inline">:</span><input class="cal-sched-input-sm" type="number" id="cal-sched-minute" value="' + parseInt(sched.minute) + '" min="0" max="59"></div>'; }
          var daysRow = '';
          if (sched.freq === 'weekly') { var dayLabels = ['Su','Mo','Tu','We','Th','Fr','Sa']; daysRow = '<div class="cal-modal-row"><label class="cal-modal-label">Days</label><div class="cal-days-row">' + dayLabels.map(function(d, i) { return '<button class="cal-day-btn' + (sched.days.indexOf(i) !== -1 ? ' active' : '') + '" data-cal-day="' + i + '">' + d + '</button>'; }).join('') + '</div></div>'; }
          var summary = self._schedSummary(sched);
          var editNotice = '';
          if (self._calModalMode === 'edit') { editNotice = '<div class="cal-edit-info"><strong>Note:</strong> Changes to the schedule will only apply to <strong>future</strong> executions. Historical run records are preserved as-is and cannot be modified.</div>'; }
          modal = '<div class="cal-modal-overlay" data-cal-overlay><div class="cal-modal"><div class="cal-modal-header"><h3>' + titleText + '</h3><button class="cal-modal-close" data-cal-modal-close>\u00d7</button></div><div class="cal-modal-body">' + editNotice + '<div class="cal-modal-row"><label class="cal-modal-label">Name</label><input class="cal-modal-input" id="cal-job-name" value="' + esc(job.name) + '" placeholder="e.g. health-check"></div><div class="cal-modal-row"><label class="cal-modal-label">Intent</label><input class="cal-modal-input" id="cal-job-description" value="' + esc(job.description || '') + '" placeholder="e.g. summarize overnight events and pending tasks"></div><div class="cal-modal-row"><label class="cal-modal-label">Recurrence</label>' + freqSelect + '</div>' + intervalRow + timeRow + daysRow + '<div class="cal-sched-summary" id="cal-sched-summary">' + esc(summary) + '</div></div><div class="cal-modal-footer"><button class="btn secondary" data-cal-modal-close>Cancel</button><button class="btn" id="cal-modal-save">' + (self._calModalMode === 'add' ? 'Add Job' : 'Save') + '</button></div></div></div>';
        }

        return '<div class="cal-wrap"><div class="cal-header"><div class="cal-header-left"><div class="cal-title">' + months[month] + ' ' + year + '</div><div class="cal-nav"><button data-cal-nav="prev">\u25c0</button><button data-cal-nav="today">\u25cf</button><button data-cal-nav="next">\u25b6</button></div></div><div class="cal-header-right"><button class="btn" style="font-size:0.75rem;padding:0.3rem 0.75rem" data-cal-add>+ Add Job</button></div></div><div class="cal-body"><div class="cal-grid-wrap"><div class="cal-dow-row">' + dowHeaders + '</div><div class="cal-grid">' + calCells + '</div></div><div class="cal-sidebar"><div class="cal-sidebar-section"><div class="cal-sidebar-title">Scheduled Jobs (' + jobs.length + ')</div>' + legend + '</div>' + (detail ? '<div class="cal-sidebar-section">' + detail + '</div>' : '') + '</div></div>' + modal + '</div>';
      });
};