roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
App.renderIntegrations = function() {
  return api('/api/integrations').then(function(data) {
    var platforms = (data && data.platforms) ? data.platforms : [];
    var html = '';

    html += '<div class="card-title" style="margin-bottom:1rem">Integrations</div>';
    html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:1rem">';

    platforms.forEach(function(p) {
      var name = p.name || 'unknown';
      var configured = !!p.configured;
      var enabled = !!p.enabled;
      var health = p.health || 'disconnected';
      var recv = p.messages_received || 0;
      var sent = p.messages_sent || 0;
      var errCount = p.error_count || 0;
      var lastErr = p.last_error || null;
      var lastActivity = p.last_activity || null;
      var lastSuccess = p.last_successful_at || null;

      var healthColor = health === 'connected' ? '#22c55e'
        : health === 'degraded' ? '#f59e0b'
        : 'var(--muted)';
      var healthLabel = health === 'connected' ? 'Connected'
        : health === 'degraded' ? 'Degraded'
        : 'Disconnected';

      var dimmed = !configured ? 'opacity:0.5;' : '';
      var icon = _integrationIcon(name);

      html += '<div class="card" style="padding:1rem;' + dimmed + '">';
      html += '<div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.75rem">';
      html += '<span style="width:28px;height:28px;display:flex;align-items:center;justify-content:center">' + icon + '</span>';
      html += '<span style="font-weight:700;font-size:0.95rem;text-transform:capitalize">' + esc(name) + '</span>';
      html += '<span style="margin-left:auto;font-size:0.7rem;padding:0.15rem 0.5rem;border-radius:9999px;'
        + 'background:' + healthColor + '20;color:' + healthColor + ';font-weight:600">'
        + esc(healthLabel) + '</span>';
      html += '</div>';

      if (!configured) {
        html += '<div style="font-size:0.8rem;color:var(--muted);margin-bottom:0.5rem">Not configured</div>';
        html += '<div style="font-size:0.75rem;color:var(--muted)">Add <code>[channels.' + esc(name) + ']</code> to roboticus.toml</div>';
      } else {
        // Stats row
        html += '<div style="display:flex;gap:1.5rem;font-size:0.78rem;margin-bottom:0.5rem">';
        html += '<div><span style="color:var(--muted)">Received</span> <strong>' + recv + '</strong></div>';
        html += '<div><span style="color:var(--muted)">Sent</span> <strong>' + sent + '</strong></div>';
        if (errCount > 0) {
          html += '<div><span style="color:var(--error, #ef4444)">Errors</span> <strong style="color:var(--error, #ef4444)">' + errCount + '</strong></div>';
        }
        html += '</div>';

        // Last error
        if (lastErr) {
          html += '<div style="font-size:0.72rem;color:var(--warning, #f59e0b);margin-bottom:0.4rem;word-break:break-word">'
            + esc(lastErr.substring(0, 120)) + '</div>';
        }

        // Timestamps
        html += '<div style="font-size:0.7rem;color:var(--muted);display:flex;flex-direction:column;gap:0.15rem">';
        if (lastActivity) {
          html += '<div>Last activity: ' + esc(_relTime(lastActivity)) + '</div>';
        }
        if (lastSuccess) {
          html += '<div>Last success: ' + esc(_relTime(lastSuccess)) + '</div>';
        }
        if (!enabled) {
          html += '<div style="color:var(--warning, #f59e0b)">Configured but disabled</div>';
        }
        html += '</div>';

        // Test button
        html += '<button class="btn secondary integration-test-btn" data-test-platform="'
          + esc(name) + '" style="font-size:0.72rem;padding:0.25rem 0.65rem;margin-top:0.6rem">Test</button>';
      }

      html += '</div>';
    });

    html += '</div>';
    return html;
  });
};

function _integrationIcon(name) {
  switch (name) {
    case 'telegram':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><path d="M1.5 7.5l13-5.5-3 12-4.5-3-2.5 2.5v-3.5l-3-2.5z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><line x1="14.5" y1="2" x2="5.5" y2="9" stroke="currentColor" stroke-width="1.2"/></svg>';
    case 'discord':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><path d="M5.5 3C4 3 3 4 3 4s-1 3-.5 5.5c.5 2.5 3 3 3 3l.5-1s-1-.5-1.5-1.5 0-3 0-3h7s.5 2 0 3S10 11.5 10 11.5l.5 1s2.5-.5 3-3S13 4 13 4s-1-1-2.5-1" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><circle cx="6" cy="8" r="1" fill="currentColor"/><circle cx="10" cy="8" r="1" fill="currentColor"/></svg>';
    case 'whatsapp':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.3"/><path d="M5.5 9.5l-.5 2.5 2.5-.5c2 1 4-1 3.5-3S8 5 6 6s-1.5 3-.5 3.5z" fill="none" stroke="currentColor" stroke-width="1.1"/></svg>';
    case 'signal':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.3"/><circle cx="8" cy="8" r="3" fill="none" stroke="currentColor" stroke-width="1.1"/><circle cx="8" cy="8" r="0.8" fill="currentColor"/></svg>';
    case 'email':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><rect x="1.5" y="3" width="13" height="10" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.3"/><polyline points="1.5,3 8,9 14.5,3" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>';
    case 'matrix':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><rect x="2" y="1.5" width="12" height="13" rx="0" fill="none" stroke="currentColor" stroke-width="1.3"/><line x1="2" y1="4" x2="0.5" y2="4" stroke="currentColor" stroke-width="1.2"/><line x1="2" y1="12" x2="0.5" y2="12" stroke="currentColor" stroke-width="1.2"/><line x1="14" y1="4" x2="15.5" y2="4" stroke="currentColor" stroke-width="1.2"/><line x1="14" y1="12" x2="15.5" y2="12" stroke="currentColor" stroke-width="1.2"/></svg>';
    case 'web':
      return '<svg viewBox="0 0 16 16" width="20" height="20"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.3"/><ellipse cx="8" cy="8" rx="3" ry="6.5" fill="none" stroke="currentColor" stroke-width="1.1"/><line x1="1.5" y1="8" x2="14.5" y2="8" stroke="currentColor" stroke-width="1.1"/></svg>';
    default:
      return '<svg viewBox="0 0 16 16" width="20" height="20"><circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="1.3"/><text x="8" y="11" font-size="8" text-anchor="middle" fill="currentColor">?</text></svg>';
  }
}

function _relTime(iso) {
  if (!iso) return 'never';
  var d = new Date(iso);
  var now = Date.now();
  var diff = Math.floor((now - d.getTime()) / 1000);
  if (diff < 60) return diff + 's ago';
  if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
  if (diff < 86400) return Math.floor(diff / 3600) + 'h ago';
  return Math.floor(diff / 86400) + 'd ago';
}