this repo has no description
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}