Initial commit

This commit is contained in:
Jordan
2026-04-01 23:16:45 +01:00
commit 91cfdaee72
200 changed files with 25589 additions and 0 deletions

133
dashboard/js/api.js Normal file
View File

@@ -0,0 +1,133 @@
/**
* API Client — all fetch calls + EventSource wrapper
*/
const BASE = '/api/v1';
class ApiClient {
constructor() {
this._eventSources = new Map();
this._bus = new EventTarget();
}
// --- Event bus ---
on(event, fn) { this._bus.addEventListener(event, fn); }
off(event, fn) { this._bus.removeEventListener(event, fn); }
_emit(event, detail) {
this._bus.dispatchEvent(new CustomEvent(event, { detail }));
}
// --- HTTP helpers ---
async _fetch(path, opts = {}) {
try {
const res = await fetch(`${BASE}${path}`, {
headers: { 'Content-Type': 'application/json', ...opts.headers },
...opts,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: res.statusText }));
throw new Error(err.detail || `HTTP ${res.status}`);
}
return res.json();
} catch (e) {
this._emit('error', { message: e.message, path });
throw e;
}
}
// --- Health ---
async checkHealth() {
try {
const data = await fetch('/health').then(r => r.json());
this._emit('health', data);
return data;
} catch {
this._emit('health', { status: 'disconnected' });
return { status: 'disconnected' };
}
}
// --- Sessions ---
async createSession(body) {
const data = await this._fetch('/sessions', {
method: 'POST',
body: JSON.stringify(body),
});
this._emit('session:created', data);
return data;
}
async getSession(sessionId) {
const data = await this._fetch(`/sessions/${sessionId}`);
this._emit('session:state', data);
return data;
}
async deleteSession(sessionId) {
const data = await this._fetch(`/sessions/${sessionId}`, { method: 'DELETE' });
this._emit('session:deleted', { session_id: sessionId });
return data;
}
async getEvents(sessionId) {
return this._fetch(`/sessions/${sessionId}/events`);
}
// --- Messages ---
async sendMessage(sessionId, message, stream = false) {
return this._fetch(`/sessions/${sessionId}/messages`, {
method: 'POST',
body: JSON.stringify({ message, stream }),
});
}
// --- SSE ---
subscribeSSE(sessionId) {
this.unsubscribeSSE(sessionId);
const url = `${BASE}/sessions/${sessionId}/stream`;
const es = new EventSource(url);
const eventTypes = [
'session.created', 'execution.started', 'agent.delta',
'tool.started', 'tool.completed', 'subagent.assigned',
'execution.completed', 'error', 'keepalive',
];
for (const type of eventTypes) {
es.addEventListener(type, (e) => {
try {
const payload = JSON.parse(e.data);
this._emit('sse', { type, ...payload });
this._emit(`sse:${type}`, payload);
} catch {
this._emit('sse', { type, raw: e.data });
}
});
}
es.onerror = () => {
this._emit('sse:error', { sessionId });
};
this._eventSources.set(sessionId, es);
return es;
}
unsubscribeSSE(sessionId) {
const es = this._eventSources.get(sessionId);
if (es) {
es.close();
this._eventSources.delete(sessionId);
}
}
unsubscribeAll() {
for (const [id, es] of this._eventSources) {
es.close();
}
this._eventSources.clear();
}
}
export const api = new ApiClient();