summaryrefslogtreecommitdiff
path: root/admin-panel/src/client/lib
diff options
context:
space:
mode:
Diffstat (limited to 'admin-panel/src/client/lib')
-rw-r--r--admin-panel/src/client/lib/api.ts155
-rw-r--r--admin-panel/src/client/lib/markdown.ts39
-rw-r--r--admin-panel/src/client/lib/ws.ts79
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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+}
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();