Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: systemic — web UI module now calls sibling API endpoints

Root cause: the web module had no knowledge of other modules' mount
paths, so the LLM duplicated the entire API under /api/* prefixes.
The duplicate had bugs and created inconsistency.

Fix: architecture prompt now explicitly tells the web module to call
sibling modules at their mount paths (/tasks, /projects, /quick-stats).
Prompt builder includes mount path info for each sibling module.
Also fixed cmdRegen to pass architecture through RegenContext.

+790 -1258
examples/todo-app/data/app.db-shm

This is a binary file and will not be displayed.

examples/todo-app/data/app.db-wal

This is a binary file and will not be displayed.

+2 -481
examples/todo-app/src/generated/todos/data-integrity.ts
··· 1 1 import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register table migrations 6 - registerMigration('projects', ` 7 - CREATE TABLE IF NOT EXISTS projects ( 8 - id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL UNIQUE, 10 - color TEXT NOT NULL DEFAULT '#3b82f6', 11 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 - ) 13 - `); 14 - 15 - registerMigration('tasks', ` 16 - CREATE TABLE IF NOT EXISTS tasks ( 17 - id INTEGER PRIMARY KEY AUTOINCREMENT, 18 - title TEXT NOT NULL CHECK(length(title) > 0 AND length(title) <= 500), 19 - description TEXT NOT NULL DEFAULT '' CHECK(length(description) <= 5000), 20 - priority TEXT NOT NULL CHECK(priority IN ('urgent', 'high', 'normal', 'low')) DEFAULT 'normal', 21 - due_date TEXT, 22 - completed INTEGER NOT NULL DEFAULT 0 CHECK(completed IN (0, 1)), 23 - project_id INTEGER REFERENCES projects(id), 24 - created_at TEXT NOT NULL DEFAULT (datetime('now')), 25 - updated_at TEXT NOT NULL DEFAULT (datetime('now')) 26 - ) 27 - `); 28 - 29 - // Add trigger for updated_at 30 - registerMigration('tasks_updated_at_trigger', ` 31 - CREATE TRIGGER IF NOT EXISTS tasks_updated_at 32 - AFTER UPDATE ON tasks 33 - BEGIN 34 - UPDATE tasks SET updated_at = datetime('now') WHERE id = NEW.id; 35 - END 36 - `); 37 - 38 - const CreateTaskSchema = z.object({ 39 - title: z.string().min(1, 'Title cannot be empty').max(500, 'Title cannot exceed 500 characters'), 40 - description: z.string().max(5000, 'Description cannot exceed 5000 characters').optional().default(''), 41 - priority: z.enum(['urgent', 'high', 'normal', 'low']).default('normal'), 42 - due_date: z.string().refine((date) => { 43 - if (!date) return true; 44 - const parsed = new Date(date); 45 - return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 46 - }, 'Invalid due date').optional(), 47 - project_id: z.number().int().optional(), 48 - }); 49 - 50 - const UpdateTaskSchema = z.object({ 51 - title: z.string().min(1, 'Title cannot be empty').max(500, 'Title cannot exceed 500 characters').optional(), 52 - description: z.string().max(5000, 'Description cannot exceed 5000 characters').optional(), 53 - priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 54 - due_date: z.string().refine((date) => { 55 - if (!date) return true; 56 - const parsed = new Date(date); 57 - return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 58 - }, 'Invalid due date').nullable().optional(), 59 - completed: z.number().int().min(0).max(1).optional(), 60 - project_id: z.number().int().nullable().optional(), 61 - }); 62 - 63 - const CreateProjectSchema = z.object({ 64 - name: z.string().min(1, 'Project name cannot be empty').max(200, 'Project name cannot exceed 200 characters'), 65 - color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Color must be a valid hex color').default('#3b82f6'), 66 - }); 67 - 68 - const UpdateProjectSchema = z.object({ 69 - name: z.string().min(1, 'Project name cannot be empty').max(200, 'Project name cannot exceed 200 characters').optional(), 70 - color: z.string().regex(/^#[0-9a-fA-F]{6}$/, 'Color must be a valid hex color').optional(), 71 - }); 72 2 73 3 const router = new Hono(); 74 4 75 - // Task routes 76 - router.get('/tasks', (c) => { 77 - let sql = ` 78 - SELECT 79 - tasks.*, 80 - projects.name as project_name, 81 - projects.color as project_color, 82 - CASE 83 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 84 - ELSE 0 85 - END as overdue 86 - FROM tasks 87 - LEFT JOIN projects ON tasks.project_id = projects.id 88 - `; 89 - 90 - const conditions: string[] = []; 91 - const params: (string | number)[] = []; 92 - 93 - const status = c.req.query('status'); 94 - if (status === 'active') { 95 - conditions.push('tasks.completed = 0'); 96 - } else if (status === 'completed') { 97 - conditions.push('tasks.completed = 1'); 98 - } 99 - 100 - const priority = c.req.query('priority'); 101 - if (priority && ['urgent', 'high', 'normal', 'low'].includes(priority)) { 102 - conditions.push('tasks.priority = ?'); 103 - params.push(priority); 104 - } 105 - 106 - const projectId = c.req.query('project_id'); 107 - if (projectId !== undefined) { 108 - if (projectId === 'null' || projectId === '') { 109 - conditions.push('tasks.project_id IS NULL'); 110 - } else { 111 - conditions.push('tasks.project_id = ?'); 112 - params.push(Number(projectId)); 113 - } 114 - } 115 - 116 - if (conditions.length > 0) { 117 - sql += ' WHERE ' + conditions.join(' AND '); 118 - } 119 - 120 - sql += ` 121 - ORDER BY 122 - tasks.completed ASC, 123 - CASE 124 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 125 - ELSE 1 126 - END ASC, 127 - CASE tasks.priority 128 - WHEN 'urgent' THEN 0 129 - WHEN 'high' THEN 1 130 - WHEN 'normal' THEN 2 131 - WHEN 'low' THEN 3 132 - END ASC, 133 - tasks.created_at DESC 134 - `; 135 - 136 - const tasks = db.prepare(sql).all(...params); 137 - return c.json(tasks); 138 - }); 139 - 140 - router.get('/tasks/:id', (c) => { 141 - const task = db.prepare(` 142 - SELECT 143 - tasks.*, 144 - projects.name as project_name, 145 - projects.color as project_color, 146 - CASE 147 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 148 - ELSE 0 149 - END as overdue 150 - FROM tasks 151 - LEFT JOIN projects ON tasks.project_id = projects.id 152 - WHERE tasks.id = ? 153 - `).get(c.req.param('id')); 154 - 155 - if (!task) return c.json({ error: 'Task not found' }, 404); 156 - return c.json(task); 157 - }); 158 - 159 - router.post('/tasks', async (c) => { 160 - let body; 161 - try { 162 - body = await c.req.json(); 163 - } catch { 164 - return c.json({ error: 'Invalid JSON' }, 400); 165 - } 166 - 167 - const result = CreateTaskSchema.safeParse(body); 168 - if (!result.success) { 169 - return c.json({ error: result.error.issues[0].message }, 400); 170 - } 171 - 172 - const { title, description, priority, due_date, project_id } = result.data; 173 - 174 - // Validate project exists if specified 175 - if (project_id !== undefined) { 176 - const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 177 - if (!project) { 178 - return c.json({ error: 'Project not found' }, 400); 179 - } 180 - } 181 - 182 - try { 183 - const info = db.prepare(` 184 - INSERT INTO tasks (title, description, priority, due_date, project_id) 185 - VALUES (?, ?, ?, ?, ?) 186 - `).run(title, description, priority, due_date || null, project_id || null); 187 - 188 - const task = db.prepare(` 189 - SELECT 190 - tasks.*, 191 - projects.name as project_name, 192 - projects.color as project_color, 193 - CASE 194 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 195 - ELSE 0 196 - END as overdue 197 - FROM tasks 198 - LEFT JOIN projects ON tasks.project_id = projects.id 199 - WHERE tasks.id = ? 200 - `).get(info.lastInsertRowid); 201 - 202 - return c.json(task, 201); 203 - } catch (error) { 204 - if (error instanceof Error && error.message?.includes('CHECK constraint failed')) { 205 - return c.json({ error: 'Data validation failed' }, 400); 206 - } 207 - throw error; 208 - } 209 - }); 210 - 211 - router.patch('/tasks/:id', async (c) => { 212 - const id = c.req.param('id'); 213 - const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 214 - if (!existing) return c.json({ error: 'Task not found' }, 404); 215 - 216 - let body; 217 - try { 218 - body = await c.req.json(); 219 - } catch { 220 - return c.json({ error: 'Invalid JSON' }, 400); 221 - } 222 - 223 - const result = UpdateTaskSchema.safeParse(body); 224 - if (!result.success) { 225 - return c.json({ error: result.error.issues[0].message }, 400); 226 - } 227 - 228 - const updates = result.data; 229 - 230 - // Validate project exists if being updated 231 - if (updates.project_id !== undefined && updates.project_id !== null) { 232 - const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 233 - if (!project) { 234 - return c.json({ error: 'Project not found' }, 400); 235 - } 236 - } 237 - 238 - try { 239 - // Update fields individually to handle last-write-wins 240 - if (updates.title !== undefined) { 241 - db.prepare('UPDATE tasks SET title = ? WHERE id = ?').run(updates.title, id); 242 - } 243 - if (updates.description !== undefined) { 244 - db.prepare('UPDATE tasks SET description = ? WHERE id = ?').run(updates.description, id); 245 - } 246 - if (updates.priority !== undefined) { 247 - db.prepare('UPDATE tasks SET priority = ? WHERE id = ?').run(updates.priority, id); 248 - } 249 - if (updates.due_date !== undefined) { 250 - db.prepare('UPDATE tasks SET due_date = ? WHERE id = ?').run(updates.due_date, id); 251 - } 252 - if (updates.completed !== undefined) { 253 - db.prepare('UPDATE tasks SET completed = ? WHERE id = ?').run(updates.completed, id); 254 - } 255 - if (updates.project_id !== undefined) { 256 - db.prepare('UPDATE tasks SET project_id = ? WHERE id = ?').run(updates.project_id, id); 257 - } 258 - 259 - const updated = db.prepare(` 260 - SELECT 261 - tasks.*, 262 - projects.name as project_name, 263 - projects.color as project_color, 264 - CASE 265 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 1 266 - ELSE 0 267 - END as overdue 268 - FROM tasks 269 - LEFT JOIN projects ON tasks.project_id = projects.id 270 - WHERE tasks.id = ? 271 - `).get(id); 272 - 273 - return c.json(updated); 274 - } catch (error) { 275 - if (error instanceof Error && error.message?.includes('CHECK constraint failed')) { 276 - return c.json({ error: 'Data validation failed' }, 400); 277 - } 278 - throw error; 279 - } 280 - }); 281 - 282 - router.delete('/tasks/:id', (c) => { 283 - const id = c.req.param('id'); 284 - const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 285 - if (!existing) return c.json({ error: 'Task not found' }, 404); 286 - 287 - db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 288 - return c.body(null, 204); 289 - }); 290 - 291 - // Project routes 292 - router.get('/projects', (c) => { 293 - const projects = db.prepare(` 294 - SELECT 295 - projects.*, 296 - COUNT(tasks.id) as task_count, 297 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 298 - FROM projects 299 - LEFT JOIN tasks ON projects.id = tasks.project_id 300 - GROUP BY projects.id 301 - ORDER BY projects.name 302 - `).all(); 303 - return c.json(projects); 304 - }); 305 - 306 - router.get('/projects/:id', (c) => { 307 - const project = db.prepare(` 308 - SELECT 309 - projects.*, 310 - COUNT(tasks.id) as task_count, 311 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 312 - FROM projects 313 - LEFT JOIN tasks ON projects.id = tasks.project_id 314 - WHERE projects.id = ? 315 - GROUP BY projects.id 316 - `).get(c.req.param('id')); 317 - 318 - if (!project) return c.json({ error: 'Project not found' }, 404); 319 - return c.json(project); 320 - }); 321 - 322 - router.post('/projects', async (c) => { 323 - let body; 324 - try { 325 - body = await c.req.json(); 326 - } catch { 327 - return c.json({ error: 'Invalid JSON' }, 400); 328 - } 329 - 330 - const result = CreateProjectSchema.safeParse(body); 331 - if (!result.success) { 332 - return c.json({ error: result.error.issues[0].message }, 400); 333 - } 334 - 335 - const { name, color } = result.data; 336 - 337 - try { 338 - const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 339 - const project = db.prepare(` 340 - SELECT 341 - projects.*, 342 - 0 as task_count, 343 - 0 as active_task_count 344 - FROM projects 345 - WHERE id = ? 346 - `).get(info.lastInsertRowid); 347 - return c.json(project, 201); 348 - } catch (error) { 349 - if (error instanceof Error && error.message?.includes('UNIQUE constraint failed')) { 350 - return c.json({ error: 'Project name already exists' }, 400); 351 - } 352 - throw error; 353 - } 354 - }); 355 - 356 - router.patch('/projects/:id', async (c) => { 357 - const id = c.req.param('id'); 358 - const existing = db.prepare('SELECT * FROM projects WHERE id = ?').get(id); 359 - if (!existing) return c.json({ error: 'Project not found' }, 404); 360 - 361 - let body; 362 - try { 363 - body = await c.req.json(); 364 - } catch { 365 - return c.json({ error: 'Invalid JSON' }, 400); 366 - } 367 - 368 - const result = UpdateProjectSchema.safeParse(body); 369 - if (!result.success) { 370 - return c.json({ error: result.error.issues[0].message }, 400); 371 - } 372 - 373 - const updates = result.data; 374 - 375 - try { 376 - if (updates.name !== undefined) { 377 - db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 378 - } 379 - if (updates.color !== undefined) { 380 - db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 381 - } 382 - 383 - const updated = db.prepare(` 384 - SELECT 385 - projects.*, 386 - COUNT(tasks.id) as task_count, 387 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 388 - FROM projects 389 - LEFT JOIN tasks ON projects.id = tasks.project_id 390 - WHERE projects.id = ? 391 - GROUP BY projects.id 392 - `).get(id); 393 - 394 - return c.json(updated); 395 - } catch (error) { 396 - if (error instanceof Error && error.message?.includes('UNIQUE constraint failed')) { 397 - return c.json({ error: 'Project name already exists' }, 400); 398 - } 399 - throw error; 400 - } 401 - }); 402 - 403 - router.delete('/projects/:id', (c) => { 404 - const id = c.req.param('id'); 405 - const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 406 - if (!existing) return c.json({ error: 'Project not found' }, 404); 407 - 408 - // Check for dependent tasks - prevent cascade deletion 409 - const dependentTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 410 - if (dependentTasks.count > 0) { 411 - return c.json({ 412 - error: `Cannot delete project with ${dependentTasks.count} associated tasks. Please reassign or delete tasks first.` 413 - }, 400); 414 - } 415 - 416 - db.prepare('DELETE FROM projects WHERE id = ?').run(id); 417 - return c.body(null, 204); 418 - }); 419 - 420 - // Data integrity validation endpoint 421 - router.get('/validate', (c) => { 422 - const issues: string[] = []; 423 - 424 - // Check for orphaned tasks (referencing non-existent projects) 425 - const orphanedTasks = db.prepare(` 426 - SELECT COUNT(*) as count 427 - FROM tasks 428 - WHERE project_id IS NOT NULL 429 - AND project_id NOT IN (SELECT id FROM projects) 430 - `).get() as { count: number }; 431 - 432 - if (orphanedTasks.count > 0) { 433 - issues.push(`${orphanedTasks.count} tasks reference non-existent projects`); 434 - } 435 - 436 - // Check for invalid due dates 437 - const invalidDates = db.prepare(` 438 - SELECT COUNT(*) as count 439 - FROM tasks 440 - WHERE due_date IS NOT NULL 441 - AND (due_date = '' OR date(due_date) IS NULL) 442 - `).get() as { count: number }; 443 - 444 - if (invalidDates.count > 0) { 445 - issues.push(`${invalidDates.count} tasks have invalid due dates`); 446 - } 447 - 448 - // Check for constraint violations that might have slipped through 449 - const invalidTitles = db.prepare(` 450 - SELECT COUNT(*) as count 451 - FROM tasks 452 - WHERE title = '' OR length(title) > 500 453 - `).get() as { count: number }; 454 - 455 - if (invalidTitles.count > 0) { 456 - issues.push(`${invalidTitles.count} tasks have invalid titles`); 457 - } 458 - 459 - const invalidDescriptions = db.prepare(` 460 - SELECT COUNT(*) as count 461 - FROM tasks 462 - WHERE length(description) > 5000 463 - `).get() as { count: number }; 464 - 465 - if (invalidDescriptions.count > 0) { 466 - issues.push(`${invalidDescriptions.count} tasks have descriptions exceeding 5000 characters`); 467 - } 468 - 469 - const invalidPriorities = db.prepare(` 470 - SELECT COUNT(*) as count 471 - FROM tasks 472 - WHERE priority NOT IN ('urgent', 'high', 'normal', 'low') 473 - `).get() as { count: number }; 474 - 475 - if (invalidPriorities.count > 0) { 476 - issues.push(`${invalidPriorities.count} tasks have invalid priority values`); 477 - } 478 - 479 - return c.json({ 480 - valid: issues.length === 0, 481 - issues: issues, 482 - checked_at: new Date().toISOString() 483 - }); 484 - }); 5 + router.get('/', (c) => c.json({ stub: true, module: 'Data Integrity', message: 'Not yet implemented' })); 485 6 486 7 export default router; 487 8 ··· 491 12 name: 'Data Integrity', 492 13 risk_tier: 'high', 493 14 canon_ids: [9 as const], 494 - } as const; 15 + } as const;
+64 -72
examples/todo-app/src/generated/todos/filtering-and-views.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register migrations for tables this module touches 5 + // Register table migrations for tasks and projects 6 6 registerMigration('projects', ` 7 7 CREATE TABLE IF NOT EXISTS projects ( 8 8 id INTEGER PRIMARY KEY AUTOINCREMENT, ··· 16 16 CREATE TABLE IF NOT EXISTS tasks ( 17 17 id INTEGER PRIMARY KEY AUTOINCREMENT, 18 18 title TEXT NOT NULL, 19 - description TEXT DEFAULT '', 19 + description TEXT NOT NULL DEFAULT '', 20 20 priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 21 due_date TEXT, 22 22 completed INTEGER NOT NULL DEFAULT 0, ··· 25 25 ) 26 26 `); 27 27 28 + const FilterSchema = z.object({ 29 + status: z.enum(['all', 'active', 'completed']).optional(), 30 + project_id: z.string().optional(), 31 + priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 32 + }); 33 + 28 34 const router = new Hono(); 29 35 30 - // Get filtered tasks with combined filters 31 - router.get('/tasks', (c) => { 36 + // Get filtered tasks with current filter state 37 + router.get('/', (c) => { 38 + const filterResult = FilterSchema.safeParse({ 39 + status: c.req.query('status'), 40 + project_id: c.req.query('project_id'), 41 + priority: c.req.query('priority'), 42 + }); 43 + 44 + if (!filterResult.success) { 45 + return c.json({ error: 'Invalid filter parameters' }, 400); 46 + } 47 + 48 + const filters = filterResult.data; 49 + 32 50 let sql = ` 33 51 SELECT 34 52 tasks.*, ··· 46 64 const conditions: string[] = []; 47 65 const params: (string | number)[] = []; 48 66 49 - // Filter by completion status 50 - const status = c.req.query('status'); 51 - if (status === 'active') { 67 + // Status filter 68 + if (filters.status === 'active') { 52 69 conditions.push('tasks.completed = 0'); 53 - } else if (status === 'completed') { 70 + } else if (filters.status === 'completed') { 54 71 conditions.push('tasks.completed = 1'); 55 72 } 56 73 57 - // Filter by project 58 - const projectId = c.req.query('project_id'); 59 - if (projectId !== undefined) { 60 - if (projectId === 'inbox') { 74 + // Project filter 75 + if (filters.project_id) { 76 + if (filters.project_id === 'inbox') { 61 77 conditions.push('tasks.project_id IS NULL'); 62 78 } else { 63 79 conditions.push('tasks.project_id = ?'); 64 - params.push(Number(projectId)); 80 + params.push(Number(filters.project_id)); 65 81 } 66 82 } 67 83 68 - // Filter by priority 69 - const priority = c.req.query('priority'); 70 - if (priority && ['urgent', 'high', 'normal', 'low'].includes(priority)) { 84 + // Priority filter 85 + if (filters.priority) { 71 86 conditions.push('tasks.priority = ?'); 72 - params.push(priority); 87 + params.push(filters.priority); 73 88 } 74 89 75 90 if (conditions.length > 0) { 76 91 sql += ' WHERE ' + conditions.join(' AND '); 77 92 } 78 93 79 - // Sort by urgency and overdue status first 94 + // Sort by urgency and overdue status 80 95 sql += ` ORDER BY 96 + tasks.completed ASC, 81 97 is_overdue DESC, 82 98 CASE tasks.priority 83 99 WHEN 'urgent' THEN 0 84 100 WHEN 'high' THEN 1 85 101 WHEN 'normal' THEN 2 86 102 WHEN 'low' THEN 3 87 - END, 103 + END ASC, 104 + tasks.due_date ASC NULLS LAST, 88 105 tasks.created_at DESC 89 106 `; 90 107 91 108 const tasks = db.prepare(sql).all(...params); 109 + 110 + // Build current filter state description 111 + const filterState: string[] = []; 92 112 93 - // Build current filter state 94 - const filterState = { 95 - status: status || 'all', 96 - project_id: projectId || null, 97 - priority: priority || null, 98 - active_filters: [] as string[] 99 - }; 100 - 101 - if (status && status !== 'all') { 102 - filterState.active_filters.push(`Status: ${status}`); 113 + if (filters.status && filters.status !== 'all') { 114 + filterState.push(`Status: ${filters.status}`); 103 115 } 104 - if (projectId) { 105 - if (projectId === 'inbox') { 106 - filterState.active_filters.push('Project: Inbox'); 116 + 117 + if (filters.project_id) { 118 + if (filters.project_id === 'inbox') { 119 + filterState.push('Project: Inbox'); 107 120 } else { 108 - const project = db.prepare('SELECT name FROM projects WHERE id = ?').get(projectId) as { name: string } | undefined; 121 + const project = db.prepare('SELECT name FROM projects WHERE id = ?').get(Number(filters.project_id)) as { name: string } | undefined; 109 122 if (project) { 110 - filterState.active_filters.push(`Project: ${project.name}`); 123 + filterState.push(`Project: ${project.name}`); 111 124 } 112 125 } 113 126 } 114 - if (priority) { 115 - filterState.active_filters.push(`Priority: ${priority}`); 127 + 128 + if (filters.priority) { 129 + filterState.push(`Priority: ${filters.priority}`); 116 130 } 117 131 118 132 return c.json({ 119 133 tasks, 120 - filter_state: filterState 134 + filter_state: { 135 + active_filters: filters, 136 + description: filterState.length > 0 ? filterState.join(', ') : 'All tasks', 137 + count: Array.isArray(tasks) ? tasks.length : 0 138 + } 121 139 }); 122 140 }); 123 141 124 - // Get filter options for dropdowns 125 - router.get('/filter-options', (c) => { 142 + // Get available filter options 143 + router.get('/options', (c) => { 126 144 const projects = db.prepare('SELECT id, name, color FROM projects ORDER BY name').all(); 127 145 const priorities = ['urgent', 'high', 'normal', 'low']; 128 - const statuses = [ 129 - { value: 'all', label: 'All' }, 130 - { value: 'active', label: 'Active' }, 131 - { value: 'completed', label: 'Completed' } 132 - ]; 146 + const statuses = ['all', 'active', 'completed']; 133 147 134 148 return c.json({ 135 149 projects: [ ··· 141 155 }); 142 156 }); 143 157 144 - // Get tasks count by filter combinations (for stats) 145 - router.get('/filter-stats', (c) => { 146 - const stats = { 147 - total: db.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number }, 148 - active: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 0').get() as { count: number }, 149 - completed: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get() as { count: number }, 150 - overdue: db.prepare('SELECT COUNT(*) as count FROM tasks WHERE due_date < date("now") AND completed = 0').get() as { count: number }, 151 - by_priority: db.prepare(` 152 - SELECT 153 - priority, 154 - COUNT(*) as count, 155 - COUNT(CASE WHEN completed = 0 THEN 1 END) as active_count 156 - FROM tasks 157 - GROUP BY priority 158 - `).all(), 159 - by_project: db.prepare(` 160 - SELECT 161 - COALESCE(projects.name, 'Inbox') as project_name, 162 - COALESCE(projects.id, 'inbox') as project_id, 163 - COUNT(*) as count, 164 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_count 165 - FROM tasks 166 - LEFT JOIN projects ON tasks.project_id = projects.id 167 - GROUP BY tasks.project_id, projects.name 168 - `).all() 169 - }; 170 - 171 - return c.json(stats); 158 + // Clear all filters 159 + router.delete('/filters', (c) => { 160 + return c.json({ 161 + message: 'Filters cleared', 162 + redirect_url: '/' 163 + }); 172 164 }); 173 165 174 166 export default router;
+66 -126
examples/todo-app/src/generated/todos/integration.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register migrations for both tasks and projects tables 5 + // Register migrations for tasks and projects tables 6 6 registerMigration('projects', ` 7 7 CREATE TABLE IF NOT EXISTS projects ( 8 8 id INTEGER PRIMARY KEY AUTOINCREMENT, ··· 66 66 const params: (string | number)[] = []; 67 67 68 68 const status = c.req.query('status'); 69 - if (status === 'active') { 70 - conditions.push('tasks.completed = 0'); 71 - } else if (status === 'completed') { 72 - conditions.push('tasks.completed = 1'); 73 - } 69 + if (status === 'active') { conditions.push('tasks.completed = 0'); } 70 + else if (status === 'completed') { conditions.push('tasks.completed = 1'); } 74 71 75 72 const priority = c.req.query('priority'); 76 - if (priority) { 77 - conditions.push('tasks.priority = ?'); 78 - params.push(priority); 79 - } 73 + if (priority) { conditions.push('tasks.priority = ?'); params.push(priority); } 80 74 81 75 const projectId = c.req.query('project_id'); 82 - if (projectId) { 83 - conditions.push('tasks.project_id = ?'); 84 - params.push(Number(projectId)); 85 - } 76 + if (projectId) { conditions.push('tasks.project_id = ?'); params.push(Number(projectId)); } 86 77 87 - if (conditions.length > 0) { 88 - sql += ' WHERE ' + conditions.join(' AND '); 89 - } 90 - 78 + if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND '); 79 + 91 80 sql += ` ORDER BY 92 - tasks.completed ASC, 93 81 CASE tasks.priority 94 82 WHEN 'urgent' THEN 0 95 83 WHEN 'high' THEN 1 ··· 100 88 WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 101 89 ELSE 1 102 90 END, 103 - tasks.due_date ASC NULLS LAST, 104 91 tasks.created_at DESC 105 92 `; 106 93 ··· 114 101 LEFT JOIN projects ON tasks.project_id = projects.id 115 102 WHERE tasks.id = ? 116 103 `).get(c.req.param('id')); 117 - 118 104 if (!task) return c.json({ error: 'Task not found' }, 404); 119 105 return c.json(task); 120 106 }); 121 107 122 108 router.post('/tasks', async (c) => { 123 109 let body; 124 - try { 125 - body = await c.req.json(); 126 - } catch { 127 - return c.json({ error: 'Invalid JSON' }, 400); 128 - } 129 - 110 + try { body = await c.req.json(); } 111 + catch { return c.json({ error: 'Invalid JSON' }, 400); } 112 + 130 113 const result = CreateTaskSchema.safeParse(body); 131 - if (!result.success) { 132 - return c.json({ error: result.error.issues[0].message }, 400); 133 - } 134 - 114 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 115 + 135 116 const { title, description, priority, due_date, project_id } = result.data; 136 - 117 + 137 118 if (project_id !== null && project_id !== undefined) { 138 119 const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 139 - if (!project) { 140 - return c.json({ error: 'Project not found' }, 400); 141 - } 120 + if (!project) return c.json({ error: 'Project not found' }, 400); 142 121 } 143 - 122 + 144 123 const info = db.prepare(` 145 124 INSERT INTO tasks (title, description, priority, due_date, project_id) 146 125 VALUES (?, ?, ?, ?, ?) 147 126 `).run(title, description, priority, due_date, project_id); 148 - 127 + 149 128 const task = db.prepare(` 150 129 SELECT tasks.*, projects.name as project_name, projects.color as project_color 151 130 FROM tasks 152 131 LEFT JOIN projects ON tasks.project_id = projects.id 153 132 WHERE tasks.id = ? 154 133 `).get(info.lastInsertRowid); 155 - 134 + 156 135 return c.json(task, 201); 157 136 }); 158 137 ··· 160 139 const id = c.req.param('id'); 161 140 const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 162 141 if (!existing) return c.json({ error: 'Task not found' }, 404); 163 - 142 + 164 143 let body; 165 - try { 166 - body = await c.req.json(); 167 - } catch { 168 - return c.json({ error: 'Invalid JSON' }, 400); 169 - } 170 - 144 + try { body = await c.req.json(); } 145 + catch { return c.json({ error: 'Invalid JSON' }, 400); } 146 + 171 147 const result = UpdateTaskSchema.safeParse(body); 172 - if (!result.success) { 173 - return c.json({ error: result.error.issues[0].message }, 400); 174 - } 175 - 148 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 149 + 176 150 const updates = result.data; 177 - 151 + 178 152 if (updates.project_id !== undefined && updates.project_id !== null) { 179 153 const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 180 - if (!project) { 181 - return c.json({ error: 'Project not found' }, 400); 182 - } 183 - } 184 - 185 - if (updates.title !== undefined) { 186 - db.prepare('UPDATE tasks SET title = ?, updated_at = datetime("now") WHERE id = ?').run(updates.title, id); 187 - } 188 - if (updates.description !== undefined) { 189 - db.prepare('UPDATE tasks SET description = ?, updated_at = datetime("now") WHERE id = ?').run(updates.description, id); 190 - } 191 - if (updates.priority !== undefined) { 192 - db.prepare('UPDATE tasks SET priority = ?, updated_at = datetime("now") WHERE id = ?').run(updates.priority, id); 193 - } 194 - if (updates.due_date !== undefined) { 195 - db.prepare('UPDATE tasks SET due_date = ?, updated_at = datetime("now") WHERE id = ?').run(updates.due_date, id); 196 - } 197 - if (updates.completed !== undefined) { 198 - db.prepare('UPDATE tasks SET completed = ?, updated_at = datetime("now") WHERE id = ?').run(updates.completed, id); 199 - } 200 - if (updates.project_id !== undefined) { 201 - db.prepare('UPDATE tasks SET project_id = ?, updated_at = datetime("now") WHERE id = ?').run(updates.project_id, id); 154 + if (!project) return c.json({ error: 'Project not found' }, 400); 202 155 } 203 - 156 + 157 + if (updates.title !== undefined) db.prepare('UPDATE tasks SET title = ?, updated_at = datetime("now") WHERE id = ?').run(updates.title, id); 158 + if (updates.description !== undefined) db.prepare('UPDATE tasks SET description = ?, updated_at = datetime("now") WHERE id = ?').run(updates.description, id); 159 + if (updates.priority !== undefined) db.prepare('UPDATE tasks SET priority = ?, updated_at = datetime("now") WHERE id = ?').run(updates.priority, id); 160 + if (updates.due_date !== undefined) db.prepare('UPDATE tasks SET due_date = ?, updated_at = datetime("now") WHERE id = ?').run(updates.due_date, id); 161 + if (updates.completed !== undefined) db.prepare('UPDATE tasks SET completed = ?, updated_at = datetime("now") WHERE id = ?').run(updates.completed, id); 162 + if (updates.project_id !== undefined) db.prepare('UPDATE tasks SET project_id = ?, updated_at = datetime("now") WHERE id = ?').run(updates.project_id, id); 163 + 204 164 const updated = db.prepare(` 205 165 SELECT tasks.*, projects.name as project_name, projects.color as project_color 206 166 FROM tasks 207 167 LEFT JOIN projects ON tasks.project_id = projects.id 208 168 WHERE tasks.id = ? 209 169 `).get(id); 210 - 170 + 211 171 return c.json(updated); 212 172 }); 213 173 ··· 215 175 const id = c.req.param('id'); 216 176 const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 217 177 if (!existing) return c.json({ error: 'Task not found' }, 404); 218 - 178 + 219 179 db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 220 180 return c.body(null, 204); 221 181 }); ··· 224 184 router.get('/projects', (c) => { 225 185 const projects = db.prepare(` 226 186 SELECT projects.*, 227 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 228 - COUNT(tasks.id) as total_task_count 187 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 229 188 FROM projects 230 189 LEFT JOIN tasks ON projects.id = tasks.project_id 231 190 GROUP BY projects.id 232 191 ORDER BY projects.created_at DESC 233 192 `).all(); 234 - 235 193 return c.json(projects); 236 194 }); 237 195 238 196 router.get('/projects/:id', (c) => { 239 197 const project = db.prepare(` 240 198 SELECT projects.*, 241 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 242 - COUNT(tasks.id) as total_task_count 199 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 243 200 FROM projects 244 201 LEFT JOIN tasks ON projects.id = tasks.project_id 245 202 WHERE projects.id = ? 246 203 GROUP BY projects.id 247 204 `).get(c.req.param('id')); 248 - 249 205 if (!project) return c.json({ error: 'Project not found' }, 404); 250 206 return c.json(project); 251 207 }); 252 208 253 209 router.post('/projects', async (c) => { 254 210 let body; 255 - try { 256 - body = await c.req.json(); 257 - } catch { 258 - return c.json({ error: 'Invalid JSON' }, 400); 259 - } 260 - 211 + try { body = await c.req.json(); } 212 + catch { return c.json({ error: 'Invalid JSON' }, 400); } 213 + 261 214 const result = CreateProjectSchema.safeParse(body); 262 - if (!result.success) { 263 - return c.json({ error: result.error.issues[0].message }, 400); 264 - } 265 - 215 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 216 + 266 217 const { name, color } = result.data; 267 - 218 + 268 219 try { 269 220 const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 270 221 const project = db.prepare(` 271 222 SELECT projects.*, 272 - 0 as active_task_count, 273 - 0 as total_task_count 223 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 274 224 FROM projects 225 + LEFT JOIN tasks ON projects.id = tasks.project_id 275 226 WHERE projects.id = ? 227 + GROUP BY projects.id 276 228 `).get(info.lastInsertRowid); 277 - 278 229 return c.json(project, 201); 279 - } catch (error: any) { 280 - if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 230 + } catch (error: unknown) { 231 + if (error instanceof Error && 'code' in error && (error as any).code === 'SQLITE_CONSTRAINT_UNIQUE') { 281 232 return c.json({ error: 'Project name already exists' }, 400); 282 233 } 283 234 throw error; ··· 288 239 const id = c.req.param('id'); 289 240 const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 290 241 if (!existing) return c.json({ error: 'Project not found' }, 404); 291 - 242 + 292 243 let body; 293 - try { 294 - body = await c.req.json(); 295 - } catch { 296 - return c.json({ error: 'Invalid JSON' }, 400); 297 - } 298 - 244 + try { body = await c.req.json(); } 245 + catch { return c.json({ error: 'Invalid JSON' }, 400); } 246 + 299 247 const result = UpdateProjectSchema.safeParse(body); 300 - if (!result.success) { 301 - return c.json({ error: result.error.issues[0].message }, 400); 302 - } 303 - 248 + if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 249 + 304 250 const updates = result.data; 305 - 251 + 306 252 try { 307 - if (updates.name !== undefined) { 308 - db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 309 - } 310 - if (updates.color !== undefined) { 311 - db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 312 - } 313 - 253 + if (updates.name !== undefined) db.prepare('UPDATE projects SET name = ? WHERE id = ?').run(updates.name, id); 254 + if (updates.color !== undefined) db.prepare('UPDATE projects SET color = ? WHERE id = ?').run(updates.color, id); 255 + 314 256 const updated = db.prepare(` 315 257 SELECT projects.*, 316 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count, 317 - COUNT(tasks.id) as total_task_count 258 + COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 318 259 FROM projects 319 260 LEFT JOIN tasks ON projects.id = tasks.project_id 320 261 WHERE projects.id = ? 321 262 GROUP BY projects.id 322 263 `).get(id); 323 - 264 + 324 265 return c.json(updated); 325 - } catch (error: any) { 326 - if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 266 + } catch (error: unknown) { 267 + if (error instanceof Error && 'code' in error && (error as any).code === 'SQLITE_CONSTRAINT_UNIQUE') { 327 268 return c.json({ error: 'Project name already exists' }, 400); 328 269 } 329 270 throw error; ··· 334 275 const id = c.req.param('id'); 335 276 const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 336 277 if (!existing) return c.json({ error: 'Project not found' }, 404); 337 - 338 - // Check for dependent tasks 339 - const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 340 - if (taskCount.count > 0) { 278 + 279 + const dependentTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 280 + if (dependentTasks.count > 0) { 341 281 return c.json({ error: 'Cannot delete project with existing tasks' }, 400); 342 282 } 343 - 283 + 344 284 db.prepare('DELETE FROM projects WHERE id = ?').run(id); 345 285 return c.body(null, 204); 346 286 });
+23 -43
examples/todo-app/src/generated/todos/projects.ts
··· 41 41 router.get('/', (c) => { 42 42 const projects = db.prepare(` 43 43 SELECT 44 - projects.*, 45 - COALESCE(task_counts.active_count, 0) as active_task_count 46 - FROM projects 47 - LEFT JOIN ( 48 - SELECT 49 - project_id, 50 - COUNT(*) as active_count 51 - FROM tasks 52 - WHERE completed = 0 53 - GROUP BY project_id 54 - ) task_counts ON projects.id = task_counts.project_id 55 - ORDER BY projects.created_at DESC 44 + p.*, 45 + COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 46 + FROM projects p 47 + LEFT JOIN tasks t ON p.id = t.project_id 48 + GROUP BY p.id, p.name, p.color, p.created_at 49 + ORDER BY p.name 56 50 `).all(); 57 51 return c.json(projects); 58 52 }); ··· 61 55 router.get('/:id', (c) => { 62 56 const project = db.prepare(` 63 57 SELECT 64 - projects.*, 65 - COALESCE(task_counts.active_count, 0) as active_task_count 66 - FROM projects 67 - LEFT JOIN ( 68 - SELECT 69 - project_id, 70 - COUNT(*) as active_count 71 - FROM tasks 72 - WHERE completed = 0 73 - GROUP BY project_id 74 - ) task_counts ON projects.id = task_counts.project_id 75 - WHERE projects.id = ? 58 + p.*, 59 + COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 60 + FROM projects p 61 + LEFT JOIN tasks t ON p.id = t.project_id 62 + WHERE p.id = ? 63 + GROUP BY p.id, p.name, p.color, p.created_at 76 64 `).get(c.req.param('id')); 77 - 78 65 if (!project) return c.json({ error: 'Project not found' }, 404); 79 66 return c.json(project); 80 67 }); ··· 99 86 const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 100 87 const project = db.prepare(` 101 88 SELECT 102 - projects.*, 89 + p.*, 103 90 0 as active_task_count 104 - FROM projects 105 - WHERE projects.id = ? 91 + FROM projects p 92 + WHERE p.id = ? 106 93 `).get(info.lastInsertRowid); 107 94 return c.json(project, 201); 108 95 } catch (error: any) { 109 - if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 96 + if (error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 110 97 return c.json({ error: 'Project name already exists' }, 400); 111 98 } 112 99 throw error; ··· 143 130 144 131 const updated = db.prepare(` 145 132 SELECT 146 - projects.*, 147 - COALESCE(task_counts.active_count, 0) as active_task_count 148 - FROM projects 149 - LEFT JOIN ( 150 - SELECT 151 - project_id, 152 - COUNT(*) as active_count 153 - FROM tasks 154 - WHERE completed = 0 155 - GROUP BY project_id 156 - ) task_counts ON projects.id = task_counts.project_id 157 - WHERE projects.id = ? 133 + p.*, 134 + COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 135 + FROM projects p 136 + LEFT JOIN tasks t ON p.id = t.project_id 137 + WHERE p.id = ? 138 + GROUP BY p.id, p.name, p.color, p.created_at 158 139 `).get(id); 159 - 160 140 return c.json(updated); 161 141 } catch (error: any) { 162 - if (error && typeof error === 'object' && 'code' in error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 142 + if (error && error.code === 'SQLITE_CONSTRAINT_UNIQUE') { 163 143 return c.json({ error: 'Project name already exists' }, 400); 164 144 } 165 145 throw error;
+5 -9
examples/todo-app/src/generated/todos/quick-stats.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register migrations for tables this module reads from 5 + // Register table migrations for tasks and projects 6 6 registerMigration('projects', ` 7 7 CREATE TABLE IF NOT EXISTS projects ( 8 8 id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL UNIQUE, 9 + name TEXT NOT NULL, 10 10 color TEXT NOT NULL DEFAULT '#3b82f6', 11 11 created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 12 ) ··· 16 16 CREATE TABLE IF NOT EXISTS tasks ( 17 17 id INTEGER PRIMARY KEY AUTOINCREMENT, 18 18 title TEXT NOT NULL, 19 - description TEXT NOT NULL DEFAULT '', 19 + description TEXT DEFAULT '', 20 20 priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 21 due_date TEXT, 22 22 completed INTEGER NOT NULL DEFAULT 0, ··· 27 27 28 28 const router = new Hono(); 29 29 30 - // Get quick stats summary 30 + // Get stats summary 31 31 router.get('/', (c) => { 32 - // Total tasks 33 32 const totalTasks = db.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number }; 34 33 35 - // Completed tasks 36 34 const completedTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get() as { count: number }; 37 35 38 - // Overdue tasks (due date is past and not completed) 39 36 const overdueTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE due_date < date("now") AND completed = 0').get() as { count: number }; 40 37 41 - // Calculate completion percentage 42 38 const completionPercentage = totalTasks.count > 0 43 - ? Math.round((completedTasks.count / totalTasks.count) * 100) 39 + ? Math.round((completedTasks.count / totalTasks.count) * 100) 44 40 : 0; 45 41 46 42 return c.json({
+61 -16
examples/todo-app/src/generated/todos/tasks.ts
··· 11 11 priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 12 12 due_date TEXT, 13 13 completed INTEGER NOT NULL DEFAULT 0, 14 - project_id INTEGER, 14 + project_id INTEGER REFERENCES projects(id), 15 15 created_at TEXT NOT NULL DEFAULT (datetime('now')) 16 16 ) 17 17 `); ··· 37 37 38 38 // List all tasks with filtering and sorting 39 39 router.get('/', (c) => { 40 - let sql = 'SELECT * FROM tasks'; 40 + let sql = ` 41 + SELECT tasks.*, projects.name as project_name 42 + FROM tasks 43 + LEFT JOIN projects ON tasks.project_id = projects.id 44 + `; 41 45 const conditions: string[] = []; 42 46 const params: (string | number)[] = []; 43 47 44 48 const status = c.req.query('status'); 45 49 if (status === 'active') { 46 - conditions.push('completed = 0'); 50 + conditions.push('tasks.completed = 0'); 47 51 } else if (status === 'completed') { 48 - conditions.push('completed = 1'); 52 + conditions.push('tasks.completed = 1'); 49 53 } 50 54 51 55 const priority = c.req.query('priority'); 52 56 if (priority) { 53 - conditions.push('priority = ?'); 57 + conditions.push('tasks.priority = ?'); 54 58 params.push(priority); 55 59 } 56 60 57 61 const projectId = c.req.query('project_id'); 58 62 if (projectId) { 59 - conditions.push('project_id = ?'); 63 + conditions.push('tasks.project_id = ?'); 60 64 params.push(Number(projectId)); 61 65 } 62 66 ··· 64 68 sql += ' WHERE ' + conditions.join(' AND '); 65 69 } 66 70 67 - // Sort by urgency and overdue status first, then by created_at 71 + // Sort by urgency and overdue status first, then by creation date 68 72 sql += ` ORDER BY 69 - CASE WHEN due_date < date('now') AND completed = 0 THEN 0 ELSE 1 END, 70 - CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END, 71 - created_at DESC`; 73 + CASE WHEN tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 ELSE 1 END, 74 + CASE tasks.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END, 75 + tasks.created_at DESC 76 + `; 72 77 73 78 const tasks = db.prepare(sql).all(...params); 74 79 return c.json(tasks); ··· 76 81 77 82 // Get single task 78 83 router.get('/:id', (c) => { 79 - const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(c.req.param('id')); 84 + const task = db.prepare(` 85 + SELECT tasks.*, projects.name as project_name 86 + FROM tasks 87 + LEFT JOIN projects ON tasks.project_id = projects.id 88 + WHERE tasks.id = ? 89 + `).get(c.req.param('id')); 90 + 80 91 if (!task) return c.json({ error: 'Task not found' }, 404); 81 92 return c.json(task); 82 93 }); ··· 97 108 98 109 const { title, description, priority, due_date, project_id } = result.data; 99 110 111 + // Validate project exists if provided 112 + if (project_id !== undefined && project_id !== null) { 113 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 114 + if (!project) { 115 + return c.json({ error: 'Project not found' }, 400); 116 + } 117 + } 118 + 100 119 const info = db.prepare(` 101 120 INSERT INTO tasks (title, description, priority, due_date, project_id) 102 121 VALUES (?, ?, ?, ?, ?) 103 122 `).run(title, description, priority, due_date, project_id); 104 123 105 - const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(info.lastInsertRowid); 124 + const task = db.prepare(` 125 + SELECT tasks.*, projects.name as project_name 126 + FROM tasks 127 + LEFT JOIN projects ON tasks.project_id = projects.id 128 + WHERE tasks.id = ? 129 + `).get(info.lastInsertRowid); 130 + 106 131 return c.json(task, 201); 107 132 }); 108 133 ··· 110 135 router.patch('/:id', async (c) => { 111 136 const id = c.req.param('id'); 112 137 const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 113 - if (!existing) return c.json({ error: 'Task not found' }, 404); 138 + if (!existing) { 139 + return c.json({ error: 'Task not found' }, 404); 140 + } 114 141 115 142 let body; 116 143 try { ··· 125 152 } 126 153 127 154 const updates = result.data; 128 - 155 + 156 + // Validate project exists if being updated 157 + if (updates.project_id !== undefined && updates.project_id !== null) { 158 + const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 159 + if (!project) { 160 + return c.json({ error: 'Project not found' }, 400); 161 + } 162 + } 163 + 164 + // Apply updates 129 165 if (updates.title !== undefined) { 130 166 db.prepare('UPDATE tasks SET title = ? WHERE id = ?').run(updates.title, id); 131 167 } ··· 145 181 db.prepare('UPDATE tasks SET project_id = ? WHERE id = ?').run(updates.project_id, id); 146 182 } 147 183 148 - const updated = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 184 + const updated = db.prepare(` 185 + SELECT tasks.*, projects.name as project_name 186 + FROM tasks 187 + LEFT JOIN projects ON tasks.project_id = projects.id 188 + WHERE tasks.id = ? 189 + `).get(id); 190 + 149 191 return c.json(updated); 150 192 }); 151 193 ··· 153 195 router.delete('/:id', (c) => { 154 196 const id = c.req.param('id'); 155 197 const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 156 - if (!existing) return c.json({ error: 'Task not found' }, 404); 198 + if (!existing) { 199 + return c.json({ error: 'Task not found' }, 404); 200 + } 157 201 158 202 db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 159 203 return c.body(null, 204); ··· 161 205 162 206 export default router; 163 207 208 + /** @internal Phoenix VCS traceability — do not remove. */ 164 209 export const _phoenix = { 165 210 iu_id: '1628f3b0f6e0816a698cf8b53a7b135c5dc11469a9bca1fa49299db6018b08f7', 166 211 name: 'Tasks',
+550 -506
examples/todo-app/src/generated/todos/web-experience.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register migrations for all tables this module touches 6 - registerMigration('projects', ` 7 - CREATE TABLE IF NOT EXISTS projects ( 8 - id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL UNIQUE, 10 - color TEXT NOT NULL DEFAULT '#3b82f6', 11 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 12 - ) 13 - `); 14 - 15 - registerMigration('tasks', ` 16 - CREATE TABLE IF NOT EXISTS tasks ( 17 - id INTEGER PRIMARY KEY AUTOINCREMENT, 18 - title TEXT NOT NULL, 19 - description TEXT NOT NULL DEFAULT '', 20 - priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 21 - due_date TEXT, 22 - completed INTEGER NOT NULL DEFAULT 0, 23 - project_id INTEGER REFERENCES projects(id), 24 - created_at TEXT NOT NULL DEFAULT (datetime('now')) 25 - ) 26 - `); 27 - 28 - const createTaskSchema = z.object({ 29 - title: z.string().min(1), 30 - description: z.string().default(''), 31 - priority: z.enum(['urgent', 'high', 'normal', 'low']).default('normal'), 32 - due_date: z.string().nullable().optional(), 33 - project_id: z.number().nullable().optional() 34 - }); 35 - 36 - const updateTaskSchema = z.object({ 37 - title: z.string().min(1).optional(), 38 - description: z.string().optional(), 39 - priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 40 - due_date: z.string().nullable().optional(), 41 - completed: z.number().min(0).max(1).optional(), 42 - project_id: z.number().nullable().optional() 43 - }); 44 - 45 - const createProjectSchema = z.object({ 46 - name: z.string().min(1), 47 - color: z.string().default('#3b82f6') 48 - }); 49 - 50 5 const router = new Hono(); 51 6 52 - // Web interface route 53 7 router.get('/', (c) => { 54 8 return c.html(`<!DOCTYPE html> 55 9 <html lang="en"> ··· 66 20 67 21 body { 68 22 font-family: system-ui, -apple-system, sans-serif; 69 - background-color: #f8fafc; 70 - color: #1e293b; 71 - line-height: 1.6; 23 + background: #f8f9fa; 24 + color: #333; 25 + line-height: 1.5; 72 26 } 73 27 74 28 .container { ··· 83 37 } 84 38 85 39 .header h1 { 86 - color: #0f172a; 40 + color: #2563eb; 87 41 margin-bottom: 10px; 88 42 } 89 43 90 - .stats { 44 + .sidebar { 45 + background: white; 46 + border-radius: 8px; 47 + padding: 20px; 48 + margin-bottom: 20px; 49 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 50 + } 51 + 52 + .sidebar h3 { 53 + margin-bottom: 15px; 54 + color: #374151; 55 + } 56 + 57 + .project-list { 91 58 display: flex; 92 - gap: 20px; 93 - justify-content: center; 94 - margin-bottom: 30px; 95 59 flex-wrap: wrap; 60 + gap: 10px; 61 + margin-bottom: 20px; 96 62 } 97 63 98 - .stat-card { 99 - background: white; 100 - padding: 15px 20px; 101 - border-radius: 8px; 102 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 103 - text-align: center; 104 - min-width: 120px; 64 + .project-item { 65 + display: flex; 66 + align-items: center; 67 + gap: 8px; 68 + padding: 8px 12px; 69 + background: #f3f4f6; 70 + border-radius: 6px; 71 + cursor: pointer; 72 + border: 2px solid transparent; 73 + transition: all 0.2s; 105 74 } 106 75 107 - .stat-number { 108 - font-size: 24px; 109 - font-weight: bold; 110 - color: #3b82f6; 76 + .project-item:hover { 77 + background: #e5e7eb; 111 78 } 112 79 113 - .stat-label { 114 - font-size: 14px; 115 - color: #64748b; 80 + .project-item.active { 81 + border-color: #2563eb; 82 + background: #eff6ff; 83 + } 84 + 85 + .project-dot { 86 + width: 12px; 87 + height: 12px; 88 + border-radius: 50%; 89 + } 90 + 91 + .project-count { 92 + background: #6b7280; 93 + color: white; 94 + padding: 2px 6px; 95 + border-radius: 10px; 96 + font-size: 12px; 116 97 } 117 98 118 99 .add-task-form { 119 100 background: white; 120 - padding: 20px; 121 101 border-radius: 8px; 122 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 123 - margin-bottom: 30px; 102 + padding: 20px; 103 + margin-bottom: 20px; 104 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 105 + } 106 + 107 + .add-task-form h3 { 108 + margin-bottom: 15px; 109 + color: #374151; 124 110 } 125 111 126 112 .form-row { ··· 135 121 min-width: 200px; 136 122 } 137 123 138 - .form-group.full-width { 139 - flex: 100%; 140 - } 141 - 142 - label { 124 + .form-group label { 143 125 display: block; 144 126 margin-bottom: 5px; 145 127 font-weight: 500; 146 128 color: #374151; 147 129 } 148 130 149 - input, textarea, select { 131 + .form-group input, 132 + .form-group select, 133 + .form-group textarea { 150 134 width: 100%; 151 135 padding: 8px 12px; 152 136 border: 1px solid #d1d5db; ··· 154 138 font-size: 14px; 155 139 } 156 140 157 - textarea { 141 + .form-group textarea { 158 142 resize: vertical; 159 143 min-height: 80px; 160 144 } ··· 162 146 .description-toggle { 163 147 background: none; 164 148 border: none; 165 - color: #3b82f6; 149 + color: #2563eb; 166 150 cursor: pointer; 167 151 font-size: 14px; 168 - text-decoration: underline; 169 152 margin-bottom: 10px; 170 153 } 171 154 ··· 178 161 } 179 162 180 163 .btn { 181 - background: #3b82f6; 182 - color: white; 183 - border: none; 184 164 padding: 10px 20px; 165 + border: none; 185 166 border-radius: 6px; 186 167 cursor: pointer; 187 168 font-size: 14px; 188 169 font-weight: 500; 170 + transition: all 0.2s; 189 171 } 190 172 191 - .btn:hover { 173 + .btn-primary { 192 174 background: #2563eb; 175 + color: white; 176 + } 177 + 178 + .btn-primary:hover { 179 + background: #1d4ed8; 193 180 } 194 181 195 182 .filters { 183 + background: white; 184 + border-radius: 8px; 185 + padding: 20px; 186 + margin-bottom: 20px; 187 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 188 + } 189 + 190 + .filter-row { 196 191 display: flex; 197 192 gap: 15px; 198 - margin-bottom: 20px; 199 - flex-wrap: wrap; 200 193 align-items: center; 194 + flex-wrap: wrap; 201 195 } 202 196 203 - .filter-group { 197 + .filter-buttons { 204 198 display: flex; 205 199 gap: 5px; 206 200 } ··· 212 206 border-radius: 6px; 213 207 cursor: pointer; 214 208 font-size: 14px; 209 + transition: all 0.2s; 215 210 } 216 211 217 212 .filter-btn.active { 218 - background: #3b82f6; 213 + background: #2563eb; 219 214 color: white; 220 - border-color: #3b82f6; 215 + border-color: #2563eb; 216 + } 217 + 218 + .stats { 219 + background: white; 220 + border-radius: 8px; 221 + padding: 20px; 222 + margin-bottom: 20px; 223 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 224 + } 225 + 226 + .stats-grid { 227 + display: grid; 228 + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 229 + gap: 15px; 230 + } 231 + 232 + .stat-item { 233 + text-align: center; 234 + } 235 + 236 + .stat-number { 237 + font-size: 24px; 238 + font-weight: bold; 239 + color: #2563eb; 240 + } 241 + 242 + .stat-label { 243 + font-size: 14px; 244 + color: #6b7280; 221 245 } 222 246 223 247 .task-list { 224 248 background: white; 225 249 border-radius: 8px; 226 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 250 + box-shadow: 0 1px 3px rgba(0,0,0,0.1); 227 251 } 228 252 229 253 .task-item { 230 254 padding: 15px 20px; 231 - border-bottom: 1px solid #e5e7eb; 255 + border-bottom: 1px solid #f3f4f6; 232 256 position: relative; 233 - transition: background-color 0.2s; 257 + transition: all 0.2s; 234 258 } 235 259 236 260 .task-item:last-child { ··· 238 262 } 239 263 240 264 .task-item:hover { 241 - background-color: #f9fafb; 265 + background: #f9fafb; 242 266 } 243 267 244 268 .task-item.completed { ··· 250 274 } 251 275 252 276 .task-item.overdue { 253 - border-left: 4px solid #ef4444; 277 + border-left: 4px solid #dc2626; 278 + background: #fef2f2; 254 279 } 255 280 256 281 .task-header { 257 282 display: flex; 258 283 align-items: center; 259 - gap: 10px; 284 + gap: 12px; 260 285 margin-bottom: 8px; 261 286 } 262 287 ··· 269 294 .task-title { 270 295 font-weight: 500; 271 296 flex: 1; 297 + cursor: pointer; 298 + } 299 + 300 + .task-title.editing { 301 + display: none; 302 + } 303 + 304 + .task-title-input { 305 + display: none; 306 + flex: 1; 307 + padding: 4px 8px; 308 + border: 1px solid #d1d5db; 309 + border-radius: 4px; 310 + } 311 + 312 + .task-title-input.editing { 313 + display: block; 272 314 } 273 315 274 316 .priority-badge { ··· 279 321 } 280 322 281 323 .priority-urgent { 282 - background: #fee2e2; 324 + background: #fef2f2; 283 325 color: #dc2626; 284 326 } 285 327 286 328 .priority-high { 287 - background: #fed7aa; 329 + background: #fff7ed; 288 330 color: #ea580c; 289 331 } 290 332 291 333 .priority-normal { 292 - background: #dbeafe; 334 + background: #eff6ff; 293 335 color: #2563eb; 294 336 } 295 337 ··· 301 343 .task-meta { 302 344 display: flex; 303 345 gap: 15px; 346 + align-items: center; 304 347 font-size: 14px; 305 - color: #64748b; 306 - flex-wrap: wrap; 348 + color: #6b7280; 349 + margin-left: 30px; 307 350 } 308 351 309 - .project-name { 310 - color: #3b82f6; 352 + .task-project { 353 + display: flex; 354 + align-items: center; 355 + gap: 6px; 311 356 } 312 357 313 - .due-date { 314 - color: #059669; 315 - } 316 - 317 - .overdue-badge { 318 - color: #dc2626; 319 - font-weight: 500; 358 + .task-actions { 359 + position: absolute; 360 + right: 20px; 361 + top: 50%; 362 + transform: translateY(-50%); 363 + display: none; 364 + gap: 8px; 320 365 } 321 366 322 - .task-description { 323 - margin-top: 8px; 324 - color: #64748b; 325 - font-size: 14px; 367 + .task-item:hover .task-actions { 368 + display: flex; 326 369 } 327 370 328 - .delete-btn { 329 - position: absolute; 330 - right: 15px; 331 - top: 50%; 332 - transform: translateY(-50%); 333 - background: #ef4444; 334 - color: white; 371 + .action-btn { 372 + padding: 4px 8px; 335 373 border: none; 336 - padding: 6px 10px; 337 374 border-radius: 4px; 338 375 cursor: pointer; 339 376 font-size: 12px; 340 - opacity: 0; 341 - transition: opacity 0.2s; 377 + transition: all 0.2s; 378 + } 379 + 380 + .action-btn.edit { 381 + background: #f3f4f6; 382 + color: #374151; 342 383 } 343 384 344 - .task-item:hover .delete-btn { 345 - opacity: 1; 385 + .action-btn.edit:hover { 386 + background: #e5e7eb; 346 387 } 347 388 348 - .delete-btn:hover { 389 + .action-btn.delete { 390 + background: #fef2f2; 391 + color: #dc2626; 392 + } 393 + 394 + .action-btn.delete:hover { 395 + background: #fee2e2; 396 + } 397 + 398 + .overdue-badge { 349 399 background: #dc2626; 400 + color: white; 401 + padding: 2px 6px; 402 + border-radius: 10px; 403 + font-size: 11px; 404 + font-weight: 500; 350 405 } 351 406 352 407 .empty-state { 353 408 text-align: center; 354 409 padding: 40px 20px; 355 - color: #64748b; 410 + color: #6b7280; 356 411 } 357 412 358 - @media (max-width: 600px) { 413 + @media (max-width: 640px) { 359 414 .container { 360 - padding: 15px; 415 + padding: 10px; 361 416 } 362 417 363 418 .form-row { 364 419 flex-direction: column; 365 420 } 366 421 367 - .form-group { 368 - min-width: auto; 369 - } 370 - 371 - .filters { 422 + .filter-row { 372 423 flex-direction: column; 373 424 align-items: stretch; 374 425 } 375 426 376 - .stats { 427 + .task-meta { 377 428 flex-direction: column; 429 + align-items: flex-start; 430 + gap: 5px; 378 431 } 379 432 } 380 433 </style> ··· 383 436 <div class="container"> 384 437 <div class="header"> 385 438 <h1>Task Manager</h1> 386 - <div class="stats" id="stats"> 387 - <!-- Stats will be populated by JavaScript --> 439 + <p>Organize your work and life</p> 440 + </div> 441 + 442 + <div class="sidebar"> 443 + <h3>Projects</h3> 444 + <div class="project-list" id="projectList"> 445 + <div class="project-item active" data-project="inbox"> 446 + <span class="project-dot" style="background: #6b7280;"></span> 447 + <span>Inbox</span> 448 + <span class="project-count" id="inboxCount">0</span> 449 + </div> 388 450 </div> 389 451 </div> 390 452 391 453 <div class="add-task-form"> 454 + <h3>Add New Task</h3> 392 455 <form id="addTaskForm"> 393 456 <div class="form-row"> 394 457 <div class="form-group"> 395 - <label for="title">Task Title</label> 396 - <input type="text" id="title" name="title" required> 458 + <label for="taskTitle">Title *</label> 459 + <input type="text" id="taskTitle" required> 397 460 </div> 398 461 <div class="form-group"> 399 - <label for="priority">Priority</label> 400 - <select id="priority" name="priority"> 462 + <label for="taskPriority">Priority</label> 463 + <select id="taskPriority"> 401 464 <option value="normal">Normal</option> 402 465 <option value="low">Low</option> 403 466 <option value="high">High</option> ··· 406 469 </div> 407 470 </div> 408 471 472 + <button type="button" class="description-toggle" id="descriptionToggle"> 473 + + Add Description 474 + </button> 475 + 476 + <div class="description-section" id="descriptionSection"> 477 + <div class="form-group"> 478 + <label for="taskDescription">Description</label> 479 + <textarea id="taskDescription" placeholder="Add task details..."></textarea> 480 + </div> 481 + </div> 482 + 409 483 <div class="form-row"> 410 484 <div class="form-group"> 411 - <label for="project">Project</label> 412 - <select id="project" name="project_id"> 485 + <label for="taskProject">Project</label> 486 + <select id="taskProject"> 413 487 <option value="">Inbox</option> 414 488 </select> 415 489 </div> 416 490 <div class="form-group"> 417 - <label for="dueDate">Due Date</label> 418 - <input type="date" id="dueDate" name="due_date"> 419 - </div> 420 - </div> 421 - 422 - <button type="button" class="description-toggle" onclick="toggleDescription()"> 423 - + Add Description 424 - </button> 425 - 426 - <div class="description-section" id="descriptionSection"> 427 - <div class="form-group full-width"> 428 - <label for="description">Description</label> 429 - <textarea id="description" name="description" placeholder="Optional task description..."></textarea> 491 + <label for="taskDueDate">Due Date</label> 492 + <input type="date" id="taskDueDate"> 430 493 </div> 431 494 </div> 432 495 433 - <button type="submit" class="btn">Add Task</button> 496 + <button type="submit" class="btn btn-primary">Add Task</button> 434 497 </form> 435 498 </div> 436 499 437 500 <div class="filters"> 438 - <div class="filter-group"> 439 - <button class="filter-btn active" data-status="all">All</button> 440 - <button class="filter-btn" data-status="active">Active</button> 441 - <button class="filter-btn" data-status="completed">Completed</button> 442 - </div> 443 - 444 - <div class="form-group" style="min-width: 150px;"> 445 - <select id="priorityFilter"> 446 - <option value="">All Priorities</option> 447 - <option value="urgent">Urgent</option> 448 - <option value="high">High</option> 449 - <option value="normal">Normal</option> 450 - <option value="low">Low</option> 451 - </select> 501 + <div class="filter-row"> 502 + <div class="filter-buttons"> 503 + <button class="filter-btn active" data-status="all">All</button> 504 + <button class="filter-btn" data-status="active">Active</button> 505 + <button class="filter-btn" data-status="completed">Completed</button> 506 + </div> 507 + 508 + <div class="form-group" style="min-width: 150px;"> 509 + <select id="priorityFilter"> 510 + <option value="">All Priorities</option> 511 + <option value="urgent">Urgent</option> 512 + <option value="high">High</option> 513 + <option value="normal">Normal</option> 514 + <option value="low">Low</option> 515 + </select> 516 + </div> 452 517 </div> 453 - 454 - <div class="form-group" style="min-width: 150px;"> 455 - <select id="projectFilter"> 456 - <option value="">All Projects</option> 457 - </select> 518 + </div> 519 + 520 + <div class="stats" id="statsSection"> 521 + <div class="stats-grid" id="statsGrid"> 522 + <!-- Stats will be loaded here --> 458 523 </div> 459 524 </div> 460 525 461 526 <div class="task-list" id="taskList"> 462 - <!-- Tasks will be populated by JavaScript --> 527 + <!-- Tasks will be loaded here --> 463 528 </div> 464 529 </div> 465 530 466 531 <script> 467 - let currentFilters = { 468 - status: 'all', 469 - priority: '', 470 - project: '' 471 - }; 532 + let currentProject = 'inbox'; 533 + let currentStatus = 'all'; 534 + let currentPriority = ''; 535 + let projects = []; 536 + let tasks = []; 472 537 473 538 // Initialize the app 474 - document.addEventListener('DOMContentLoaded', function() { 539 + document.addEventListener('DOMContentLoaded', () => { 475 540 loadProjects(); 476 541 loadTasks(); 477 542 loadStats(); ··· 479 544 }); 480 545 481 546 function setupEventListeners() { 547 + // Description toggle 548 + document.getElementById('descriptionToggle').addEventListener('click', () => { 549 + const section = document.getElementById('descriptionSection'); 550 + const toggle = document.getElementById('descriptionToggle'); 551 + if (section.classList.contains('expanded')) { 552 + section.classList.remove('expanded'); 553 + toggle.textContent = '+ Add Description'; 554 + } else { 555 + section.classList.add('expanded'); 556 + toggle.textContent = '- Hide Description'; 557 + } 558 + }); 559 + 482 560 // Add task form 483 - document.getElementById('addTaskForm').addEventListener('submit', handleAddTask); 484 - 561 + document.getElementById('addTaskForm').addEventListener('submit', async (e) => { 562 + e.preventDefault(); 563 + await addTask(); 564 + }); 565 + 485 566 // Status filters 486 567 document.querySelectorAll('[data-status]').forEach(btn => { 487 - btn.addEventListener('click', function() { 568 + btn.addEventListener('click', (e) => { 488 569 document.querySelectorAll('[data-status]').forEach(b => b.classList.remove('active')); 489 - this.classList.add('active'); 490 - currentFilters.status = this.dataset.status; 570 + e.target.classList.add('active'); 571 + currentStatus = e.target.dataset.status; 491 572 loadTasks(); 492 573 }); 493 574 }); 494 - 575 + 495 576 // Priority filter 496 - document.getElementById('priorityFilter').addEventListener('change', function() { 497 - currentFilters.priority = this.value; 498 - loadTasks(); 499 - }); 500 - 501 - // Project filter 502 - document.getElementById('projectFilter').addEventListener('change', function() { 503 - currentFilters.project = this.value; 577 + document.getElementById('priorityFilter').addEventListener('change', (e) => { 578 + currentPriority = e.target.value; 504 579 loadTasks(); 505 580 }); 506 - } 507 581 508 - function toggleDescription() { 509 - const section = document.getElementById('descriptionSection'); 510 - const toggle = document.querySelector('.description-toggle'); 511 - 512 - if (section.classList.contains('expanded')) { 513 - section.classList.remove('expanded'); 514 - toggle.textContent = '+ Add Description'; 515 - } else { 516 - section.classList.add('expanded'); 517 - toggle.textContent = '- Hide Description'; 518 - } 582 + // Project selection 583 + document.addEventListener('click', (e) => { 584 + if (e.target.closest('.project-item')) { 585 + const projectItem = e.target.closest('.project-item'); 586 + document.querySelectorAll('.project-item').forEach(p => p.classList.remove('active')); 587 + projectItem.classList.add('active'); 588 + currentProject = projectItem.dataset.project; 589 + loadTasks(); 590 + } 591 + }); 519 592 } 520 593 521 594 async function loadProjects() { 522 595 try { 523 - const response = await fetch('/api/projects'); 524 - const projects = await response.json(); 525 - 526 - const projectSelect = document.getElementById('project'); 527 - const projectFilter = document.getElementById('projectFilter'); 528 - 529 - // Clear existing options (except first) 530 - projectSelect.innerHTML = '<option value="">Inbox</option>'; 531 - projectFilter.innerHTML = '<option value="">All Projects</option>'; 532 - 533 - projects.forEach(project => { 534 - const option1 = document.createElement('option'); 535 - option1.value = project.id; 536 - option1.textContent = project.name; 537 - projectSelect.appendChild(option1); 538 - 539 - const option2 = document.createElement('option'); 540 - option2.value = project.id; 541 - option2.textContent = project.name; 542 - projectFilter.appendChild(option2); 543 - }); 596 + const response = await fetch('/projects'); 597 + projects = await response.json(); 598 + renderProjects(); 599 + populateProjectDropdown(); 544 600 } catch (error) { 545 - console.error('Error loading projects:', error); 601 + console.error('Failed to load projects:', error); 546 602 } 547 603 } 548 604 549 605 async function loadTasks() { 550 606 try { 607 + let url = '/filtering-and-views'; 551 608 const params = new URLSearchParams(); 552 609 553 - if (currentFilters.status === 'active') { 610 + if (currentProject !== 'inbox') { 611 + params.append('project_id', currentProject); 612 + } else { 613 + params.append('project_id', ''); 614 + } 615 + 616 + if (currentStatus === 'active') { 554 617 params.append('completed', '0'); 555 - } else if (currentFilters.status === 'completed') { 618 + } else if (currentStatus === 'completed') { 556 619 params.append('completed', '1'); 557 620 } 558 621 559 - if (currentFilters.priority) { 560 - params.append('priority', currentFilters.priority); 622 + if (currentPriority) { 623 + params.append('priority', currentPriority); 561 624 } 562 625 563 - if (currentFilters.project) { 564 - params.append('project_id', currentFilters.project); 626 + if (params.toString()) { 627 + url += '?' + params.toString(); 565 628 } 566 629 567 - const response = await fetch('/api/tasks?' + params.toString()); 568 - const tasks = await response.json(); 569 - 570 - renderTasks(tasks); 571 - loadStats(); // Refresh stats after loading tasks 630 + const response = await fetch(url); 631 + tasks = await response.json(); 632 + renderTasks(); 633 + updateProjectCounts(); 634 + } catch (error) { 635 + console.error('Failed to load tasks:', error); 636 + } 637 + } 638 + 639 + async function loadStats() { 640 + try { 641 + const response = await fetch('/quick-stats'); 642 + const stats = await response.json(); 643 + renderStats(stats); 572 644 } catch (error) { 573 - console.error('Error loading tasks:', error); 645 + console.error('Failed to load stats:', error); 574 646 } 575 647 } 576 648 577 - function renderTasks(tasks) { 649 + function renderProjects() { 650 + const projectList = document.getElementById('projectList'); 651 + const inboxItem = projectList.querySelector('[data-project="inbox"]'); 652 + 653 + // Clear existing projects (keep inbox) 654 + projectList.innerHTML = ''; 655 + projectList.appendChild(inboxItem); 656 + 657 + projects.forEach(project => { 658 + const projectItem = document.createElement('div'); 659 + projectItem.className = 'project-item'; 660 + projectItem.dataset.project = project.id; 661 + projectItem.innerHTML = \` 662 + <span class="project-dot" style="background: \${project.color};"></span> 663 + <span>\${project.name}</span> 664 + <span class="project-count" id="project-\${project.id}-count">0</span> 665 + \`; 666 + projectList.appendChild(projectItem); 667 + }); 668 + } 669 + 670 + function populateProjectDropdown() { 671 + const select = document.getElementById('taskProject'); 672 + select.innerHTML = '<option value="">Inbox</option>'; 673 + projects.forEach(project => { 674 + const option = document.createElement('option'); 675 + option.value = project.id; 676 + option.textContent = project.name; 677 + select.appendChild(option); 678 + }); 679 + } 680 + 681 + function renderTasks() { 578 682 const taskList = document.getElementById('taskList'); 579 683 580 684 if (tasks.length === 0) { 581 - taskList.innerHTML = '<div class="empty-state">No tasks found. Add your first task above!</div>'; 685 + taskList.innerHTML = '<div class="empty-state">No tasks found</div>'; 582 686 return; 583 687 } 584 688 585 - const now = new Date().toISOString().split('T')[0]; 586 - 587 689 taskList.innerHTML = tasks.map(task => { 588 - const isOverdue = task.due_date && task.due_date < now && !task.completed; 589 - const priorityClass = 'priority-' + task.priority; 690 + const isOverdue = task.due_date && new Date(task.due_date) < new Date() && !task.completed; 691 + const project = projects.find(p => p.id === task.project_id); 590 692 591 693 return \` 592 - <div class="task-item \${task.completed ? 'completed' : ''} \${isOverdue ? 'overdue' : ''}" data-id="\${task.id}"> 694 + <div class="task-item \${task.completed ? 'completed' : ''} \${isOverdue ? 'overdue' : ''}" data-task-id="\${task.id}"> 593 695 <div class="task-header"> 594 - <input type="checkbox" class="task-checkbox" \${task.completed ? 'checked' : ''} 595 - onchange="toggleTask(\${task.id}, this.checked)"> 596 - <div class="task-title">\${escapeHtml(task.title)}</div> 597 - <span class="priority-badge \${priorityClass}">\${task.priority}</span> 696 + <input type="checkbox" class="task-checkbox" \${task.completed ? 'checked' : ''} onchange="toggleTask(\${task.id})"> 697 + <span class="task-title" onclick="editTaskTitle(\${task.id})">\${task.title}</span> 698 + <input type="text" class="task-title-input" value="\${task.title}" onblur="saveTaskTitle(\${task.id})" onkeydown="handleTitleKeydown(event, \${task.id})"> 699 + <span class="priority-badge priority-\${task.priority}">\${task.priority}</span> 700 + \${isOverdue ? '<span class="overdue-badge">Overdue</span>' : ''} 598 701 </div> 599 - 702 + \${task.description ? \`<div class="task-description" style="margin-left: 30px; color: #6b7280; font-size: 14px;">\${task.description}</div>\` : ''} 600 703 <div class="task-meta"> 601 - \${task.project_name ? \`<span class="project-name">\${escapeHtml(task.project_name)}</span>\` : '<span class="project-name">Inbox</span>'} 602 - \${task.due_date ? (isOverdue ? \`<span class="overdue-badge">Overdue (\${formatDate(task.due_date)})</span>\` : \`<span class="due-date">Due \${formatDate(task.due_date)}</span>\`) : ''} 704 + \${project ? \`<div class="task-project"><span class="project-dot" style="background: \${project.color};"></span>\${project.name}</div>\` : '<div class="task-project">Inbox</div>'} 705 + \${task.due_date ? \`<div>Due: \${new Date(task.due_date).toLocaleDateString()}</div>\` : ''} 603 706 </div> 604 - 605 - \${task.description ? \`<div class="task-description">\${escapeHtml(task.description)}</div>\` : ''} 606 - 607 - <button class="delete-btn" onclick="deleteTask(\${task.id})">Delete</button> 707 + <div class="task-actions"> 708 + <button class="action-btn edit" onclick="editTask(\${task.id})">Edit</button> 709 + <button class="action-btn delete" onclick="deleteTask(\${task.id})">Delete</button> 710 + </div> 608 711 </div> 609 712 \`; 610 713 }).join(''); 611 714 } 612 715 613 - async function loadStats() { 614 - try { 615 - const response = await fetch('/api/tasks/stats'); 616 - const stats = await response.json(); 617 - 618 - const statsContainer = document.getElementById('stats'); 619 - statsContainer.innerHTML = \` 620 - <div class="stat-card"> 621 - <div class="stat-number">\${stats.total_tasks}</div> 622 - <div class="stat-label">Total</div> 623 - </div> 624 - <div class="stat-card"> 625 - <div class="stat-number">\${stats.active_tasks}</div> 626 - <div class="stat-label">Active</div> 627 - </div> 628 - <div class="stat-card"> 629 - <div class="stat-number">\${stats.completed_tasks}</div> 630 - <div class="stat-label">Completed</div> 631 - </div> 632 - <div class="stat-card"> 633 - <div class="stat-number">\${stats.overdue_tasks}</div> 634 - <div class="stat-label">Overdue</div> 635 - </div> 636 - \`; 637 - } catch (error) { 638 - console.error('Error loading stats:', error); 639 - } 716 + function renderStats(stats) { 717 + const statsGrid = document.getElementById('statsGrid'); 718 + statsGrid.innerHTML = \` 719 + <div class="stat-item"> 720 + <div class="stat-number">\${stats.total_tasks}</div> 721 + <div class="stat-label">Total</div> 722 + </div> 723 + <div class="stat-item"> 724 + <div class="stat-number">\${stats.active_tasks}</div> 725 + <div class="stat-label">Active</div> 726 + </div> 727 + <div class="stat-item"> 728 + <div class="stat-number">\${stats.completed_tasks}</div> 729 + <div class="stat-label">Completed</div> 730 + </div> 731 + <div class="stat-item"> 732 + <div class="stat-number">\${stats.overdue_tasks}</div> 733 + <div class="stat-label">Overdue</div> 734 + </div> 735 + \`; 640 736 } 641 737 642 - async function handleAddTask(e) { 643 - e.preventDefault(); 738 + function updateProjectCounts() { 739 + // Update inbox count 740 + const inboxTasks = tasks.filter(t => !t.project_id && !t.completed); 741 + document.getElementById('inboxCount').textContent = inboxTasks.length; 644 742 645 - const formData = new FormData(e.target); 743 + // Update project counts 744 + projects.forEach(project => { 745 + const projectTasks = tasks.filter(t => t.project_id === project.id && !t.completed); 746 + const countEl = document.getElementById(\`project-\${project.id}-count\`); 747 + if (countEl) { 748 + countEl.textContent = projectTasks.length; 749 + } 750 + }); 751 + } 752 + 753 + async function addTask() { 754 + const title = document.getElementById('taskTitle').value.trim(); 755 + const description = document.getElementById('taskDescription').value.trim(); 756 + const priority = document.getElementById('taskPriority').value; 757 + const projectId = document.getElementById('taskProject').value; 758 + const dueDate = document.getElementById('taskDueDate').value; 759 + 760 + if (!title) return; 761 + 646 762 const taskData = { 647 - title: formData.get('title'), 648 - description: formData.get('description') || '', 649 - priority: formData.get('priority'), 650 - due_date: formData.get('due_date') || null, 651 - project_id: formData.get('project_id') ? parseInt(formData.get('project_id')) : null 763 + title, 764 + priority, 765 + ...(description && { description }), 766 + ...(projectId && { project_id: parseInt(projectId) }), 767 + ...(dueDate && { due_date: dueDate }) 652 768 }; 653 769 654 770 try { 655 - const response = await fetch('/api/tasks', { 771 + const response = await fetch('/tasks', { 656 772 method: 'POST', 657 773 headers: { 'Content-Type': 'application/json' }, 658 774 body: JSON.stringify(taskData) 659 775 }); 660 776 661 777 if (response.ok) { 662 - e.target.reset(); 778 + document.getElementById('addTaskForm').reset(); 663 779 document.getElementById('descriptionSection').classList.remove('expanded'); 664 - document.querySelector('.description-toggle').textContent = '+ Add Description'; 780 + document.getElementById('descriptionToggle').textContent = '+ Add Description'; 665 781 loadTasks(); 666 - } else { 667 - const error = await response.json(); 668 - alert('Error: ' + error.error); 782 + loadStats(); 669 783 } 670 784 } catch (error) { 671 - console.error('Error adding task:', error); 672 - alert('Error adding task'); 785 + console.error('Failed to add task:', error); 673 786 } 674 787 } 675 788 676 - async function toggleTask(id, completed) { 789 + async function toggleTask(taskId) { 790 + const task = tasks.find(t => t.id === taskId); 791 + if (!task) return; 792 + 677 793 try { 678 - const response = await fetch(\`/api/tasks/\${id}\`, { 794 + await fetch(\`/tasks/\${taskId}\`, { 679 795 method: 'PATCH', 680 796 headers: { 'Content-Type': 'application/json' }, 681 - body: JSON.stringify({ completed: completed ? 1 : 0 }) 797 + body: JSON.stringify({ completed: task.completed ? 0 : 1 }) 682 798 }); 683 - 684 - if (response.ok) { 685 - loadTasks(); 686 - } else { 687 - console.error('Error toggling task'); 688 - } 799 + loadTasks(); 800 + loadStats(); 689 801 } catch (error) { 690 - console.error('Error toggling task:', error); 802 + console.error('Failed to toggle task:', error); 691 803 } 692 804 } 693 805 694 - async function deleteTask(id) { 806 + async function deleteTask(taskId) { 695 807 if (!confirm('Are you sure you want to delete this task?')) return; 696 808 697 809 try { 698 - const response = await fetch(\`/api/tasks/\${id}\`, { 699 - method: 'DELETE' 810 + await fetch(\`/tasks/\${taskId}\`, { method: 'DELETE' }); 811 + loadTasks(); 812 + loadStats(); 813 + } catch (error) { 814 + console.error('Failed to delete task:', error); 815 + } 816 + } 817 + 818 + function editTaskTitle(taskId) { 819 + const taskItem = document.querySelector(\`[data-task-id="\${taskId}"]\`); 820 + const titleSpan = taskItem.querySelector('.task-title'); 821 + const titleInput = taskItem.querySelector('.task-title-input'); 822 + 823 + titleSpan.classList.add('editing'); 824 + titleInput.classList.add('editing'); 825 + titleInput.focus(); 826 + titleInput.select(); 827 + } 828 + 829 + async function saveTaskTitle(taskId) { 830 + const taskItem = document.querySelector(\`[data-task-id="\${taskId}"]\`); 831 + const titleSpan = taskItem.querySelector('.task-title'); 832 + const titleInput = taskItem.querySelector('.task-title-input'); 833 + const newTitle = titleInput.value.trim(); 834 + 835 + if (newTitle && newTitle !== titleSpan.textContent) { 836 + try { 837 + await fetch(\`/tasks/\${taskId}\`, { 838 + method: 'PATCH', 839 + headers: { 'Content-Type': 'application/json' }, 840 + body: JSON.stringify({ title: newTitle }) 841 + }); 842 + titleSpan.textContent = newTitle; 843 + } catch (error) { 844 + console.error('Failed to update task title:', error); 845 + titleInput.value = titleSpan.textContent; 846 + } 847 + } 848 + 849 + titleSpan.classList.remove('editing'); 850 + titleInput.classList.remove('editing'); 851 + } 852 + 853 + function handleTitleKeydown(event, taskId) { 854 + if (event.key === 'Enter') { 855 + event.target.blur(); 856 + } else if (event.key === 'Escape') { 857 + const taskItem = document.querySelector(\`[data-task-id="\${taskId}"]\`); 858 + const titleSpan = taskItem.querySelector('.task-title'); 859 + const titleInput = taskItem.querySelector('.task-title-input'); 860 + titleInput.value = titleSpan.textContent; 861 + titleInput.blur(); 862 + } 863 + } 864 + 865 + async function editTask(taskId) { 866 + const task = tasks.find(t => t.id === taskId); 867 + if (!task) return; 868 + 869 + // Simple edit - just populate the form with current values 870 + document.getElementById('taskTitle').value = task.title; 871 + document.getElementById('taskDescription').value = task.description || ''; 872 + document.getElementById('taskPriority').value = task.priority; 873 + document.getElementById('taskProject').value = task.project_id || ''; 874 + document.getElementById('taskDueDate').value = task.due_date || ''; 875 + 876 + if (task.description) { 877 + document.getElementById('descriptionSection').classList.add('expanded'); 878 + document.getElementById('descriptionToggle').textContent = '- Hide Description'; 879 + } 880 + 881 + // Change form to edit mode 882 + const form = document.getElementById('addTaskForm'); 883 + const submitBtn = form.querySelector('button[type="submit"]'); 884 + submitBtn.textContent = 'Update Task'; 885 + 886 + form.onsubmit = async (e) => { 887 + e.preventDefault(); 888 + await updateTask(taskId); 889 + }; 890 + 891 + // Scroll to form 892 + form.scrollIntoView({ behavior: 'smooth' }); 893 + } 894 + 895 + async function updateTask(taskId) { 896 + const title = document.getElementById('taskTitle').value.trim(); 897 + const description = document.getElementById('taskDescription').value.trim(); 898 + const priority = document.getElementById('taskPriority').value; 899 + const projectId = document.getElementById('taskProject').value; 900 + const dueDate = document.getElementById('taskDueDate').value; 901 + 902 + if (!title) return; 903 + 904 + const taskData = { 905 + title, 906 + priority, 907 + description: description || null, 908 + project_id: projectId ? parseInt(projectId) : null, 909 + due_date: dueDate || null 910 + }; 911 + 912 + try { 913 + const response = await fetch(\`/tasks/\${taskId}\`, { 914 + method: 'PATCH', 915 + headers: { 'Content-Type': 'application/json' }, 916 + body: JSON.stringify(taskData) 700 917 }); 701 918 702 919 if (response.ok) { 920 + // Reset form to add mode 921 + document.getElementById('addTaskForm').reset(); 922 + document.getElementById('descriptionSection').classList.remove('expanded'); 923 + document.getElementById('descriptionToggle').textContent = '+ Add Description'; 924 + 925 + const form = document.getElementById('addTaskForm'); 926 + const submitBtn = form.querySelector('button[type="submit"]'); 927 + submitBtn.textContent = 'Add Task'; 928 + 929 + form.onsubmit = async (e) => { 930 + e.preventDefault(); 931 + await addTask(); 932 + }; 933 + 703 934 loadTasks(); 704 - } else { 705 - console.error('Error deleting task'); 935 + loadStats(); 706 936 } 707 937 } catch (error) { 708 - console.error('Error deleting task:', error); 938 + console.error('Failed to update task:', error); 709 939 } 710 940 } 711 - 712 - function escapeHtml(text) { 713 - const div = document.createElement('div'); 714 - div.textContent = text; 715 - return div.innerHTML; 716 - } 717 - 718 - function formatDate(dateString) { 719 - const date = new Date(dateString); 720 - return date.toLocaleDateString(); 721 - } 722 941 </script> 723 942 </body> 724 943 </html>`); 725 944 }); 726 945 727 - // API Routes 728 - 729 - // Projects 730 - router.get('/api/projects', (c) => { 731 - const projects = db.prepare('SELECT * FROM projects ORDER BY name').all(); 732 - return c.json(projects); 733 - }); 734 - 735 - router.post('/api/projects', async (c) => { 736 - const body = await c.req.json(); 737 - const result = createProjectSchema.safeParse(body); 738 - 739 - if (!result.success) { 740 - return c.json({ error: 'Invalid project data' }, 400); 741 - } 742 - 743 - try { 744 - const stmt = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)'); 745 - const info = stmt.run(result.data.name, result.data.color); 746 - 747 - const project = db.prepare('SELECT * FROM projects WHERE id = ?').get(info.lastInsertRowid); 748 - return c.json(project, 201); 749 - } catch (error) { 750 - return c.json({ error: 'Project name already exists' }, 400); 751 - } 752 - }); 753 - 754 - // Tasks 755 - router.get('/api/tasks', (c) => { 756 - const { completed, priority, project_id } = c.req.query(); 757 - 758 - let sql = ` 759 - SELECT t.*, p.name as project_name 760 - FROM tasks t 761 - LEFT JOIN projects p ON t.project_id = p.id 762 - WHERE 1=1 763 - `; 764 - const params: unknown[] = []; 765 - 766 - if (completed !== undefined) { 767 - sql += ' AND t.completed = ?'; 768 - params.push(parseInt(completed)); 769 - } 770 - 771 - if (priority) { 772 - sql += ' AND t.priority = ?'; 773 - params.push(priority); 774 - } 775 - 776 - if (project_id) { 777 - sql += ' AND t.project_id = ?'; 778 - params.push(parseInt(project_id)); 779 - } 780 - 781 - sql += ' ORDER BY t.completed ASC, t.priority = "urgent" DESC, t.priority = "high" DESC, t.due_date ASC, t.created_at DESC'; 782 - 783 - const tasks = db.prepare(sql).all(...params); 784 - return c.json(tasks); 785 - }); 786 - 787 - router.get('/api/tasks/stats', (c) => { 788 - const stats = db.prepare(` 789 - SELECT 790 - COUNT(*) as total_tasks, 791 - SUM(CASE WHEN completed = 0 THEN 1 ELSE 0 END) as active_tasks, 792 - SUM(CASE WHEN completed = 1 THEN 1 ELSE 0 END) as completed_tasks, 793 - SUM(CASE WHEN completed = 0 AND due_date IS NOT NULL AND due_date < date('now') THEN 1 ELSE 0 END) as overdue_tasks 794 - FROM tasks 795 - `).get(); 796 - 797 - return c.json(stats); 798 - }); 799 - 800 - router.get('/api/tasks/:id', (c) => { 801 - const id = parseInt(c.req.param('id')); 802 - const task = db.prepare(` 803 - SELECT t.*, p.name as project_name 804 - FROM tasks t 805 - LEFT JOIN projects p ON t.project_id = p.id 806 - WHERE t.id = ? 807 - `).get(id); 808 - 809 - if (!task) { 810 - return c.json({ error: 'Task not found' }, 404); 811 - } 812 - 813 - return c.json(task); 814 - }); 815 - 816 - router.post('/api/tasks', async (c) => { 817 - const body = await c.req.json(); 818 - const result = createTaskSchema.safeParse(body); 819 - 820 - if (!result.success) { 821 - return c.json({ error: 'Invalid task data' }, 400); 822 - } 823 - 824 - const stmt = db.prepare(` 825 - INSERT INTO tasks (title, description, priority, due_date, project_id) 826 - VALUES (?, ?, ?, ?, ?) 827 - `); 828 - 829 - const info = stmt.run( 830 - result.data.title, 831 - result.data.description, 832 - result.data.priority, 833 - result.data.due_date || null, 834 - result.data.project_id || null 835 - ); 836 - 837 - const task = db.prepare(` 838 - SELECT t.*, p.name as project_name 839 - FROM tasks t 840 - LEFT JOIN projects p ON t.project_id = p.id 841 - WHERE t.id = ? 842 - `).get(info.lastInsertRowid); 843 - 844 - return c.json(task, 201); 845 - }); 846 - 847 - router.patch('/api/tasks/:id', async (c) => { 848 - const id = parseInt(c.req.param('id')); 849 - const body = await c.req.json(); 850 - const result = updateTaskSchema.safeParse(body); 851 - 852 - if (!result.success) { 853 - return c.json({ error: 'Invalid task data' }, 400); 854 - } 855 - 856 - const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 857 - if (!existing) { 858 - return c.json({ error: 'Task not found' }, 404); 859 - } 860 - 861 - const updates: string[] = []; 862 - const params: unknown[] = []; 863 - 864 - Object.entries(result.data).forEach(([key, value]) => { 865 - if (value !== undefined) { 866 - updates.push(`${key} = ?`); 867 - params.push(value); 868 - } 869 - }); 870 - 871 - if (updates.length === 0) { 872 - return c.json({ error: 'No valid fields to update' }, 400); 873 - } 874 - 875 - params.push(id); 876 - const sql = `UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`; 877 - db.prepare(sql).run(...params); 878 - 879 - const task = db.prepare(` 880 - SELECT t.*, p.name as project_name 881 - FROM tasks t 882 - LEFT JOIN projects p ON t.project_id = p.id 883 - WHERE t.id = ? 884 - `).get(id); 885 - 886 - return c.json(task); 887 - }); 888 - 889 - router.delete('/api/tasks/:id', (c) => { 890 - const id = parseInt(c.req.param('id')); 891 - 892 - const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 893 - if (!existing) { 894 - return c.json({ error: 'Task not found' }, 404); 895 - } 896 - 897 - db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 898 - return c.body(null, 204); 899 - }); 900 - 901 946 export default router; 902 947 903 - /** @internal Phoenix VCS traceability — do not remove. */ 904 948 export const _phoenix = { 905 949 iu_id: '335590ecf9457e5b14124f79e4d9399888f58b7aff87edd6a264b6aa6fdc2d48', 906 950 name: 'Web Experience',
+4 -1
src/architectures/sqlite-web-api.ts
··· 144 144 145 145 ### Web interface / HTML pages 146 146 - If the spec describes a web interface or HTML page, generate a Hono route that returns \`c.html()\` with a complete HTML string. 147 + - The web module ONLY serves HTML at GET /. It must NOT define any API routes. 148 + - The JavaScript in the HTML must call the OTHER modules' endpoints using their mount paths. For example, if there is a "Tasks" module mounted at /tasks and a "Projects" module mounted at /projects, the JavaScript uses fetch('/tasks'), fetch('/projects'), etc. 149 + - The mount paths follow the pattern: /lowercase-module-name (e.g., "Tasks" → /tasks, "Quick Stats" → /quick-stats, "Projects" → /projects). 147 150 - Include ALL CSS and JavaScript inline in the HTML — no external files or build steps. 148 - - The JavaScript must use fetch() to call the API endpoints (same origin, e.g., fetch('/todos')). 149 151 - After any create/update/delete action, refresh the displayed data by re-fetching. 150 152 - Use modern vanilla JavaScript (no frameworks). Use template literals for HTML generation. 151 153 - The HTML must be a complete document with <!DOCTYPE html>, <head>, and <body>. 154 + - Do NOT prefix API calls with /api/. The endpoints are at the root level: /tasks, /projects, etc. 152 155 `; 153 156 154 157 const CODE_EXAMPLES = `
+15 -4
src/llm/prompt.ts
··· 150 150 lines.push(`## Risk Tier: ${iu.risk_tier}`); 151 151 lines.push(''); 152 152 153 - // Context: sibling modules 153 + // Context: sibling modules with mount paths for architecture mode 154 154 if (siblingModules && siblingModules.length > 0) { 155 - lines.push(`## Other modules in this service (for context, do NOT import them):`); 156 - for (const m of siblingModules) { 157 - lines.push(`- ${m}`); 155 + if (arch) { 156 + lines.push(`## Other API modules (do NOT import them — call their HTTP endpoints from JavaScript):`); 157 + for (const m of siblingModules) { 158 + const lowerName = m.toLowerCase(); 159 + const isWebUI = /\b(web|ui|frontend|interface|page|dashboard)\b/.test(lowerName); 160 + if (isWebUI) continue; // skip other web modules 161 + const mountPath = '/' + lowerName.replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 162 + lines.push(`- "${m}" mounted at ${mountPath} — use fetch('${mountPath}') or fetch('${mountPath}/...') to call it`); 163 + } 164 + } else { 165 + lines.push(`## Other modules in this service (for context, do NOT import them):`); 166 + for (const m of siblingModules) { 167 + lines.push(`- ${m}`); 168 + } 158 169 } 159 170 lines.push(''); 160 171 }