this repo has no description
at main 189 lines 5.5 kB view raw
1import { processCommand } from '../cli/process'; 2import { getDayDetail, getDays, getProjects, getStats, updateProjectStatus } from '../core/db'; 3import type { ProjectStatus } from '../types'; 4 5type ApiHandler = (req: Request, url: URL) => Promise<Response>; 6 7interface URLWithParams extends URL { 8 params?: Record<string, string>; 9} 10 11const routes: Record<string, ApiHandler> = { 12 'GET /api/days': handleGetDays, 13 'GET /api/days/:date': handleGetDayDetail, 14 'GET /api/days/:date/brag': handleGetDayBrag, 15 'GET /api/stats': handleGetStats, 16 'POST /api/refresh': handleRefresh, 17 'GET /api/projects': handleGetProjects, 18 'PATCH /api/projects/status': handleUpdateProjectStatus, 19}; 20 21export async function handleApiRequest(req: Request, url: URL): Promise<Response> { 22 const method = req.method; 23 const path = url.pathname; 24 25 // Match routes 26 for (const [route, handler] of Object.entries(routes)) { 27 const [routeMethod, routePath] = route.split(' '); 28 if (method !== routeMethod) continue; 29 30 const params = matchPath(routePath, path); 31 if (params !== null) { 32 try { 33 // Attach params to URL for handler access 34 (url as URLWithParams).params = params; 35 return await handler(req, url); 36 } catch (error) { 37 console.error('API error:', error); 38 return jsonResponse({ error: 'Internal server error' }, 500); 39 } 40 } 41 } 42 43 return jsonResponse({ error: 'Not found' }, 404); 44} 45 46function matchPath(pattern: string, path: string): Record<string, string> | null { 47 const patternParts = pattern.split('/'); 48 const pathParts = path.split('/'); 49 50 if (patternParts.length !== pathParts.length) return null; 51 52 const params: Record<string, string> = {}; 53 54 for (const [i, patternPart] of patternParts.entries()) { 55 // pathPart is guaranteed to exist since we verified lengths match 56 const pathPart = pathParts[i] ?? ''; 57 58 if (patternPart.startsWith(':')) { 59 params[patternPart.slice(1)] = pathPart; 60 } else if (patternPart !== pathPart) { 61 return null; 62 } 63 } 64 65 return params; 66} 67 68function jsonResponse(data: unknown, status = 200): Response { 69 return new Response(JSON.stringify(data), { 70 status, 71 headers: { 72 'Content-Type': 'application/json', 73 'Access-Control-Allow-Origin': '*', 74 }, 75 }); 76} 77 78// Handlers 79 80function handleGetDays(_req: Request, url: URL): Promise<Response> { 81 const limitParam = url.searchParams.get('limit'); 82 const days = limitParam !== null ? getDays(parseInt(limitParam, 10)) : getDays(); 83 return Promise.resolve(jsonResponse(days)); 84} 85 86function handleGetDayDetail(_req: Request, url: URL): Promise<Response> { 87 const params = (url as { params?: Record<string, string> }).params; 88 const date = params?.date; 89 90 if (date === undefined) { 91 return Promise.resolve(jsonResponse({ error: 'Missing date parameter' }, 400)); 92 } 93 94 const detail = getDayDetail(date); 95 if (detail === null) { 96 return Promise.resolve(jsonResponse({ error: 'Day not found' }, 404)); 97 } 98 99 return Promise.resolve(jsonResponse(detail)); 100} 101 102function handleGetDayBrag(_req: Request, url: URL): Promise<Response> { 103 const params = (url as { params?: Record<string, string> }).params; 104 const date = params?.date; 105 106 if (date === undefined) { 107 return Promise.resolve(jsonResponse({ error: 'Missing date parameter' }, 400)); 108 } 109 110 const detail = getDayDetail(date); 111 if (detail === null) { 112 return Promise.resolve(jsonResponse({ error: 'Day not found' }, 404)); 113 } 114 115 return Promise.resolve( 116 jsonResponse({ 117 date, 118 bragSummary: detail.bragSummary ?? 'No summary available', 119 projectCount: detail.projects.length, 120 sessionCount: detail.stats.totalSessions, 121 }), 122 ); 123} 124 125function handleGetStats(_req: Request, _url: URL): Promise<Response> { 126 const stats = getStats(); 127 return Promise.resolve(jsonResponse(stats)); 128} 129 130async function handleRefresh(_req: Request, _url: URL): Promise<Response> { 131 // Run processing in background 132 const startTime = Date.now(); 133 134 try { 135 const result = await processCommand({ 136 force: false, 137 verbose: false, 138 }); 139 140 return jsonResponse({ 141 success: true, 142 sessionsProcessed: result.sessionsProcessed, 143 errors: result.errors, 144 durationMs: Date.now() - startTime, 145 }); 146 } catch (error) { 147 return jsonResponse( 148 { 149 success: false, 150 error: error instanceof Error ? error.message : 'Unknown error', 151 }, 152 500, 153 ); 154 } 155} 156 157function handleGetProjects(_req: Request, url: URL): Promise<Response> { 158 const status = url.searchParams.get('status') as ProjectStatus | null; 159 const projects = getProjects(status ?? undefined); 160 return Promise.resolve(jsonResponse(projects)); 161} 162 163async function handleUpdateProjectStatus(req: Request, _url: URL): Promise<Response> { 164 const body = (await req.json()) as { path?: string; status?: ProjectStatus }; 165 166 if (body.path === undefined || body.status === undefined) { 167 return jsonResponse({ error: 'Missing path or status' }, 400); 168 } 169 170 const validStatuses: ProjectStatus[] = [ 171 'shipped', 172 'in_progress', 173 'ready_to_ship', 174 'abandoned', 175 'ignore', 176 'one_off', 177 'experiment', 178 ]; 179 if (!validStatuses.includes(body.status)) { 180 return jsonResponse({ error: 'Invalid status' }, 400); 181 } 182 183 const updated = updateProjectStatus(body.path, body.status); 184 if (!updated) { 185 return jsonResponse({ error: 'Project not found' }, 404); 186 } 187 188 return jsonResponse({ success: true }); 189}