diff options
Diffstat (limited to 'admin-panel/src/client/lib')
| -rw-r--r-- | admin-panel/src/client/lib/api.ts | 155 | ||||
| -rw-r--r-- | admin-panel/src/client/lib/markdown.ts | 39 | ||||
| -rw-r--r-- | admin-panel/src/client/lib/ws.ts | 79 |
3 files changed, 273 insertions, 0 deletions
diff --git a/admin-panel/src/client/lib/api.ts b/admin-panel/src/client/lib/api.ts new file mode 100644 index 0000000..d82693f --- /dev/null +++ b/admin-panel/src/client/lib/api.ts @@ -0,0 +1,155 @@ +const BASE = '/api'; + +async function request<T>(path: string, options?: RequestInit): Promise<T> { + const res = await fetch(`${BASE}${path}`, { + headers: { 'Content-Type': 'application/json' }, + ...options, + }); + + if (res.status === 204) return undefined as T; + + const data = await res.json(); + if (!res.ok) { + throw new Error(data.error || `Request failed: ${res.status}`); + } + return data as T; +} + +export const api = { + // Projects + getProjects: () => request<Project[]>('/projects'), + createProject: (data: CreateProject) => + request<Project>('/projects', { method: 'POST', body: JSON.stringify(data) }), + getProject: (name: string) => request<Project>(`/projects/${name}`), + updateProject: (name: string, data: Partial<Project>) => + request<Project>(`/projects/${name}`, { method: 'PATCH', body: JSON.stringify(data) }), + deleteProject: (name: string, deleteRepo = false) => + request<void>(`/projects/${name}?delete_repo=${deleteRepo}`, { method: 'DELETE' }), + + // Tasks + getTasks: (project: string) => request<Task[]>(`/projects/${project}/tasks`), + createTask: (project: string, data: CreateTask) => + request<Task>(`/projects/${project}/tasks`, { method: 'POST', body: JSON.stringify(data) }), + getTask: (project: string, id: number) => request<Task>(`/projects/${project}/tasks/${id}`), + updateTask: (project: string, id: number, data: Partial<Task>) => + request<Task>(`/projects/${project}/tasks/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), + deleteTask: (project: string, id: number) => + request<void>(`/projects/${project}/tasks/${id}`, { method: 'DELETE' }), + reorderTask: (project: string, id: number, rank: number, status?: string) => + request<Task>(`/projects/${project}/tasks/${id}/reorder`, { + method: 'PATCH', + body: JSON.stringify({ rank, status }), + }), + + // Pipeline + runPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/run/${taskId}`, { method: 'POST' }), + stepPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/step/${taskId}`, { method: 'POST' }), + stopPipeline: (project: string, taskId: number) => + request<void>(`/projects/${project}/pipeline/stop/${taskId}`, { method: 'POST' }), + getPipelineStatus: (project: string, taskId: number) => + request<PipelineStatus>(`/projects/${project}/pipeline/status/${taskId}`), + + // Dashboard + getDashboardStats: () => request<DashboardStats>('/dashboard/stats'), + getDashboardActivity: () => request<ActivityEntry[]>('/dashboard/activity'), + + // Settings + getSettings: () => request<Record<string, string>>('/settings'), + updateSettings: (data: Record<string, string>) => + request<void>('/settings', { method: 'PATCH', body: JSON.stringify(data) }), +}; + +// Types +export interface Project { + id: number; + name: string; + display_name: string; + description: string | null; + repo_name: string; + repo_path: string; + default_branch: string; + created_at: string; + updated_at: string; +} + +export interface CreateProject { + name: string; + display_name: string; + description?: string; +} + +export interface Task { + id: number; + title: string; + status: string; + priority: string; + level: number; + description: string | null; + plan: string | null; + decision_log: string | null; + done_when: string | null; + implementation_notes: string | null; + tags: string; + plan_review_comments: string; + review_comments: string; + test_results: string; + agent_log: string; + current_agent: string | null; + plan_review_count: number; + impl_review_count: number; + branch_name: string | null; + gerrit_change_id: string | null; + gerrit_change_number: number | null; + diff: string | null; + rank: number; + created_at: string; + started_at: string | null; + planned_at: string | null; + reviewed_at: string | null; + tested_at: string | null; + completed_at: string | null; +} + +export interface CreateTask { + title: string; + description?: string; + priority?: string; + level?: number; + tags?: string[]; +} + +export interface PipelineStatus { + running: boolean; + currentAgent: string | null; + jobs: PipelineJob[]; +} + +export interface PipelineJob { + id: number; + task_id: number; + agent: string; + status: string; + model: string; + started_at: string | null; + completed_at: string | null; + error: string | null; + tokens_used: number; +} + +export interface DashboardStats { + totalProjects: number; + totalTasks: number; + tasksByStatus: Record<string, number>; + completedToday: number; +} + +export interface ActivityEntry { + project: string; + taskId: number; + taskTitle: string; + agent: string; + action: string; + timestamp: string; +} diff --git a/admin-panel/src/client/lib/markdown.ts b/admin-panel/src/client/lib/markdown.ts new file mode 100644 index 0000000..c89fa77 --- /dev/null +++ b/admin-panel/src/client/lib/markdown.ts @@ -0,0 +1,39 @@ +// Minimal markdown renderer — handles the basics without pulling in a library +export function renderMarkdown(text: string): string { + if (!text) return ''; + + let html = escapeHtml(text); + + // Code blocks + html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="lang-$1">$2</code></pre>'); + + // Inline code + html = html.replace(/`([^`]+)`/g, '<code>$1</code>'); + + // Headers + html = html.replace(/^### (.+)$/gm, '<h4>$1</h4>'); + html = html.replace(/^## (.+)$/gm, '<h3>$1</h3>'); + html = html.replace(/^# (.+)$/gm, '<h2>$1</h2>'); + + // Bold and italic + html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); + html = html.replace(/\*(.+?)\*/g, '<em>$1</em>'); + + // Lists + html = html.replace(/^- (.+)$/gm, '<li>$1</li>'); + html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>'); + + // Line breaks + html = html.replace(/\n\n/g, '</p><p>'); + html = `<p>${html}</p>`; + html = html.replace(/<p><\/p>/g, ''); + + return html; +} + +function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); +} diff --git a/admin-panel/src/client/lib/ws.ts b/admin-panel/src/client/lib/ws.ts new file mode 100644 index 0000000..14e0cec --- /dev/null +++ b/admin-panel/src/client/lib/ws.ts @@ -0,0 +1,79 @@ +type EventHandler = (data: unknown) => void; + +class WsClient { + private ws: WebSocket | null = null; + private handlers = new Map<string, Set<EventHandler>>(); + private reconnectTimer: ReturnType<typeof setTimeout> | null = null; + private subscribedProjects = new Set<string>(); + + connect(): void { + if (this.ws?.readyState === WebSocket.OPEN) return; + + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + this.ws = new WebSocket(`${protocol}//${location.host}/ws`); + + this.ws.onopen = () => { + // Re-subscribe to projects + for (const project of this.subscribedProjects) { + this.send({ type: 'subscribe', project }); + } + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + const handlers = this.handlers.get(data.type); + if (handlers) { + for (const handler of handlers) { + handler(data); + } + } + // Also fire wildcard handlers + const wildcardHandlers = this.handlers.get('*'); + if (wildcardHandlers) { + for (const handler of wildcardHandlers) { + handler(data); + } + } + } catch { + // ignore + } + }; + + this.ws.onclose = () => { + this.reconnectTimer = setTimeout(() => this.connect(), 3000); + }; + } + + disconnect(): void { + if (this.reconnectTimer) clearTimeout(this.reconnectTimer); + this.ws?.close(); + this.ws = null; + } + + subscribe(project: string): void { + this.subscribedProjects.add(project); + this.send({ type: 'subscribe', project }); + } + + unsubscribe(project: string): void { + this.subscribedProjects.delete(project); + this.send({ type: 'unsubscribe', project }); + } + + on(event: string, handler: EventHandler): () => void { + if (!this.handlers.has(event)) { + this.handlers.set(event, new Set()); + } + this.handlers.get(event)!.add(handler); + return () => this.handlers.get(event)?.delete(handler); + } + + private send(data: unknown): void { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + } + } +} + +export const wsClient = new WsClient(); |
