Some checks are pending
build-and-push / image (push) Waiting to run
- Implemented demand pane for managing trend-driven design jobs. - Created diffusion pane for generating images via Stable Diffusion. - Added inbox pane for sweeping and routing artifacts through the CxAI inbox classifier. - Developed items pane for CRUD operations against /api/items. - Introduced lang pane for running language pipelines. - Established mac pane for macOS app distribution information. - Integrated slack pane for sending messages and displaying diagnostics. - Built system pane for process introspection and version information. - Launched tools pane for browsing and invoking MCP tools. - Set up websocket pane for connecting to the /ws/echo service.
113 lines
3.9 KiB
JavaScript
113 lines
3.9 KiB
JavaScript
// lib/api.js — fetch helpers, MCP base resolver, service catalogue.
|
|
|
|
export const MCP_BASE = (() => {
|
|
const fromAttr = document.body?.dataset?.mcpBase;
|
|
return (fromAttr || 'http://127.0.0.1:8082').replace(/\/+$/, '');
|
|
})();
|
|
|
|
class HttpError extends Error {
|
|
constructor(status, body) {
|
|
super(`HTTP ${status}`);
|
|
this.status = status;
|
|
this.body = body;
|
|
}
|
|
}
|
|
|
|
async function parseResp(res) {
|
|
const text = await res.text();
|
|
let body;
|
|
try { body = text ? JSON.parse(text) : null; } catch { body = text; }
|
|
if (!res.ok) throw new HttpError(res.status, body);
|
|
return body;
|
|
}
|
|
|
|
export async function jget(url, opts = {}) {
|
|
const res = await fetch(url, { ...opts, headers: { Accept: 'application/json', ...(opts.headers || {}) } });
|
|
return parseResp(res);
|
|
}
|
|
|
|
export async function jpost(url, body, opts = {}) {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
...opts,
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(opts.headers || {}) },
|
|
body: body == null ? undefined : (typeof body === 'string' ? body : JSON.stringify(body)),
|
|
});
|
|
return parseResp(res);
|
|
}
|
|
|
|
export async function jdelete(url, opts = {}) {
|
|
const res = await fetch(url, { method: 'DELETE', ...opts });
|
|
return parseResp(res);
|
|
}
|
|
|
|
export async function jraw(method, url, body) {
|
|
const init = { method };
|
|
if (body && body !== '') {
|
|
init.headers = { 'Content-Type': 'application/json' };
|
|
init.body = body;
|
|
}
|
|
const res = await fetch(url, init);
|
|
const text = await res.text();
|
|
let parsed;
|
|
try { parsed = text ? JSON.parse(text) : null; } catch { parsed = text; }
|
|
return { status: res.status, ok: res.ok, body: parsed, raw: text };
|
|
}
|
|
|
|
// ---------- MCP shortcuts (used by agent/inbox/tools panes) ----------
|
|
export const mcp = {
|
|
status: () => jget(`${MCP_BASE}/status`),
|
|
events: (limit = 150) => jget(`${MCP_BASE}/events?limit=${limit}`),
|
|
tools: () => jget(`${MCP_BASE}/tools`),
|
|
call: (name, args = {}) => jpost(`${MCP_BASE}/tools/${name}`, args),
|
|
};
|
|
|
|
// ---------- Service registry (used by dashboard health pills) ----------
|
|
export const services = [
|
|
{ id: 'crow', label: 'Crow API', health: '/api/health' },
|
|
{ id: 'mcp', label: 'MCP', health: `${MCP_BASE}/status` },
|
|
{ id: 'diffusion', label: 'Diffusion', health: '/api/diffusion/v1/health' },
|
|
{ id: 'demand', label: 'Demand', health: '/api/demand/healthz' },
|
|
{ id: 'lang', label: 'Lang', health: '/api/lang/healthz' },
|
|
{ id: 'slack', label: 'Slack', health: '/api/slack/healthz' },
|
|
];
|
|
|
|
export async function probeService(svc) {
|
|
const t0 = performance.now();
|
|
try {
|
|
await jget(svc.health);
|
|
return { ok: true, ms: Math.round(performance.now() - t0) };
|
|
} catch (err) {
|
|
return { ok: false, ms: Math.round(performance.now() - t0), error: err.status || err.message };
|
|
}
|
|
}
|
|
|
|
export function escapeHtml(s) {
|
|
return String(s ?? '').replace(/[&<>"']/g, c => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
}[c]));
|
|
}
|
|
|
|
export function formatUptime(seconds) {
|
|
if (seconds == null || isNaN(seconds)) return '—';
|
|
seconds = Math.max(0, Math.floor(seconds));
|
|
const d = Math.floor(seconds / 86400); seconds %= 86400;
|
|
const h = Math.floor(seconds / 3600); seconds %= 3600;
|
|
const m = Math.floor(seconds / 60); const s = seconds % 60;
|
|
if (d) return `${d}d ${h}h ${m}m`;
|
|
if (h) return `${h}h ${m}m ${s}s`;
|
|
if (m) return `${m}m ${s}s`;
|
|
return `${s}s`;
|
|
}
|
|
|
|
export function statusClass(status) {
|
|
if (!status) return 'muted';
|
|
const s = String(status).toLowerCase();
|
|
if (s.includes('success') || s === 'ok' || s === 'ready' || s === 'healthy') return 'ok';
|
|
if (s.includes('error') || s.includes('fail')) return 'err';
|
|
if (s.includes('warn') || s.includes('pending') || s.includes('queued') || s.includes('running')) return 'warn';
|
|
return 'info';
|
|
}
|
|
|
|
export { HttpError };
|