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: three systemic pipeline issues found via user testing

1. IU planner fragmentation: cross-cutting spec sections (Filtering,
Stats, Data Integrity, Integration) became separate modules with
separate mount paths. The web UI then called /filtering-and-views
instead of /tasks. Fix: consolidated spec to 3 resource-oriented
sections (Tasks, Projects, Web Experience). Long-term fix needed
in IU planner to merge non-resource IUs into parent resources.

2. SQL quoting: LLM generates date("now") with double quotes inside
JS template literals. SQLite treats double quotes as column names.
Fix: architecture prompt now explicitly requires single quotes for
SQL string literals.

3. Error visibility: Hono's default error handler returns "Internal
Server Error" with no details, making debugging impossible.
Fix: shared app.ts now includes onError handler that logs stack
traces and returns JSON error messages.

+664 -1513
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.

+11 -3
examples/todo-app/package.json
··· 8 8 "typecheck": "tsc --noEmit", 9 9 "test": "vitest run", 10 10 "test:watch": "vitest", 11 - "start:todos": "tsc && node dist/generated/todos/server.js", 12 - "start": "tsc && node dist/generated/todos/server.js" 11 + "dev": "tsx watch src/server.ts", 12 + "start": "tsx src/server.ts" 13 + }, 14 + "dependencies": { 15 + "hono": "^4.6.0", 16 + "@hono/node-server": "^1.13.0", 17 + "better-sqlite3": "^11.7.0", 18 + "zod": "^3.24.0" 13 19 }, 14 20 "devDependencies": { 15 21 "typescript": "^5.4.0", 16 22 "vitest": "^2.0.0", 17 - "@types/node": "^22.0.0" 23 + "@types/node": "^22.0.0", 24 + "@types/better-sqlite3": "^7.6.0", 25 + "tsx": "^4.0.0" 18 26 } 19 27 }
+15 -32
examples/todo-app/spec/todos.md
··· 5 5 ## Tasks 6 6 7 7 - A task has a title, an optional description, a priority (urgent, high, normal, low), an optional due date, and a completion status 8 + - Every task must have a stable unique integer identifier that external systems can reference 8 9 - Users can create tasks by providing at least a title 9 10 - Users can view all their tasks, with the most urgent and overdue tasks shown first 10 11 - Users can mark a task as complete or reopen a completed task 11 12 - Users can edit a task's title, description, priority, or due date at any time 12 13 - Users can delete a task permanently 13 - - Overdue tasks must be visually highlighted so they stand out 14 - - Completed tasks must be visually distinct from active tasks (e.g., strikethrough, dimmed) 14 + - Users can filter tasks by status: all, active (not completed), or completed 15 + - Users can filter tasks by project 16 + - Users can filter tasks by priority 17 + - Filters must be combinable: for example, viewing all urgent tasks in a specific project 18 + - Users must see a stats summary showing: total tasks, completed tasks, overdue tasks, and completion percentage; this summary must update immediately after any change 19 + - Task titles must not be empty and must not exceed 500 characters 20 + - Descriptions must not exceed 5000 characters 21 + - Priority must always be one of: urgent, high, normal, low 22 + - Due dates must be valid dates; the system must reject obviously invalid dates 23 + - The system must expose these capabilities as a programmatic interface so external tools can create, read, update, and delete tasks using standard conventions 15 24 16 25 ## Projects 17 26 18 27 - A project has a name and a color for visual identification 28 + - Every project must have a stable unique integer identifier that external systems can reference 19 29 - Users can create projects to group related tasks 20 30 - Users can assign a task to a project, or leave it in a default "Inbox" with no project 21 - - Users can view tasks filtered by project 22 - - Users can delete a project only if it contains no tasks; the system must warn them otherwise 31 + - Users can delete a project only if it contains no tasks; the system must prevent deletion otherwise 23 32 - The project list must show how many active (incomplete) tasks each project has 24 - 25 - ## Filtering and Views 26 - 27 - - Users can filter tasks by status: all, active (not completed), or completed 28 - - Users can filter tasks by project 29 - - Users can filter tasks by priority 30 - - Filters must be combinable: a user can view "all urgent tasks in the Work project" for example 31 - - The current filter state must be visible so users know what they're looking at 32 - 33 - ## Quick Stats 34 - 35 - - Users must see a summary showing: total tasks, completed tasks, overdue tasks, and completion percentage 36 - - The summary must update immediately when tasks are created, completed, or deleted 37 - 38 - ## Integration 39 - 40 - - The system must expose a programmatic interface so external tools can create, read, update, and delete tasks and projects 41 - - The programmatic interface must use standard conventions so it works with common integration platforms (Zapier, Slack bots, calendar sync) 42 - - Every task and project must have a stable unique identifier that external systems can reference 33 + - Deleting a project must never silently delete its tasks 34 + - The system must expose project management as a programmatic interface 43 35 44 36 ## Web Experience 45 37 ··· 54 46 - Users can delete a task via a small delete button that appears on hover 55 47 - The design must be clean, modern, and responsive with a maximum content width of 800px, system-ui font, and a light neutral color scheme with colored accents for priorities and projects 56 48 - All interactions (create, complete, edit, delete, filter) must work without page reloads by calling the programmatic interface and updating the display 57 - 58 - ## Data Integrity 59 - 60 - - Task titles must not be empty and must not exceed 500 characters 61 - - Descriptions must not exceed 5000 characters 62 - - Priority must always be one of: urgent, high, normal, low 63 - - Due dates must be valid dates; the system must reject obviously invalid dates 64 - - Deleting a project must never silently delete its tasks 65 - - The system must never lose data due to concurrent updates; last-write-wins is acceptable for this version
+6
examples/todo-app/src/app.ts
··· 9 9 10 10 app.get('/health', (c) => c.json({ status: 'ok', uptime: process.uptime() })); 11 11 12 + // Error handler — log details and return JSON 13 + app.onError((err, c) => { 14 + console.error('Unhandled error:', err.message, err.stack); 15 + return c.json({ error: err.message }, 500); 16 + }); 17 + 12 18 /** 13 19 * Mount a route module. Call this for each generated module. 14 20 */
+1 -1
examples/todo-app/src/generated/index.ts
··· 7 7 export * as todos from './todos/index.js'; 8 8 9 9 export const services = [ 10 - { name: 'Todos', dir: 'todos', port: 3000, modules: 7 }, 10 + { name: 'Todos', dir: 'todos', port: 3000, modules: 3 }, 11 11 ] as const;
+13 -131
examples/todo-app/src/generated/todos/__tests__/todos.test.ts
··· 1 1 /** 2 2 * Todos — Generated Tests 3 - * 4 3 * AUTO-GENERATED by Phoenix VCS 5 - * Tests module structure, server health, and Phoenix traceability. 6 4 */ 7 5 8 - import { describe, it, expect, afterAll } from 'vitest'; 9 - import { startServer } from '../server.js'; 10 - 11 - import * as dataIntegrity from '../data-integrity.js'; 12 - import * as filteringAndViews from '../filtering-and-views.js'; 13 - import * as integration from '../integration.js'; 14 - import * as projects from '../projects.js'; 15 - import * as quickStats from '../quick-stats.js'; 16 - import * as tasks from '../tasks.js'; 17 - import * as webExperience from '../web-experience.js'; 6 + import { describe, it, expect } from 'vitest'; 7 + import projects from '../projects.js'; 8 + import tasks from '../tasks.js'; 9 + import web_experience from '../web-experience.js'; 18 10 19 11 describe('Todos modules', () => { 20 - describe('Data Integrity', () => { 21 - it('exports Phoenix traceability metadata', () => { 22 - expect(dataIntegrity._phoenix).toBeDefined(); 23 - expect(dataIntegrity._phoenix.name).toBe('Data Integrity'); 24 - expect(dataIntegrity._phoenix.risk_tier).toBeTruthy(); 25 - }); 26 - 27 - it('has exported functions', () => { 28 - const exports = Object.keys(dataIntegrity).filter(k => k !== '_phoenix'); 29 - expect(exports.length).toBeGreaterThan(0); 30 - }); 31 - }); 32 - 33 - describe('Filtering and Views', () => { 34 - it('exports Phoenix traceability metadata', () => { 35 - expect(filteringAndViews._phoenix).toBeDefined(); 36 - expect(filteringAndViews._phoenix.name).toBe('Filtering and Views'); 37 - expect(filteringAndViews._phoenix.risk_tier).toBeTruthy(); 38 - }); 39 - 40 - it('has exported functions', () => { 41 - const exports = Object.keys(filteringAndViews).filter(k => k !== '_phoenix'); 42 - expect(exports.length).toBeGreaterThan(0); 43 - }); 44 - }); 45 - 46 - describe('Integration', () => { 47 - it('exports Phoenix traceability metadata', () => { 48 - expect(integration._phoenix).toBeDefined(); 49 - expect(integration._phoenix.name).toBe('Integration'); 50 - expect(integration._phoenix.risk_tier).toBeTruthy(); 51 - }); 52 - 53 - it('has exported functions', () => { 54 - const exports = Object.keys(integration).filter(k => k !== '_phoenix'); 55 - expect(exports.length).toBeGreaterThan(0); 56 - }); 57 - }); 58 - 59 12 describe('Projects', () => { 60 - it('exports Phoenix traceability metadata', () => { 61 - expect(projects._phoenix).toBeDefined(); 62 - expect(projects._phoenix.name).toBe('Projects'); 63 - expect(projects._phoenix.risk_tier).toBeTruthy(); 64 - }); 65 - 66 - it('has exported functions', () => { 67 - const exports = Object.keys(projects).filter(k => k !== '_phoenix'); 68 - expect(exports.length).toBeGreaterThan(0); 69 - }); 70 - }); 71 - 72 - describe('Quick Stats', () => { 73 - it('exports Phoenix traceability metadata', () => { 74 - expect(quickStats._phoenix).toBeDefined(); 75 - expect(quickStats._phoenix.name).toBe('Quick Stats'); 76 - expect(quickStats._phoenix.risk_tier).toBeTruthy(); 77 - }); 78 - 79 - it('has exported functions', () => { 80 - const exports = Object.keys(quickStats).filter(k => k !== '_phoenix'); 81 - expect(exports.length).toBeGreaterThan(0); 13 + it('exports a Hono router as default', () => { 14 + expect(projects).toBeDefined(); 15 + expect(typeof projects.fetch).toBe('function'); 82 16 }); 83 17 }); 84 - 85 18 describe('Tasks', () => { 86 - it('exports Phoenix traceability metadata', () => { 87 - expect(tasks._phoenix).toBeDefined(); 88 - expect(tasks._phoenix.name).toBe('Tasks'); 89 - expect(tasks._phoenix.risk_tier).toBeTruthy(); 90 - }); 91 - 92 - it('has exported functions', () => { 93 - const exports = Object.keys(tasks).filter(k => k !== '_phoenix'); 94 - expect(exports.length).toBeGreaterThan(0); 19 + it('exports a Hono router as default', () => { 20 + expect(tasks).toBeDefined(); 21 + expect(typeof tasks.fetch).toBe('function'); 95 22 }); 96 23 }); 97 - 98 24 describe('Web Experience', () => { 99 - it('exports Phoenix traceability metadata', () => { 100 - expect(webExperience._phoenix).toBeDefined(); 101 - expect(webExperience._phoenix.name).toBe('Web Experience'); 102 - expect(webExperience._phoenix.risk_tier).toBeTruthy(); 103 - }); 104 - 105 - it('has exported functions', () => { 106 - const exports = Object.keys(webExperience).filter(k => k !== '_phoenix'); 107 - expect(exports.length).toBeGreaterThan(0); 25 + it('exports a Hono router as default', () => { 26 + expect(web_experience).toBeDefined(); 27 + expect(typeof web_experience.fetch).toBe('function'); 108 28 }); 109 - }); 110 - 111 - }); 112 - 113 - describe('Todos server', () => { 114 - const instance = startServer(0); // random port 115 - 116 - afterAll(() => new Promise<void>(resolve => instance.server.close(() => resolve()))); 117 - 118 - it('GET /health returns 200', async () => { 119 - await instance.ready; 120 - const res = await fetch(`http://localhost:${instance.port}/health`); 121 - expect(res.status).toBe(200); 122 - const body = await res.json() as Record<string, unknown>; 123 - expect(body.status).toBe('ok'); 124 - expect(body.service).toBe('Todos'); 125 - }); 126 - 127 - it('GET /metrics returns request counts', async () => { 128 - await instance.ready; 129 - const res = await fetch(`http://localhost:${instance.port}/metrics`); 130 - expect(res.status).toBe(200); 131 - const body = await res.json() as Record<string, unknown>; 132 - expect(typeof body.requests_total).toBe('number'); 133 - }); 134 - 135 - it('GET /modules lists all registered modules', async () => { 136 - await instance.ready; 137 - const res = await fetch(`http://localhost:${instance.port}/modules`); 138 - expect(res.status).toBe(200); 139 - const body = await res.json() as Array<Record<string, unknown>>; 140 - expect(body.length).toBe(7); 141 - }); 142 - 143 - it('GET /unknown returns 404', async () => { 144 - await instance.ready; 145 - const res = await fetch(`http://localhost:${instance.port}/unknown`); 146 - expect(res.status).toBe(404); 147 29 }); 148 30 });
-15
examples/todo-app/src/generated/todos/data-integrity.ts
··· 1 - import { Hono } from 'hono'; 2 - 3 - const router = new Hono(); 4 - 5 - router.get('/', (c) => c.json({ stub: true, module: 'Data Integrity', message: 'Not yet implemented' })); 6 - 7 - export default router; 8 - 9 - /** @internal Phoenix VCS traceability — do not remove. */ 10 - export const _phoenix = { 11 - iu_id: '1f677d4ba5f46a3cd75931c51f4bdc76ac0da22a981004342a40d675ad84749b', 12 - name: 'Data Integrity', 13 - risk_tier: 'high', 14 - canon_ids: [9 as const], 15 - } as const;
-174
examples/todo-app/src/generated/todos/filtering-and-views.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register table migrations for tasks and projects 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 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 - 34 - const router = new Hono(); 35 - 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 - 50 - let sql = ` 51 - SELECT 52 - tasks.*, 53 - projects.name as project_name, 54 - projects.color as project_color, 55 - CASE 56 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 57 - THEN 1 58 - ELSE 0 59 - END as is_overdue 60 - FROM tasks 61 - LEFT JOIN projects ON tasks.project_id = projects.id 62 - `; 63 - 64 - const conditions: string[] = []; 65 - const params: (string | number)[] = []; 66 - 67 - // Status filter 68 - if (filters.status === 'active') { 69 - conditions.push('tasks.completed = 0'); 70 - } else if (filters.status === 'completed') { 71 - conditions.push('tasks.completed = 1'); 72 - } 73 - 74 - // Project filter 75 - if (filters.project_id) { 76 - if (filters.project_id === 'inbox') { 77 - conditions.push('tasks.project_id IS NULL'); 78 - } else { 79 - conditions.push('tasks.project_id = ?'); 80 - params.push(Number(filters.project_id)); 81 - } 82 - } 83 - 84 - // Priority filter 85 - if (filters.priority) { 86 - conditions.push('tasks.priority = ?'); 87 - params.push(filters.priority); 88 - } 89 - 90 - if (conditions.length > 0) { 91 - sql += ' WHERE ' + conditions.join(' AND '); 92 - } 93 - 94 - // Sort by urgency and overdue status 95 - sql += ` ORDER BY 96 - tasks.completed ASC, 97 - is_overdue DESC, 98 - CASE tasks.priority 99 - WHEN 'urgent' THEN 0 100 - WHEN 'high' THEN 1 101 - WHEN 'normal' THEN 2 102 - WHEN 'low' THEN 3 103 - END ASC, 104 - tasks.due_date ASC NULLS LAST, 105 - tasks.created_at DESC 106 - `; 107 - 108 - const tasks = db.prepare(sql).all(...params); 109 - 110 - // Build current filter state description 111 - const filterState: string[] = []; 112 - 113 - if (filters.status && filters.status !== 'all') { 114 - filterState.push(`Status: ${filters.status}`); 115 - } 116 - 117 - if (filters.project_id) { 118 - if (filters.project_id === 'inbox') { 119 - filterState.push('Project: Inbox'); 120 - } else { 121 - const project = db.prepare('SELECT name FROM projects WHERE id = ?').get(Number(filters.project_id)) as { name: string } | undefined; 122 - if (project) { 123 - filterState.push(`Project: ${project.name}`); 124 - } 125 - } 126 - } 127 - 128 - if (filters.priority) { 129 - filterState.push(`Priority: ${filters.priority}`); 130 - } 131 - 132 - return c.json({ 133 - tasks, 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 - } 139 - }); 140 - }); 141 - 142 - // Get available filter options 143 - router.get('/options', (c) => { 144 - const projects = db.prepare('SELECT id, name, color FROM projects ORDER BY name').all(); 145 - const priorities = ['urgent', 'high', 'normal', 'low']; 146 - const statuses = ['all', 'active', 'completed']; 147 - 148 - return c.json({ 149 - projects: [ 150 - { id: 'inbox', name: 'Inbox', color: '#6b7280' }, 151 - ...projects 152 - ], 153 - priorities, 154 - statuses 155 - }); 156 - }); 157 - 158 - // Clear all filters 159 - router.delete('/filters', (c) => { 160 - return c.json({ 161 - message: 'Filters cleared', 162 - redirect_url: '/' 163 - }); 164 - }); 165 - 166 - export default router; 167 - 168 - /** @internal Phoenix VCS traceability — do not remove. */ 169 - export const _phoenix = { 170 - iu_id: 'c986c6a7885993ce90d626af61ecc90d5de2801eac95c0ff99b368e0e90e8bcc', 171 - name: 'Filtering and Views', 172 - risk_tier: 'low', 173 - canon_ids: [2 as const], 174 - } as const;
-4
examples/todo-app/src/generated/todos/index.ts
··· 5 5 * Barrel export for all Todos modules. 6 6 */ 7 7 8 - export * as dataIntegrity from './data-integrity.js'; 9 - export * as filteringAndViews from './filtering-and-views.js'; 10 - export * as integration from './integration.js'; 11 8 export * as projects from './projects.js'; 12 - export * as quickStats from './quick-stats.js'; 13 9 export * as tasks from './tasks.js'; 14 10 export * as webExperience from './web-experience.js';
-296
examples/todo-app/src/generated/todos/integration.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register migrations for tasks and projects tables 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 - updated_at TEXT NOT NULL DEFAULT (datetime('now')) 26 - ) 27 - `); 28 - 29 - const CreateTaskSchema = z.object({ 30 - title: z.string().min(1).max(200), 31 - description: z.string().optional().default(''), 32 - priority: z.enum(['urgent', 'high', 'normal', 'low']).optional().default('normal'), 33 - due_date: z.string().nullable().optional(), 34 - project_id: z.number().int().nullable().optional(), 35 - }); 36 - 37 - const UpdateTaskSchema = z.object({ 38 - title: z.string().min(1).max(200).optional(), 39 - description: z.string().optional(), 40 - priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 41 - due_date: z.string().nullable().optional(), 42 - completed: z.number().int().min(0).max(1).optional(), 43 - project_id: z.number().int().nullable().optional(), 44 - }); 45 - 46 - const CreateProjectSchema = z.object({ 47 - name: z.string().min(1).max(100), 48 - color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().default('#3b82f6'), 49 - }); 50 - 51 - const UpdateProjectSchema = z.object({ 52 - name: z.string().min(1).max(100).optional(), 53 - color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(), 54 - }); 55 - 56 - const router = new Hono(); 57 - 58 - // Tasks endpoints 59 - router.get('/tasks', (c) => { 60 - let sql = ` 61 - SELECT tasks.*, projects.name as project_name, projects.color as project_color 62 - FROM tasks 63 - LEFT JOIN projects ON tasks.project_id = projects.id 64 - `; 65 - const conditions: string[] = []; 66 - const params: (string | number)[] = []; 67 - 68 - const status = c.req.query('status'); 69 - if (status === 'active') { conditions.push('tasks.completed = 0'); } 70 - else if (status === 'completed') { conditions.push('tasks.completed = 1'); } 71 - 72 - const priority = c.req.query('priority'); 73 - if (priority) { conditions.push('tasks.priority = ?'); params.push(priority); } 74 - 75 - const projectId = c.req.query('project_id'); 76 - if (projectId) { conditions.push('tasks.project_id = ?'); params.push(Number(projectId)); } 77 - 78 - if (conditions.length > 0) sql += ' WHERE ' + conditions.join(' AND '); 79 - 80 - sql += ` ORDER BY 81 - CASE tasks.priority 82 - WHEN 'urgent' THEN 0 83 - WHEN 'high' THEN 1 84 - WHEN 'normal' THEN 2 85 - WHEN 'low' THEN 3 86 - END, 87 - CASE 88 - WHEN tasks.due_date IS NOT NULL AND tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 89 - ELSE 1 90 - END, 91 - tasks.created_at DESC 92 - `; 93 - 94 - return c.json(db.prepare(sql).all(...params)); 95 - }); 96 - 97 - router.get('/tasks/:id', (c) => { 98 - const task = db.prepare(` 99 - SELECT tasks.*, projects.name as project_name, projects.color as project_color 100 - FROM tasks 101 - LEFT JOIN projects ON tasks.project_id = projects.id 102 - WHERE tasks.id = ? 103 - `).get(c.req.param('id')); 104 - if (!task) return c.json({ error: 'Task not found' }, 404); 105 - return c.json(task); 106 - }); 107 - 108 - router.post('/tasks', async (c) => { 109 - let body; 110 - try { body = await c.req.json(); } 111 - catch { return c.json({ error: 'Invalid JSON' }, 400); } 112 - 113 - const result = CreateTaskSchema.safeParse(body); 114 - if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 115 - 116 - const { title, description, priority, due_date, project_id } = result.data; 117 - 118 - if (project_id !== null && project_id !== undefined) { 119 - const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 120 - if (!project) return c.json({ error: 'Project not found' }, 400); 121 - } 122 - 123 - const info = db.prepare(` 124 - INSERT INTO tasks (title, description, priority, due_date, project_id) 125 - VALUES (?, ?, ?, ?, ?) 126 - `).run(title, description, priority, due_date, project_id); 127 - 128 - const task = db.prepare(` 129 - SELECT tasks.*, projects.name as project_name, projects.color as project_color 130 - FROM tasks 131 - LEFT JOIN projects ON tasks.project_id = projects.id 132 - WHERE tasks.id = ? 133 - `).get(info.lastInsertRowid); 134 - 135 - return c.json(task, 201); 136 - }); 137 - 138 - router.patch('/tasks/:id', async (c) => { 139 - const id = c.req.param('id'); 140 - const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 141 - if (!existing) return c.json({ error: 'Task not found' }, 404); 142 - 143 - let body; 144 - try { body = await c.req.json(); } 145 - catch { return c.json({ error: 'Invalid JSON' }, 400); } 146 - 147 - const result = UpdateTaskSchema.safeParse(body); 148 - if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 149 - 150 - const updates = result.data; 151 - 152 - if (updates.project_id !== undefined && updates.project_id !== null) { 153 - const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 154 - if (!project) return c.json({ error: 'Project not found' }, 400); 155 - } 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 - 164 - const updated = db.prepare(` 165 - SELECT tasks.*, projects.name as project_name, projects.color as project_color 166 - FROM tasks 167 - LEFT JOIN projects ON tasks.project_id = projects.id 168 - WHERE tasks.id = ? 169 - `).get(id); 170 - 171 - return c.json(updated); 172 - }); 173 - 174 - router.delete('/tasks/:id', (c) => { 175 - const id = c.req.param('id'); 176 - const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 177 - if (!existing) return c.json({ error: 'Task not found' }, 404); 178 - 179 - db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 180 - return c.body(null, 204); 181 - }); 182 - 183 - // Projects endpoints 184 - router.get('/projects', (c) => { 185 - const projects = db.prepare(` 186 - SELECT projects.*, 187 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 188 - FROM projects 189 - LEFT JOIN tasks ON projects.id = tasks.project_id 190 - GROUP BY projects.id 191 - ORDER BY projects.created_at DESC 192 - `).all(); 193 - return c.json(projects); 194 - }); 195 - 196 - router.get('/projects/:id', (c) => { 197 - const project = db.prepare(` 198 - SELECT projects.*, 199 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 200 - FROM projects 201 - LEFT JOIN tasks ON projects.id = tasks.project_id 202 - WHERE projects.id = ? 203 - GROUP BY projects.id 204 - `).get(c.req.param('id')); 205 - if (!project) return c.json({ error: 'Project not found' }, 404); 206 - return c.json(project); 207 - }); 208 - 209 - router.post('/projects', async (c) => { 210 - let body; 211 - try { body = await c.req.json(); } 212 - catch { return c.json({ error: 'Invalid JSON' }, 400); } 213 - 214 - const result = CreateProjectSchema.safeParse(body); 215 - if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 216 - 217 - const { name, color } = result.data; 218 - 219 - try { 220 - const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 221 - const project = db.prepare(` 222 - SELECT projects.*, 223 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 224 - FROM projects 225 - LEFT JOIN tasks ON projects.id = tasks.project_id 226 - WHERE projects.id = ? 227 - GROUP BY projects.id 228 - `).get(info.lastInsertRowid); 229 - return c.json(project, 201); 230 - } catch (error: unknown) { 231 - if (error instanceof Error && 'code' in error && (error as any).code === 'SQLITE_CONSTRAINT_UNIQUE') { 232 - return c.json({ error: 'Project name already exists' }, 400); 233 - } 234 - throw error; 235 - } 236 - }); 237 - 238 - router.patch('/projects/:id', async (c) => { 239 - const id = c.req.param('id'); 240 - const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 241 - if (!existing) return c.json({ error: 'Project not found' }, 404); 242 - 243 - let body; 244 - try { body = await c.req.json(); } 245 - catch { return c.json({ error: 'Invalid JSON' }, 400); } 246 - 247 - const result = UpdateProjectSchema.safeParse(body); 248 - if (!result.success) return c.json({ error: result.error.issues[0].message }, 400); 249 - 250 - const updates = result.data; 251 - 252 - try { 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 - 256 - const updated = db.prepare(` 257 - SELECT projects.*, 258 - COUNT(CASE WHEN tasks.completed = 0 THEN 1 END) as active_task_count 259 - FROM projects 260 - LEFT JOIN tasks ON projects.id = tasks.project_id 261 - WHERE projects.id = ? 262 - GROUP BY projects.id 263 - `).get(id); 264 - 265 - return c.json(updated); 266 - } catch (error: unknown) { 267 - if (error instanceof Error && 'code' in error && (error as any).code === 'SQLITE_CONSTRAINT_UNIQUE') { 268 - return c.json({ error: 'Project name already exists' }, 400); 269 - } 270 - throw error; 271 - } 272 - }); 273 - 274 - router.delete('/projects/:id', (c) => { 275 - const id = c.req.param('id'); 276 - const existing = db.prepare('SELECT id FROM projects WHERE id = ?').get(id); 277 - if (!existing) return c.json({ error: 'Project not found' }, 404); 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) { 281 - return c.json({ error: 'Cannot delete project with existing tasks' }, 400); 282 - } 283 - 284 - db.prepare('DELETE FROM projects WHERE id = ?').run(id); 285 - return c.body(null, 204); 286 - }); 287 - 288 - export default router; 289 - 290 - /** @internal Phoenix VCS traceability — do not remove. */ 291 - export const _phoenix = { 292 - iu_id: '7ee19d155ffb4b7ff0346e313207867d19efacb2af2bfcb3dce82a7f2adfd73f', 293 - name: 'Integration', 294 - risk_tier: 'low', 295 - canon_ids: [3 as const], 296 - } as const;
+50 -36
examples/todo-app/src/generated/todos/projects.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register table migrations 5 + // Register table migration 6 6 registerMigration('projects', ` 7 7 CREATE TABLE IF NOT EXISTS projects ( 8 8 id INTEGER PRIMARY KEY AUTOINCREMENT, ··· 12 12 ) 13 13 `); 14 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 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 15 const CreateProjectSchema = z.object({ 29 - name: z.string().min(1).max(200), 16 + name: z.string().min(1).max(100), 30 17 color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional().default('#3b82f6'), 31 18 }); 32 19 33 20 const UpdateProjectSchema = z.object({ 34 - name: z.string().min(1).max(200).optional(), 21 + name: z.string().min(1).max(100).optional(), 35 22 color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(), 36 23 }); 37 24 ··· 41 28 router.get('/', (c) => { 42 29 const projects = db.prepare(` 43 30 SELECT 44 - p.*, 45 - COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 31 + p.id, 32 + p.name, 33 + p.color, 34 + p.created_at, 35 + COALESCE(task_counts.active_count, 0) as active_task_count 46 36 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 37 + LEFT JOIN ( 38 + SELECT 39 + project_id, 40 + COUNT(*) as active_count 41 + FROM tasks 42 + WHERE completed = 0 43 + GROUP BY project_id 44 + ) task_counts ON p.id = task_counts.project_id 49 45 ORDER BY p.name 50 46 `).all(); 51 47 return c.json(projects); ··· 55 51 router.get('/:id', (c) => { 56 52 const project = db.prepare(` 57 53 SELECT 58 - p.*, 59 - COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 54 + p.id, 55 + p.name, 56 + p.color, 57 + p.created_at, 58 + COALESCE(task_counts.active_count, 0) as active_task_count 60 59 FROM projects p 61 - LEFT JOIN tasks t ON p.id = t.project_id 60 + LEFT JOIN ( 61 + SELECT 62 + project_id, 63 + COUNT(*) as active_count 64 + FROM tasks 65 + WHERE completed = 0 66 + GROUP BY project_id 67 + ) task_counts ON p.id = task_counts.project_id 62 68 WHERE p.id = ? 63 - GROUP BY p.id, p.name, p.color, p.created_at 64 69 `).get(c.req.param('id')); 65 70 if (!project) return c.json({ error: 'Project not found' }, 404); 66 71 return c.json(project); ··· 86 91 const info = db.prepare('INSERT INTO projects (name, color) VALUES (?, ?)').run(name, color); 87 92 const project = db.prepare(` 88 93 SELECT 89 - p.*, 94 + p.id, 95 + p.name, 96 + p.color, 97 + p.created_at, 90 98 0 as active_task_count 91 99 FROM projects p 92 100 WHERE p.id = ? ··· 130 138 131 139 const updated = db.prepare(` 132 140 SELECT 133 - p.*, 134 - COALESCE(COUNT(CASE WHEN t.completed = 0 THEN 1 END), 0) as active_task_count 141 + p.id, 142 + p.name, 143 + p.color, 144 + p.created_at, 145 + COALESCE(task_counts.active_count, 0) as active_task_count 135 146 FROM projects p 136 - LEFT JOIN tasks t ON p.id = t.project_id 147 + LEFT JOIN ( 148 + SELECT 149 + project_id, 150 + COUNT(*) as active_count 151 + FROM tasks 152 + WHERE completed = 0 153 + GROUP BY project_id 154 + ) task_counts ON p.id = task_counts.project_id 137 155 WHERE p.id = ? 138 - GROUP BY p.id, p.name, p.color, p.created_at 139 156 `).get(id); 140 157 return c.json(updated); 141 158 } catch (error: any) { ··· 155 172 // Check for tasks in this project 156 173 const taskCount = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE project_id = ?').get(id) as { count: number }; 157 174 if (taskCount.count > 0) { 158 - return c.json({ 159 - error: 'Cannot delete project that contains tasks', 160 - task_count: taskCount.count 161 - }, 400); 175 + return c.json({ error: 'Cannot delete project that contains tasks' }, 400); 162 176 } 163 177 164 178 db.prepare('DELETE FROM projects WHERE id = ?').run(id); ··· 169 183 170 184 /** @internal Phoenix VCS traceability — do not remove. */ 171 185 export const _phoenix = { 172 - iu_id: '684e98680b126a8a1535a88875ffb4157cfc3bc1881f7a6b34c2fdae1830e9b1', 186 + iu_id: '4144f40fc7c93037f0d2e7445ad0d5911b755792604940786e5ea04a654683b6', 173 187 name: 'Projects', 174 - risk_tier: 'low', 175 - canon_ids: [3 as const], 188 + risk_tier: 'high', 189 + canon_ids: [6 as const], 176 190 } as const;
-58
examples/todo-app/src/generated/todos/quick-stats.ts
··· 1 - import { Hono } from 'hono'; 2 - import { db, registerMigration } from '../../db.js'; 3 - import { z } from 'zod'; 4 - 5 - // Register table migrations for tasks and projects 6 - registerMigration('projects', ` 7 - CREATE TABLE IF NOT EXISTS projects ( 8 - id INTEGER PRIMARY KEY AUTOINCREMENT, 9 - name TEXT NOT NULL, 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 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 router = new Hono(); 29 - 30 - // Get stats summary 31 - router.get('/', (c) => { 32 - const totalTasks = db.prepare('SELECT COUNT(*) as count FROM tasks').get() as { count: number }; 33 - 34 - const completedTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE completed = 1').get() as { count: number }; 35 - 36 - const overdueTasks = db.prepare('SELECT COUNT(*) as count FROM tasks WHERE due_date < date("now") AND completed = 0').get() as { count: number }; 37 - 38 - const completionPercentage = totalTasks.count > 0 39 - ? Math.round((completedTasks.count / totalTasks.count) * 100) 40 - : 0; 41 - 42 - return c.json({ 43 - total_tasks: totalTasks.count, 44 - completed_tasks: completedTasks.count, 45 - overdue_tasks: overdueTasks.count, 46 - completion_percentage: completionPercentage 47 - }); 48 - }); 49 - 50 - export default router; 51 - 52 - /** @internal Phoenix VCS traceability — do not remove. */ 53 - export const _phoenix = { 54 - iu_id: 'e971f2b7f67c9ac5f5b54f0baf9d19f5e50593b2fc1e9f9c93fb01f6029e712e', 55 - name: 'Quick Stats', 56 - risk_tier: 'low', 57 - canon_ids: [2 as const], 58 - } as const;
-131
examples/todo-app/src/generated/todos/server.ts
··· 1 - /** 2 - * Todos — HTTP Server 3 - * 4 - * AUTO-GENERATED by Phoenix VCS 5 - * Provides health check, metrics, and module endpoints. 6 - */ 7 - 8 - import { createServer, IncomingMessage, ServerResponse } from 'node:http'; 9 - 10 - import * as dataIntegrity from './data-integrity.js'; 11 - import * as filteringAndViews from './filtering-and-views.js'; 12 - import * as integration from './integration.js'; 13 - import * as projects from './projects.js'; 14 - import * as quickStats from './quick-stats.js'; 15 - import * as tasks from './tasks.js'; 16 - import * as webExperience from './web-experience.js'; 17 - 18 - // ─── Metrics ───────────────────────────────────────────────────────────────── 19 - 20 - const _svcMetrics = { 21 - requests_total: 0, 22 - requests_by_path: {} as Record<string, number>, 23 - errors_total: 0, 24 - uptime_start: Date.now(), 25 - }; 26 - 27 - // ─── Module Registry ───────────────────────────────────────────────────────── 28 - 29 - const _svcModules = { 30 - 'data-integrity': dataIntegrity, 31 - 'filtering-and-views': filteringAndViews, 32 - 'integration': integration, 33 - 'projects': projects, 34 - 'quick-stats': quickStats, 35 - 'tasks': tasks, 36 - 'web-experience': webExperience, 37 - }; 38 - 39 - // ─── Router ────────────────────────────────────────────────────────────────── 40 - 41 - type Handler = (req: IncomingMessage, res: ServerResponse) => void | Promise<void>; 42 - 43 - const routes: Record<string, Handler> = { 44 - '/health': (_req, res) => { 45 - res.writeHead(200, { 'Content-Type': 'application/json' }); 46 - res.end(JSON.stringify({ 47 - status: 'ok', 48 - service: 'Todos', 49 - uptime: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 50 - modules: Object.keys(_svcModules), 51 - })); 52 - }, 53 - 54 - '/metrics': (_req, res) => { 55 - res.writeHead(200, { 'Content-Type': 'application/json' }); 56 - res.end(JSON.stringify({ 57 - ..._svcMetrics, 58 - uptime_seconds: Math.floor((Date.now() - _svcMetrics.uptime_start) / 1000), 59 - }, null, 2)); 60 - }, 61 - 62 - '/modules': (_req, res) => { 63 - const info = Object.entries(_svcModules).map(([name, mod]) => { 64 - const phoenix = (mod as Record<string, unknown>)._phoenix as Record<string, unknown> | undefined; 65 - return { 66 - name, 67 - risk_tier: phoenix?.risk_tier ?? 'unknown', 68 - exports: Object.keys(mod).filter(k => k !== '_phoenix'), 69 - }; 70 - }); 71 - res.writeHead(200, { 'Content-Type': 'application/json' }); 72 - res.end(JSON.stringify(info, null, 2)); 73 - }, 74 - }; 75 - 76 - // ─── Server ────────────────────────────────────────────────────────────────── 77 - 78 - function handleRequest(req: IncomingMessage, res: ServerResponse): void { 79 - const url = req.url ?? '/'; 80 - const path = url.split('?')[0]; 81 - 82 - _svcMetrics.requests_total++; 83 - _svcMetrics.requests_by_path[path] = (_svcMetrics.requests_by_path[path] ?? 0) + 1; 84 - 85 - const handler = routes[path]; 86 - if (handler) { 87 - try { 88 - handler(req, res); 89 - } catch (err) { 90 - _svcMetrics.errors_total++; 91 - res.writeHead(500, { 'Content-Type': 'application/json' }); 92 - res.end(JSON.stringify({ error: String(err) })); 93 - } 94 - } else { 95 - res.writeHead(404, { 'Content-Type': 'application/json' }); 96 - res.end(JSON.stringify({ 97 - error: 'Not Found', 98 - path, 99 - available: Object.keys(routes), 100 - })); 101 - } 102 - } 103 - 104 - export function startServer(port?: number): { server: ReturnType<typeof createServer>; port: number; ready: Promise<void> } { 105 - const requestedPort = port ?? parseInt(process.env.TODOS_PORT ?? process.env.PORT ?? '3000', 10); 106 - const server = createServer(handleRequest); 107 - let actualPort = requestedPort; 108 - 109 - const ready = new Promise<void>(resolve => { 110 - server.listen(requestedPort, () => { 111 - const addr = server.address(); 112 - if (addr && typeof addr === 'object') actualPort = addr.port; 113 - result.port = actualPort; 114 - console.log(`Todos listening on http://localhost:${actualPort}`); 115 - console.log(` /health — health check`); 116 - console.log(` /metrics — request metrics`); 117 - console.log(` /modules — registered modules`); 118 - resolve(); 119 - }); 120 - }); 121 - 122 - const result = { server, port: actualPort, ready }; 123 - return result; 124 - } 125 - 126 - // Start when run directly 127 - const isMain = process.argv[1]?.endsWith('/todos/server.js') || 128 - process.argv[1]?.endsWith('/todos/server.ts'); 129 - if (isMain) { 130 - startServer(); 131 - }
+88 -36
examples/todo-app/src/generated/todos/tasks.ts
··· 2 2 import { db, registerMigration } from '../../db.js'; 3 3 import { z } from 'zod'; 4 4 5 - // Register table migration 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 + 6 15 registerMigration('tasks', ` 7 16 CREATE TABLE IF NOT EXISTS tasks ( 8 17 id INTEGER PRIMARY KEY AUTOINCREMENT, 9 18 title TEXT NOT NULL, 10 - description TEXT DEFAULT '', 19 + description TEXT NOT NULL DEFAULT '', 11 20 priority TEXT NOT NULL DEFAULT 'normal' CHECK (priority IN ('urgent', 'high', 'normal', 'low')), 12 21 due_date TEXT, 13 22 completed INTEGER NOT NULL DEFAULT 0, ··· 17 26 `); 18 27 19 28 const CreateTaskSchema = z.object({ 20 - title: z.string().min(1).max(200), 21 - description: z.string().optional().default(''), 29 + title: z.string().min(1, 'Title is required').max(500, 'Title must not exceed 500 characters'), 30 + description: z.string().max(5000, 'Description must not exceed 5000 characters').optional().default(''), 22 31 priority: z.enum(['urgent', 'high', 'normal', 'low']).optional().default('normal'), 23 - due_date: z.string().nullable().optional(), 24 - project_id: z.number().int().nullable().optional(), 32 + due_date: z.string().refine((date) => { 33 + if (!date) return true; 34 + const parsed = new Date(date); 35 + return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 36 + }, 'Invalid due date').optional(), 37 + project_id: z.number().int().optional(), 25 38 }); 26 39 27 40 const UpdateTaskSchema = z.object({ 28 - title: z.string().min(1).max(200).optional(), 29 - description: z.string().optional(), 41 + title: z.string().min(1, 'Title is required').max(500, 'Title must not exceed 500 characters').optional(), 42 + description: z.string().max(5000, 'Description must not exceed 5000 characters').optional(), 30 43 priority: z.enum(['urgent', 'high', 'normal', 'low']).optional(), 31 - due_date: z.string().nullable().optional(), 44 + due_date: z.string().nullable().refine((date) => { 45 + if (!date) return true; 46 + const parsed = new Date(date); 47 + return !isNaN(parsed.getTime()) && parsed.getFullYear() > 1900 && parsed.getFullYear() < 3000; 48 + }, 'Invalid due date').optional(), 32 49 completed: z.number().int().min(0).max(1).optional(), 33 50 project_id: z.number().int().nullable().optional(), 34 51 }); 35 52 36 53 const router = new Hono(); 37 54 38 - // List all tasks with filtering and sorting 55 + // Stats endpoint - moved before /:id to avoid route conflicts 56 + router.get('/stats', (c) => { 57 + const projectId = c.req.query('project_id'); 58 + let whereClause = ''; 59 + const params: (string | number)[] = []; 60 + 61 + if (projectId !== undefined) { 62 + if (projectId === 'inbox') { 63 + whereClause = 'WHERE project_id IS NULL'; 64 + } else { 65 + whereClause = 'WHERE project_id = ?'; 66 + params.push(Number(projectId)); 67 + } 68 + } 69 + 70 + const stats = db.prepare(` 71 + SELECT 72 + COUNT(*) as total_tasks, 73 + SUM(completed) as completed_tasks, 74 + COUNT(CASE WHEN due_date < date('now') AND completed = 0 THEN 1 END) as overdue_tasks 75 + FROM tasks ${whereClause} 76 + `).get(...params) as { total_tasks: number; completed_tasks: number; overdue_tasks: number }; 77 + 78 + const completion_percentage = stats.total_tasks > 0 79 + ? Math.round((stats.completed_tasks / stats.total_tasks) * 100) 80 + : 0; 81 + 82 + return c.json({ 83 + total_tasks: stats.total_tasks, 84 + completed_tasks: stats.completed_tasks, 85 + overdue_tasks: stats.overdue_tasks, 86 + completion_percentage 87 + }); 88 + }); 89 + 90 + // List tasks with filtering and sorting 39 91 router.get('/', (c) => { 40 92 let sql = ` 41 - SELECT tasks.*, projects.name as project_name 93 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 42 94 FROM tasks 43 95 LEFT JOIN projects ON tasks.project_id = projects.id 44 96 `; ··· 52 104 conditions.push('tasks.completed = 1'); 53 105 } 54 106 107 + const projectId = c.req.query('project_id'); 108 + if (projectId !== undefined) { 109 + if (projectId === 'inbox') { 110 + conditions.push('tasks.project_id IS NULL'); 111 + } else { 112 + conditions.push('tasks.project_id = ?'); 113 + params.push(Number(projectId)); 114 + } 115 + } 116 + 55 117 const priority = c.req.query('priority'); 56 118 if (priority) { 57 119 conditions.push('tasks.priority = ?'); 58 120 params.push(priority); 59 121 } 60 122 61 - const projectId = c.req.query('project_id'); 62 - if (projectId) { 63 - conditions.push('tasks.project_id = ?'); 64 - params.push(Number(projectId)); 65 - } 66 - 67 123 if (conditions.length > 0) { 68 124 sql += ' WHERE ' + conditions.join(' AND '); 69 125 } 70 126 71 127 // Sort by urgency and overdue status first, then by creation date 72 128 sql += ` ORDER BY 73 - CASE WHEN tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 ELSE 1 END, 74 129 CASE tasks.priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 WHEN 'low' THEN 3 END, 130 + CASE WHEN tasks.due_date < date('now') AND tasks.completed = 0 THEN 0 ELSE 1 END, 75 131 tasks.created_at DESC 76 132 `; 77 133 ··· 82 138 // Get single task 83 139 router.get('/:id', (c) => { 84 140 const task = db.prepare(` 85 - SELECT tasks.*, projects.name as project_name 141 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 86 142 FROM tasks 87 143 LEFT JOIN projects ON tasks.project_id = projects.id 88 144 WHERE tasks.id = ? ··· 94 150 95 151 // Create task 96 152 router.post('/', async (c) => { 97 - let body; 153 + let body: unknown; 98 154 try { 99 155 body = await c.req.json(); 100 156 } catch { ··· 109 165 const { title, description, priority, due_date, project_id } = result.data; 110 166 111 167 // Validate project exists if provided 112 - if (project_id !== undefined && project_id !== null) { 168 + if (project_id !== undefined) { 113 169 const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(project_id); 114 170 if (!project) { 115 171 return c.json({ error: 'Project not found' }, 400); ··· 119 175 const info = db.prepare(` 120 176 INSERT INTO tasks (title, description, priority, due_date, project_id) 121 177 VALUES (?, ?, ?, ?, ?) 122 - `).run(title, description, priority, due_date, project_id); 178 + `).run(title, description, priority, due_date ?? null, project_id ?? null); 123 179 124 180 const task = db.prepare(` 125 - SELECT tasks.*, projects.name as project_name 181 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 126 182 FROM tasks 127 183 LEFT JOIN projects ON tasks.project_id = projects.id 128 184 WHERE tasks.id = ? ··· 134 190 // Update task 135 191 router.patch('/:id', async (c) => { 136 192 const id = c.req.param('id'); 137 - const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 138 - if (!existing) { 139 - return c.json({ error: 'Task not found' }, 404); 140 - } 193 + const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 194 + if (!existing) return c.json({ error: 'Task not found' }, 404); 141 195 142 - let body; 196 + let body: unknown; 143 197 try { 144 198 body = await c.req.json(); 145 199 } catch { ··· 153 207 154 208 const updates = result.data; 155 209 156 - // Validate project exists if being updated 210 + // Validate project exists if provided 157 211 if (updates.project_id !== undefined && updates.project_id !== null) { 158 212 const project = db.prepare('SELECT id FROM projects WHERE id = ?').get(updates.project_id); 159 213 if (!project) { ··· 182 236 } 183 237 184 238 const updated = db.prepare(` 185 - SELECT tasks.*, projects.name as project_name 239 + SELECT tasks.*, projects.name as project_name, projects.color as project_color 186 240 FROM tasks 187 241 LEFT JOIN projects ON tasks.project_id = projects.id 188 242 WHERE tasks.id = ? ··· 194 248 // Delete task 195 249 router.delete('/:id', (c) => { 196 250 const id = c.req.param('id'); 197 - const existing = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id); 198 - if (!existing) { 199 - return c.json({ error: 'Task not found' }, 404); 200 - } 251 + const existing = db.prepare('SELECT id FROM tasks WHERE id = ?').get(id); 252 + if (!existing) return c.json({ error: 'Task not found' }, 404); 201 253 202 254 db.prepare('DELETE FROM tasks WHERE id = ?').run(id); 203 255 return c.body(null, 204); ··· 207 259 208 260 /** @internal Phoenix VCS traceability — do not remove. */ 209 261 export const _phoenix = { 210 - iu_id: '1628f3b0f6e0816a698cf8b53a7b135c5dc11469a9bca1fa49299db6018b08f7', 262 + iu_id: '72e5373eca8ea41d110527651ae938509fb7c778e5a71c99c46d83839e91915c', 211 263 name: 'Tasks', 212 264 risk_tier: 'high', 213 - canon_ids: [4 as const], 265 + canon_ids: [14 as const], 214 266 } as const;
+473 -588
examples/todo-app/src/generated/todos/web-experience.ts
··· 20 20 21 21 body { 22 22 font-family: system-ui, -apple-system, sans-serif; 23 - background: #f8f9fa; 24 - color: #333; 23 + background-color: #f8f9fa; 24 + color: #212529; 25 25 line-height: 1.5; 26 26 } 27 27 ··· 37 37 } 38 38 39 39 .header h1 { 40 - color: #2563eb; 40 + color: #495057; 41 41 margin-bottom: 10px; 42 42 } 43 43 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 { 58 - display: flex; 59 - flex-wrap: wrap; 60 - gap: 10px; 61 - margin-bottom: 20px; 62 - } 63 - 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; 74 - } 75 - 76 - .project-item:hover { 77 - background: #e5e7eb; 78 - } 79 - 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; 97 - } 98 - 99 44 .add-task-form { 100 45 background: white; 101 46 border-radius: 8px; 102 47 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; 48 + margin-bottom: 30px; 49 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 110 50 } 111 51 112 52 .form-row { ··· 121 61 min-width: 200px; 122 62 } 123 63 124 - .form-group label { 64 + .form-group.full-width { 65 + flex: 100%; 66 + } 67 + 68 + label { 125 69 display: block; 126 70 margin-bottom: 5px; 127 71 font-weight: 500; 128 - color: #374151; 72 + color: #495057; 129 73 } 130 74 131 - .form-group input, 132 - .form-group select, 133 - .form-group textarea { 75 + input, textarea, select { 134 76 width: 100%; 135 77 padding: 8px 12px; 136 - border: 1px solid #d1d5db; 137 - border-radius: 6px; 78 + border: 1px solid #ced4da; 79 + border-radius: 4px; 138 80 font-size: 14px; 139 81 } 140 82 141 - .form-group textarea { 83 + textarea { 142 84 resize: vertical; 143 85 min-height: 80px; 144 86 } ··· 146 88 .description-toggle { 147 89 background: none; 148 90 border: none; 149 - color: #2563eb; 91 + color: #007bff; 150 92 cursor: pointer; 151 93 font-size: 14px; 94 + text-decoration: underline; 152 95 margin-bottom: 10px; 153 96 } 154 97 ··· 161 104 } 162 105 163 106 .btn { 164 - padding: 10px 20px; 107 + background: #007bff; 108 + color: white; 165 109 border: none; 166 - border-radius: 6px; 110 + padding: 10px 20px; 111 + border-radius: 4px; 167 112 cursor: pointer; 168 113 font-size: 14px; 169 114 font-weight: 500; 170 - transition: all 0.2s; 171 115 } 172 116 173 - .btn-primary { 174 - background: #2563eb; 175 - color: white; 117 + .btn:hover { 118 + background: #0056b3; 119 + } 120 + 121 + .btn-danger { 122 + background: #dc3545; 123 + } 124 + 125 + .btn-danger:hover { 126 + background: #c82333; 176 127 } 177 128 178 - .btn-primary:hover { 179 - background: #1d4ed8; 129 + .btn-small { 130 + padding: 4px 8px; 131 + font-size: 12px; 180 132 } 181 133 182 - .filters { 134 + .sidebar { 183 135 background: white; 184 136 border-radius: 8px; 185 137 padding: 20px; 186 138 margin-bottom: 20px; 187 - box-shadow: 0 1px 3px rgba(0,0,0,0.1); 139 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 188 140 } 189 141 190 - .filter-row { 142 + .sidebar h3 { 143 + margin-bottom: 15px; 144 + color: #495057; 145 + } 146 + 147 + .project-list { 148 + list-style: none; 149 + } 150 + 151 + .project-item { 191 152 display: flex; 192 - gap: 15px; 193 153 align-items: center; 194 - flex-wrap: wrap; 154 + padding: 8px 0; 155 + cursor: pointer; 156 + border-radius: 4px; 157 + padding-left: 8px; 158 + padding-right: 8px; 195 159 } 196 160 197 - .filter-buttons { 198 - display: flex; 199 - gap: 5px; 161 + .project-item:hover { 162 + background: #f8f9fa; 200 163 } 201 164 202 - .filter-btn { 203 - padding: 6px 12px; 204 - border: 1px solid #d1d5db; 205 - background: white; 206 - border-radius: 6px; 207 - cursor: pointer; 208 - font-size: 14px; 209 - transition: all 0.2s; 165 + .project-item.active { 166 + background: #e3f2fd; 167 + } 168 + 169 + .project-dot { 170 + width: 12px; 171 + height: 12px; 172 + border-radius: 50%; 173 + margin-right: 10px; 210 174 } 211 175 212 - .filter-btn.active { 213 - background: #2563eb; 176 + .project-name { 177 + flex: 1; 178 + } 179 + 180 + .task-count { 181 + background: #6c757d; 214 182 color: white; 215 - border-color: #2563eb; 183 + border-radius: 12px; 184 + padding: 2px 8px; 185 + font-size: 12px; 216 186 } 217 187 218 - .stats { 188 + .filters { 219 189 background: white; 220 190 border-radius: 8px; 221 - padding: 20px; 191 + padding: 15px; 222 192 margin-bottom: 20px; 223 - box-shadow: 0 1px 3px rgba(0,0,0,0.1); 193 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 224 194 } 225 195 226 - .stats-grid { 227 - display: grid; 228 - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); 196 + .filter-row { 197 + display: flex; 229 198 gap: 15px; 199 + align-items: center; 200 + flex-wrap: wrap; 230 201 } 231 202 232 - .stat-item { 233 - text-align: center; 203 + .filter-buttons { 204 + display: flex; 205 + gap: 5px; 234 206 } 235 207 236 - .stat-number { 237 - font-size: 24px; 238 - font-weight: bold; 239 - color: #2563eb; 208 + .filter-btn { 209 + background: #f8f9fa; 210 + border: 1px solid #dee2e6; 211 + padding: 6px 12px; 212 + border-radius: 4px; 213 + cursor: pointer; 214 + font-size: 14px; 240 215 } 241 216 242 - .stat-label { 243 - font-size: 14px; 244 - color: #6b7280; 217 + .filter-btn.active { 218 + background: #007bff; 219 + color: white; 220 + border-color: #007bff; 245 221 } 246 222 247 223 .task-list { 248 224 background: white; 249 225 border-radius: 8px; 250 - box-shadow: 0 1px 3px rgba(0,0,0,0.1); 226 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 251 227 } 252 228 253 229 .task-item { 254 - padding: 15px 20px; 255 - border-bottom: 1px solid #f3f4f6; 230 + display: flex; 231 + align-items: center; 232 + padding: 15px; 233 + border-bottom: 1px solid #dee2e6; 256 234 position: relative; 257 - transition: all 0.2s; 258 235 } 259 236 260 237 .task-item:last-child { 261 238 border-bottom: none; 262 239 } 263 240 264 - .task-item:hover { 265 - background: #f9fafb; 241 + .task-item:hover .delete-btn { 242 + opacity: 1; 266 243 } 267 244 268 245 .task-item.completed { ··· 274 251 } 275 252 276 253 .task-item.overdue { 277 - border-left: 4px solid #dc2626; 278 - background: #fef2f2; 279 - } 280 - 281 - .task-header { 282 - display: flex; 283 - align-items: center; 284 - gap: 12px; 285 - margin-bottom: 8px; 254 + border-left: 4px solid #dc3545; 286 255 } 287 256 288 257 .task-checkbox { 258 + margin-right: 15px; 289 259 width: 18px; 290 260 height: 18px; 291 - cursor: pointer; 261 + } 262 + 263 + .task-content { 264 + flex: 1; 292 265 } 293 266 294 267 .task-title { 295 268 font-weight: 500; 296 - flex: 1; 269 + margin-bottom: 5px; 297 270 cursor: pointer; 298 271 } 299 272 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; 273 + .task-meta { 274 + display: flex; 275 + gap: 10px; 276 + align-items: center; 277 + font-size: 12px; 278 + color: #6c757d; 314 279 } 315 280 316 281 .priority-badge { 317 - padding: 2px 8px; 318 - border-radius: 12px; 319 - font-size: 12px; 282 + padding: 2px 6px; 283 + border-radius: 3px; 284 + font-size: 11px; 320 285 font-weight: 500; 321 286 } 322 287 323 288 .priority-urgent { 324 - background: #fef2f2; 325 - color: #dc2626; 289 + background: #dc3545; 290 + color: white; 326 291 } 327 292 328 293 .priority-high { 329 - background: #fff7ed; 330 - color: #ea580c; 294 + background: #fd7e14; 295 + color: white; 331 296 } 332 297 333 298 .priority-normal { 334 - background: #eff6ff; 335 - color: #2563eb; 299 + background: #007bff; 300 + color: white; 336 301 } 337 302 338 303 .priority-low { 339 - background: #f3f4f6; 340 - color: #6b7280; 304 + background: #6c757d; 305 + color: white; 341 306 } 342 307 343 - .task-meta { 344 - display: flex; 345 - gap: 15px; 346 - align-items: center; 347 - font-size: 14px; 348 - color: #6b7280; 349 - margin-left: 30px; 308 + .overdue-badge { 309 + background: #dc3545; 310 + color: white; 311 + padding: 2px 6px; 312 + border-radius: 3px; 313 + font-size: 11px; 314 + font-weight: 500; 350 315 } 351 316 352 - .task-project { 353 - display: flex; 354 - align-items: center; 355 - gap: 6px; 356 - } 357 - 358 - .task-actions { 317 + .delete-btn { 359 318 position: absolute; 360 - right: 20px; 361 - top: 50%; 362 - transform: translateY(-50%); 363 - display: none; 364 - gap: 8px; 319 + right: 15px; 320 + opacity: 0; 321 + transition: opacity 0.2s; 365 322 } 366 323 367 - .task-item:hover .task-actions { 368 - display: flex; 369 - } 370 - 371 - .action-btn { 372 - padding: 4px 8px; 373 - border: none; 324 + .edit-form { 325 + background: #f8f9fa; 326 + padding: 15px; 374 327 border-radius: 4px; 375 - cursor: pointer; 376 - font-size: 12px; 377 - transition: all 0.2s; 328 + margin-top: 10px; 378 329 } 379 330 380 - .action-btn.edit { 381 - background: #f3f4f6; 382 - color: #374151; 331 + .edit-form .form-row { 332 + margin-bottom: 10px; 383 333 } 384 334 385 - .action-btn.edit:hover { 386 - background: #e5e7eb; 335 + .edit-actions { 336 + display: flex; 337 + gap: 10px; 338 + margin-top: 10px; 387 339 } 388 340 389 - .action-btn.delete { 390 - background: #fef2f2; 391 - color: #dc2626; 341 + .empty-state { 342 + text-align: center; 343 + padding: 40px; 344 + color: #6c757d; 392 345 } 393 346 394 - .action-btn.delete:hover { 395 - background: #fee2e2; 347 + .stats-summary { 348 + background: white; 349 + border-radius: 8px; 350 + padding: 15px; 351 + margin-bottom: 20px; 352 + box-shadow: 0 2px 4px rgba(0,0,0,0.1); 396 353 } 397 354 398 - .overdue-badge { 399 - background: #dc2626; 400 - color: white; 401 - padding: 2px 6px; 402 - border-radius: 10px; 403 - font-size: 11px; 404 - font-weight: 500; 355 + .stats-row { 356 + display: flex; 357 + gap: 20px; 358 + font-size: 14px; 405 359 } 406 360 407 - .empty-state { 408 - text-align: center; 409 - padding: 40px 20px; 410 - color: #6b7280; 361 + .stat-item { 362 + color: #6c757d; 411 363 } 412 364 413 - @media (max-width: 640px) { 414 - .container { 415 - padding: 10px; 416 - } 417 - 418 - .form-row { 419 - flex-direction: column; 420 - } 421 - 422 - .filter-row { 423 - flex-direction: column; 424 - align-items: stretch; 425 - } 426 - 427 - .task-meta { 428 - flex-direction: column; 429 - align-items: flex-start; 430 - gap: 5px; 431 - } 365 + .stat-value { 366 + font-weight: 600; 367 + color: #495057; 432 368 } 433 369 </style> 434 370 </head> ··· 436 372 <div class="container"> 437 373 <div class="header"> 438 374 <h1>Task Manager</h1> 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> 450 - </div> 451 375 </div> 452 376 453 377 <div class="add-task-form"> 454 - <h3>Add New Task</h3> 455 - <form id="addTaskForm"> 378 + <form id="add-task-form"> 456 379 <div class="form-row"> 457 380 <div class="form-group"> 458 - <label for="taskTitle">Title *</label> 459 - <input type="text" id="taskTitle" required> 381 + <label for="task-title">Title</label> 382 + <input type="text" id="task-title" name="title" required> 460 383 </div> 461 384 <div class="form-group"> 462 - <label for="taskPriority">Priority</label> 463 - <select id="taskPriority"> 385 + <label for="task-priority">Priority</label> 386 + <select id="task-priority" name="priority"> 464 387 <option value="normal">Normal</option> 465 388 <option value="low">Low</option> 466 389 <option value="high">High</option> ··· 468 391 </select> 469 392 </div> 470 393 </div> 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 - 483 394 <div class="form-row"> 484 395 <div class="form-group"> 485 - <label for="taskProject">Project</label> 486 - <select id="taskProject"> 396 + <label for="task-project">Project</label> 397 + <select id="task-project" name="project_id"> 487 398 <option value="">Inbox</option> 488 399 </select> 489 400 </div> 490 401 <div class="form-group"> 491 - <label for="taskDueDate">Due Date</label> 492 - <input type="date" id="taskDueDate"> 402 + <label for="task-due-date">Due Date</label> 403 + <input type="date" id="task-due-date" name="due_date"> 493 404 </div> 494 405 </div> 495 - 496 - <button type="submit" class="btn btn-primary">Add Task</button> 406 + <button type="button" class="description-toggle" onclick="toggleDescription()">+ Add Description</button> 407 + <div class="description-section" id="description-section"> 408 + <div class="form-group full-width"> 409 + <label for="task-description">Description</label> 410 + <textarea id="task-description" name="description"></textarea> 411 + </div> 412 + </div> 413 + <button type="submit" class="btn">Add Task</button> 497 414 </form> 498 415 </div> 499 416 417 + <div class="sidebar"> 418 + <h3>Projects</h3> 419 + <ul class="project-list" id="project-list"> 420 + <li class="project-item active" data-project-id="" onclick="selectProject('')"> 421 + <div class="project-dot" style="background: #6c757d;"></div> 422 + <span class="project-name">Inbox</span> 423 + <span class="task-count" id="inbox-count">0</span> 424 + </li> 425 + </ul> 426 + </div> 427 + 500 428 <div class="filters"> 501 429 <div class="filter-row"> 502 430 <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> 431 + <button class="filter-btn active" data-status="all" onclick="setStatusFilter('all')">All</button> 432 + <button class="filter-btn" data-status="active" onclick="setStatusFilter('active')">Active</button> 433 + <button class="filter-btn" data-status="completed" onclick="setStatusFilter('completed')">Completed</button> 506 434 </div> 507 - 508 435 <div class="form-group" style="min-width: 150px;"> 509 - <select id="priorityFilter"> 436 + <select id="priority-filter" onchange="setPriorityFilter(this.value)"> 510 437 <option value="">All Priorities</option> 511 438 <option value="urgent">Urgent</option> 512 439 <option value="high">High</option> ··· 517 444 </div> 518 445 </div> 519 446 520 - <div class="stats" id="statsSection"> 521 - <div class="stats-grid" id="statsGrid"> 522 - <!-- Stats will be loaded here --> 447 + <div class="stats-summary" id="stats-summary"> 448 + <div class="stats-row"> 449 + <div class="stat-item">Total: <span class="stat-value" id="total-tasks">0</span></div> 450 + <div class="stat-item">Active: <span class="stat-value" id="active-tasks">0</span></div> 451 + <div class="stat-item">Completed: <span class="stat-value" id="completed-tasks">0</span></div> 452 + <div class="stat-item">Overdue: <span class="stat-value" id="overdue-tasks">0</span></div> 523 453 </div> 524 454 </div> 525 455 526 - <div class="task-list" id="taskList"> 527 - <!-- Tasks will be loaded here --> 456 + <div class="task-list" id="task-list"> 457 + <div class="empty-state"> 458 + <p>No tasks found. Add your first task above!</p> 459 + </div> 528 460 </div> 529 461 </div> 530 - 462 + 531 463 <script> 532 - let currentProject = 'inbox'; 464 + let currentProject = ''; 533 465 let currentStatus = 'all'; 534 466 let currentPriority = ''; 535 - let projects = []; 536 - let tasks = []; 537 - 467 + let editingTaskId = null; 468 + 538 469 // Initialize the app 539 - document.addEventListener('DOMContentLoaded', () => { 470 + document.addEventListener('DOMContentLoaded', function() { 540 471 loadProjects(); 541 472 loadTasks(); 542 - loadStats(); 543 - setupEventListeners(); 473 + 474 + document.getElementById('add-task-form').addEventListener('submit', handleAddTask); 544 475 }); 545 - 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 - 560 - // Add task form 561 - document.getElementById('addTaskForm').addEventListener('submit', async (e) => { 562 - e.preventDefault(); 563 - await addTask(); 564 - }); 565 - 566 - // Status filters 567 - document.querySelectorAll('[data-status]').forEach(btn => { 568 - btn.addEventListener('click', (e) => { 569 - document.querySelectorAll('[data-status]').forEach(b => b.classList.remove('active')); 570 - e.target.classList.add('active'); 571 - currentStatus = e.target.dataset.status; 572 - loadTasks(); 573 - }); 574 - }); 575 - 576 - // Priority filter 577 - document.getElementById('priorityFilter').addEventListener('change', (e) => { 578 - currentPriority = e.target.value; 579 - loadTasks(); 580 - }); 581 - 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 - }); 476 + 477 + function toggleDescription() { 478 + const section = document.getElementById('description-section'); 479 + const toggle = document.querySelector('.description-toggle'); 480 + 481 + if (section.classList.contains('expanded')) { 482 + section.classList.remove('expanded'); 483 + toggle.textContent = '+ Add Description'; 484 + } else { 485 + section.classList.add('expanded'); 486 + toggle.textContent = '- Hide Description'; 487 + } 592 488 } 593 - 489 + 594 490 async function loadProjects() { 595 491 try { 596 492 const response = await fetch('/projects'); 597 - projects = await response.json(); 598 - renderProjects(); 599 - populateProjectDropdown(); 493 + const projects = await response.json(); 494 + 495 + const projectSelect = document.getElementById('task-project'); 496 + const projectList = document.getElementById('project-list'); 497 + 498 + // Clear existing options (except Inbox) 499 + projectSelect.innerHTML = '<option value="">Inbox</option>'; 500 + 501 + // Keep inbox item, remove others 502 + const inboxItem = projectList.querySelector('[data-project-id=""]'); 503 + projectList.innerHTML = ''; 504 + projectList.appendChild(inboxItem); 505 + 506 + projects.forEach(project => { 507 + // Add to dropdown 508 + const option = document.createElement('option'); 509 + option.value = project.id; 510 + option.textContent = project.name; 511 + projectSelect.appendChild(option); 512 + 513 + // Add to sidebar 514 + const li = document.createElement('li'); 515 + li.className = 'project-item'; 516 + li.setAttribute('data-project-id', project.id); 517 + li.onclick = () => selectProject(project.id); 518 + li.innerHTML = \` 519 + <div class="project-dot" style="background: \${project.color};"></div> 520 + <span class="project-name">\${project.name}</span> 521 + <span class="task-count" id="project-\${project.id}-count">0</span> 522 + \`; 523 + projectList.appendChild(li); 524 + }); 525 + 526 + updateProjectCounts(); 600 527 } catch (error) { 601 528 console.error('Failed to load projects:', error); 602 529 } 603 530 } 604 - 531 + 605 532 async function loadTasks() { 606 533 try { 607 - let url = '/filtering-and-views'; 608 534 const params = new URLSearchParams(); 535 + if (currentProject) params.append('project_id', currentProject); 536 + if (currentStatus === 'active') params.append('completed', '0'); 537 + if (currentStatus === 'completed') params.append('completed', '1'); 538 + if (currentPriority) params.append('priority', currentPriority); 609 539 610 - if (currentProject !== 'inbox') { 611 - params.append('project_id', currentProject); 612 - } else { 613 - params.append('project_id', ''); 614 - } 540 + const response = await fetch('/tasks?' + params.toString()); 541 + const tasks = await response.json(); 615 542 616 - if (currentStatus === 'active') { 617 - params.append('completed', '0'); 618 - } else if (currentStatus === 'completed') { 619 - params.append('completed', '1'); 620 - } 621 - 622 - if (currentPriority) { 623 - params.append('priority', currentPriority); 624 - } 625 - 626 - if (params.toString()) { 627 - url += '?' + params.toString(); 628 - } 629 - 630 - const response = await fetch(url); 631 - tasks = await response.json(); 632 - renderTasks(); 543 + renderTasks(tasks); 544 + updateStats(tasks); 633 545 updateProjectCounts(); 634 546 } catch (error) { 635 547 console.error('Failed to load tasks:', error); 636 548 } 637 549 } 638 - 639 - async function loadStats() { 640 - try { 641 - const response = await fetch('/quick-stats'); 642 - const stats = await response.json(); 643 - renderStats(stats); 644 - } catch (error) { 645 - console.error('Failed to load stats:', error); 646 - } 647 - } 648 - 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() { 682 - const taskList = document.getElementById('taskList'); 550 + 551 + function renderTasks(tasks) { 552 + const taskList = document.getElementById('task-list'); 683 553 684 554 if (tasks.length === 0) { 685 - taskList.innerHTML = '<div class="empty-state">No tasks found</div>'; 555 + taskList.innerHTML = '<div class="empty-state"><p>No tasks found.</p></div>'; 686 556 return; 687 557 } 688 558 559 + const today = new Date().toISOString().split('T')[0]; 560 + 689 561 taskList.innerHTML = tasks.map(task => { 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); 562 + const isOverdue = task.due_date && task.due_date < today && !task.completed; 563 + const priorityClass = \`priority-\${task.priority}\`; 692 564 693 565 return \` 694 566 <div class="task-item \${task.completed ? 'completed' : ''} \${isOverdue ? 'overdue' : ''}" data-task-id="\${task.id}"> 695 - <div class="task-header"> 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>' : ''} 701 - </div> 702 - \${task.description ? \`<div class="task-description" style="margin-left: 30px; color: #6b7280; font-size: 14px;">\${task.description}</div>\` : ''} 703 - <div class="task-meta"> 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>\` : ''} 706 - </div> 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> 567 + <input type="checkbox" class="task-checkbox" \${task.completed ? 'checked' : ''} 568 + onchange="toggleTaskCompletion(\${task.id}, this.checked)"> 569 + <div class="task-content"> 570 + <div class="task-title" onclick="startEditTask(\${task.id})">\${task.title}</div> 571 + <div class="task-meta"> 572 + <span class="priority-badge \${priorityClass}">\${task.priority.toUpperCase()}</span> 573 + \${task.project_name ? \`<span>📁 \${task.project_name}</span>\` : '<span>📥 Inbox</span>'} 574 + \${task.due_date ? \`<span>📅 \${formatDate(task.due_date)}</span>\` : ''} 575 + \${isOverdue ? '<span class="overdue-badge">OVERDUE</span>' : ''} 576 + </div> 577 + \${task.description ? \`<div style="margin-top: 5px; font-size: 14px; color: #6c757d;">\${task.description}</div>\` : ''} 710 578 </div> 579 + <button class="btn btn-danger btn-small delete-btn" onclick="deleteTask(\${task.id})">Delete</button> 711 580 </div> 712 581 \`; 713 582 }).join(''); 714 583 } 715 - 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 - \`; 584 + 585 + function updateStats(tasks) { 586 + const total = tasks.length; 587 + const active = tasks.filter(t => !t.completed).length; 588 + const completed = tasks.filter(t => t.completed).length; 589 + const today = new Date().toISOString().split('T')[0]; 590 + const overdue = tasks.filter(t => t.due_date && t.due_date < today && !t.completed).length; 591 + 592 + document.getElementById('total-tasks').textContent = total; 593 + document.getElementById('active-tasks').textContent = active; 594 + document.getElementById('completed-tasks').textContent = completed; 595 + document.getElementById('overdue-tasks').textContent = overdue; 736 596 } 737 - 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; 597 + 598 + async function updateProjectCounts() { 599 + try { 600 + const response = await fetch('/tasks'); 601 + const allTasks = await response.json(); 602 + 603 + // Count inbox tasks (no project) 604 + const inboxCount = allTasks.filter(t => !t.project_id && !t.completed).length; 605 + document.getElementById('inbox-count').textContent = inboxCount; 606 + 607 + // Count tasks per project 608 + const projectCounts = {}; 609 + allTasks.forEach(task => { 610 + if (task.project_id && !task.completed) { 611 + projectCounts[task.project_id] = (projectCounts[task.project_id] || 0) + 1; 612 + } 613 + }); 614 + 615 + Object.entries(projectCounts).forEach(([projectId, count]) => { 616 + const countEl = document.getElementById(\`project-\${projectId}-count\`); 617 + if (countEl) countEl.textContent = count; 618 + }); 619 + 620 + // Reset counts for projects with no active tasks 621 + document.querySelectorAll('[id^="project-"][id$="-count"]').forEach(el => { 622 + const projectId = el.id.match(/project-(\\d+)-count/)?.[1]; 623 + if (projectId && !projectCounts[projectId]) { 624 + el.textContent = '0'; 625 + } 626 + }); 627 + } catch (error) { 628 + console.error('Failed to update project counts:', error); 629 + } 630 + } 631 + 632 + function selectProject(projectId) { 633 + currentProject = projectId; 742 634 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 - } 635 + // Update active state 636 + document.querySelectorAll('.project-item').forEach(item => { 637 + item.classList.remove('active'); 750 638 }); 639 + document.querySelector(\`[data-project-id="\${projectId}"]\`).classList.add('active'); 640 + 641 + loadTasks(); 751 642 } 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; 643 + 644 + function setStatusFilter(status) { 645 + currentStatus = status; 759 646 760 - if (!title) return; 647 + // Update active state 648 + document.querySelectorAll('.filter-btn').forEach(btn => { 649 + btn.classList.remove('active'); 650 + }); 651 + document.querySelector(\`[data-status="\${status}"]\`).classList.add('active'); 761 652 653 + loadTasks(); 654 + } 655 + 656 + function setPriorityFilter(priority) { 657 + currentPriority = priority; 658 + loadTasks(); 659 + } 660 + 661 + async function handleAddTask(e) { 662 + e.preventDefault(); 663 + 664 + const formData = new FormData(e.target); 762 665 const taskData = { 763 - title, 764 - priority, 765 - ...(description && { description }), 766 - ...(projectId && { project_id: parseInt(projectId) }), 767 - ...(dueDate && { due_date: dueDate }) 666 + title: formData.get('title'), 667 + description: formData.get('description') || '', 668 + priority: formData.get('priority'), 669 + project_id: formData.get('project_id') ? parseInt(formData.get('project_id')) : null, 670 + due_date: formData.get('due_date') || null 768 671 }; 769 672 770 673 try { ··· 775 678 }); 776 679 777 680 if (response.ok) { 778 - document.getElementById('addTaskForm').reset(); 779 - document.getElementById('descriptionSection').classList.remove('expanded'); 780 - document.getElementById('descriptionToggle').textContent = '+ Add Description'; 681 + e.target.reset(); 682 + document.getElementById('description-section').classList.remove('expanded'); 683 + document.querySelector('.description-toggle').textContent = '+ Add Description'; 781 684 loadTasks(); 782 - loadStats(); 685 + } else { 686 + const error = await response.json(); 687 + alert('Failed to create task: ' + error.error); 783 688 } 784 689 } catch (error) { 785 - console.error('Failed to add task:', error); 690 + console.error('Failed to create task:', error); 691 + alert('Failed to create task'); 786 692 } 787 693 } 788 - 789 - async function toggleTask(taskId) { 790 - const task = tasks.find(t => t.id === taskId); 791 - if (!task) return; 792 - 694 + 695 + async function toggleTaskCompletion(taskId, completed) { 793 696 try { 794 - await fetch(\`/tasks/\${taskId}\`, { 697 + const response = await fetch(\`/tasks/\${taskId}\`, { 795 698 method: 'PATCH', 796 699 headers: { 'Content-Type': 'application/json' }, 797 - body: JSON.stringify({ completed: task.completed ? 0 : 1 }) 700 + body: JSON.stringify({ completed: completed ? 1 : 0 }) 798 701 }); 799 - loadTasks(); 800 - loadStats(); 702 + 703 + if (response.ok) { 704 + loadTasks(); 705 + } else { 706 + alert('Failed to update task'); 707 + } 801 708 } catch (error) { 802 - console.error('Failed to toggle task:', error); 709 + console.error('Failed to update task:', error); 710 + alert('Failed to update task'); 803 711 } 804 712 } 805 - 713 + 806 714 async function deleteTask(taskId) { 807 715 if (!confirm('Are you sure you want to delete this task?')) return; 808 716 809 717 try { 810 - await fetch(\`/tasks/\${taskId}\`, { method: 'DELETE' }); 811 - loadTasks(); 812 - loadStats(); 718 + const response = await fetch(\`/tasks/\${taskId}\`, { 719 + method: 'DELETE' 720 + }); 721 + 722 + if (response.ok) { 723 + loadTasks(); 724 + } else { 725 + alert('Failed to delete task'); 726 + } 813 727 } catch (error) { 814 728 console.error('Failed to delete task:', error); 729 + alert('Failed to delete task'); 815 730 } 816 731 } 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'); 732 + 733 + function startEditTask(taskId) { 734 + if (editingTaskId === taskId) return; 822 735 823 - titleSpan.classList.add('editing'); 824 - titleInput.classList.add('editing'); 825 - titleInput.focus(); 826 - titleInput.select(); 827 - } 828 - 829 - async function saveTaskTitle(taskId) { 736 + // Cancel any existing edit 737 + if (editingTaskId) cancelEdit(); 738 + 739 + editingTaskId = taskId; 830 740 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(); 741 + const taskContent = taskItem.querySelector('.task-content'); 834 742 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 - } 743 + // Get current task data 744 + fetch(\`/tasks/\${taskId}\`) 745 + .then(response => response.json()) 746 + .then(task => { 747 + taskContent.innerHTML = \` 748 + <div class="edit-form"> 749 + <div class="form-row"> 750 + <div class="form-group"> 751 + <input type="text" id="edit-title" value="\${task.title}" placeholder="Task title"> 752 + </div> 753 + </div> 754 + <div class="form-row"> 755 + <div class="form-group"> 756 + <select id="edit-priority"> 757 + <option value="low" \${task.priority === 'low' ? 'selected' : ''}>Low</option> 758 + <option value="normal" \${task.priority === 'normal' ? 'selected' : ''}>Normal</option> 759 + <option value="high" \${task.priority === 'high' ? 'selected' : ''}>High</option> 760 + <option value="urgent" \${task.priority === 'urgent' ? 'selected' : ''}>Urgent</option> 761 + </select> 762 + </div> 763 + <div class="form-group"> 764 + <input type="date" id="edit-due-date" value="\${task.due_date || ''}"> 765 + </div> 766 + </div> 767 + <div class="form-row"> 768 + <div class="form-group full-width"> 769 + <textarea id="edit-description" placeholder="Description">\${task.description || ''}</textarea> 770 + </div> 771 + </div> 772 + <div class="edit-actions"> 773 + <button class="btn btn-small" onclick="saveEdit(\${taskId})">Save</button> 774 + <button class="btn btn-small" onclick="cancelEdit()" style="background: #6c757d;">Cancel</button> 775 + </div> 776 + </div> 777 + \`; 778 + }); 863 779 } 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 || ''; 780 + 781 + async function saveEdit(taskId) { 782 + const title = document.getElementById('edit-title').value; 783 + const priority = document.getElementById('edit-priority').value; 784 + const dueDate = document.getElementById('edit-due-date').value; 785 + const description = document.getElementById('edit-description').value; 875 786 876 - if (task.description) { 877 - document.getElementById('descriptionSection').classList.add('expanded'); 878 - document.getElementById('descriptionToggle').textContent = '- Hide Description'; 787 + if (!title.trim()) { 788 + alert('Title is required'); 789 + return; 879 790 } 880 791 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 792 try { 913 793 const response = await fetch(\`/tasks/\${taskId}\`, { 914 794 method: 'PATCH', 915 795 headers: { 'Content-Type': 'application/json' }, 916 - body: JSON.stringify(taskData) 796 + body: JSON.stringify({ 797 + title: title.trim(), 798 + priority, 799 + due_date: dueDate || null, 800 + description: description.trim() 801 + }) 917 802 }); 918 803 919 804 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 - 805 + editingTaskId = null; 934 806 loadTasks(); 935 - loadStats(); 807 + } else { 808 + const error = await response.json(); 809 + alert('Failed to update task: ' + error.error); 936 810 } 937 811 } catch (error) { 938 812 console.error('Failed to update task:', error); 813 + alert('Failed to update task'); 939 814 } 815 + } 816 + 817 + function cancelEdit() { 818 + editingTaskId = null; 819 + loadTasks(); 820 + } 821 + 822 + function formatDate(dateStr) { 823 + const date = new Date(dateStr); 824 + return date.toLocaleDateString(); 940 825 } 941 826 </script> 942 827 </body>
-8
examples/todo-app/src/server.ts
··· 3 3 import { runMigrations } from './db.js'; 4 4 5 5 // Generated route modules 6 - import data_integrity from './generated/todos/data-integrity.js'; 7 - import filtering_and_views from './generated/todos/filtering-and-views.js'; 8 - import integration from './generated/todos/integration.js'; 9 6 import projects from './generated/todos/projects.js'; 10 - import quick_stats from './generated/todos/quick-stats.js'; 11 7 import tasks from './generated/todos/tasks.js'; 12 8 import web_experience from './generated/todos/web-experience.js'; 13 9 14 10 // Mount routes 15 - mount('/data-integrity', data_integrity); 16 - mount('/filtering-and-views', filtering_and_views); 17 - mount('/integration', integration); 18 11 mount('/projects', projects); 19 - mount('/quick-stats', quick_stats); 20 12 mount('/tasks', tasks); 21 13 mount('', web_experience); 22 14
+7
src/architectures/sqlite-web-api.ts
··· 56 56 57 57 app.get('/health', (c) => c.json({ status: 'ok', uptime: process.uptime() })); 58 58 59 + // Error handler — log details and return JSON 60 + app.onError((err, c) => { 61 + console.error('Unhandled error:', err.message, err.stack); 62 + return c.json({ error: err.message }, 500); 63 + }); 64 + 59 65 /** 60 66 * Mount a route module. Call this for each generated module. 61 67 */ ··· 121 127 - Include ALL CRUD routes for the resource in a single module (GET list, GET by id, POST create, PATCH update, DELETE). 122 128 - Use better-sqlite3 synchronous API: db.prepare(sql).run(), .get(), .all() 123 129 - Use parameterized queries ALWAYS — never interpolate user input into SQL. 130 + - In SQL strings, ALWAYS use single quotes for string literals: date('now'), datetime('now'). NEVER use double quotes — SQLite treats double quotes as column identifiers. 124 131 - Parse request bodies with \`await c.req.json()\` and validate with Zod .safeParse(). 125 132 126 133 ### Response conventions