diff --git a/.cw-preview.yml b/.cw-preview.yml new file mode 100644 index 0000000..3536fe3 --- /dev/null +++ b/.cw-preview.yml @@ -0,0 +1,11 @@ +version: 1 +services: + app: + build: . + port: 3000 + healthcheck: + path: /health + interval: 3s + retries: 30 + seed: + - sh /app/scripts/seed-preview.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..23ef034 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules/ +.cw/ +workdir/ +.git/ +apps/server/dist/ +apps/web/dist/ +coverage/ +*.log +.env* +.DS_Store +.screenshots/ +.cw-previews/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..46bc2b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +# Stage 1: Install dependencies (native addons need build tools) +FROM node:20-alpine AS deps +RUN apk add --no-cache python3 make g++ +WORKDIR /app +COPY package.json package-lock.json ./ +COPY apps/web/package.json apps/web/package.json +COPY packages/shared/package.json packages/shared/package.json +RUN npm ci + +# Stage 2: Build everything (web needs server types via packages/shared) +FROM deps AS build +COPY tsconfig.json ./ +COPY apps/server/ apps/server/ +COPY apps/web/ apps/web/ +COPY packages/shared/ packages/shared/ +# Server build (tsc → apps/server/dist/) +RUN npm run build +# Web build (vite bundle → apps/web/dist/) +RUN npx --workspace=apps/web vite build + +# Stage 3: Production image +FROM node:20-alpine AS prod +RUN apk add --no-cache git curl jq +COPY --from=caddy:2-alpine /usr/bin/caddy /usr/bin/caddy + +WORKDIR /app + +# Node modules (includes native better-sqlite3) +COPY --from=deps /app/node_modules/ node_modules/ +COPY package.json ./ + +# Server build output + drizzle migrations +COPY --from=build /app/apps/server/dist/ apps/server/dist/ +COPY apps/server/drizzle/ apps/server/drizzle/ + +# Web build output +COPY --from=build /app/apps/web/dist/ apps/web/dist/ + +# Scripts and config +COPY scripts/ scripts/ +COPY scripts/Caddyfile /etc/caddy/Caddyfile +RUN chmod +x scripts/*.sh + +# Workspace volume +RUN mkdir -p /workspace + +EXPOSE 3000 +ENTRYPOINT ["sh", "/app/scripts/entrypoint.sh"] diff --git a/apps/server/preview-seed.ts b/apps/server/preview-seed.ts new file mode 100644 index 0000000..b5b4307 --- /dev/null +++ b/apps/server/preview-seed.ts @@ -0,0 +1,482 @@ +/** + * Preview Seed Script + * + * Populates the database with demo data for preview deployments. + * Run with: CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js + * + * Prints the project ID to stdout so the calling shell script + * can derive the clone path. + */ + +import { createDatabase, ensureSchema } from './db/index.js'; +import { createRepositories } from './container.js'; + +const dbPath = process.env.CW_DB_PATH; +if (!dbPath) { + console.error('CW_DB_PATH environment variable is required'); + process.exit(1); +} + +const db = createDatabase(dbPath); +ensureSchema(db); +const repos = createRepositories(db); + +// ─── Project ─── + +const project = await repos.projectRepository.create({ + name: 'demo-app', + url: 'file:///workspace/demo-repo.git', + defaultBranch: 'main', +}); + +// ─── Initiative ─── + +const initiative = await repos.initiativeRepository.create({ + name: 'Task Manager Redesign', + status: 'active', + branch: 'cw/task-manager-redesign', + executionMode: 'review_per_phase', +}); + +await repos.projectRepository.setInitiativeProjects(initiative.id, [project.id]); + +// ─── Phases ─── + +const phase1 = await repos.phaseRepository.create({ + initiativeId: initiative.id, + name: 'Backend API', + status: 'completed', +}); + +const phase2 = await repos.phaseRepository.create({ + initiativeId: initiative.id, + name: 'UI Overhaul', + status: 'pending_review', +}); + +const phase3 = await repos.phaseRepository.create({ + initiativeId: initiative.id, + name: 'Testing & Polish', + status: 'in_progress', +}); + +// ─── Tasks ─── + +// Phase 1: Backend API (all completed) +const task1a = await repos.taskRepository.create({ + phaseId: phase1.id, + initiativeId: initiative.id, + name: 'Research REST vs GraphQL patterns', + category: 'research', + status: 'completed', + description: 'Evaluate REST and GraphQL approaches for the task management API. Consider developer experience, caching, and real-time requirements.', + order: 0, + summary: 'Recommended REST with WebSocket subscriptions for real-time. GraphQL adds complexity without clear benefit for this domain.', +}); + +const task1b = await repos.taskRepository.create({ + phaseId: phase1.id, + initiativeId: initiative.id, + name: 'Implement task CRUD endpoints', + category: 'execute', + status: 'completed', + description: 'Build the core CRUD API for tasks with proper validation and error handling.', + order: 1, + summary: 'Implemented GET/POST/PUT/DELETE /api/tasks with Zod validation, proper HTTP status codes, and pagination support.', +}); + +const task1c = await repos.taskRepository.create({ + phaseId: phase1.id, + initiativeId: initiative.id, + name: 'Add input validation middleware', + category: 'execute', + status: 'completed', + description: 'Create reusable validation middleware using Zod schemas for all API endpoints.', + order: 2, + summary: 'Added validateBody() and validateQuery() middleware with typed error responses.', +}); + +// Phase 2: UI Overhaul (all completed) +const task2a = await repos.taskRepository.create({ + phaseId: phase2.id, + initiativeId: initiative.id, + name: 'Design component hierarchy', + category: 'plan', + status: 'completed', + description: 'Plan the component architecture for the redesigned UI, focusing on reusability and maintainability.', + order: 0, + summary: 'Designed 3-level hierarchy: Layout (Header, Sidebar) → Views (TaskList, TaskDetail) → Primitives (StatusBadge, PriorityTag).', +}); + +const task2b = await repos.taskRepository.create({ + phaseId: phase2.id, + initiativeId: initiative.id, + name: 'Implement responsive header', + category: 'execute', + status: 'completed', + description: 'Build the responsive header component with search functionality and notification bell.', + order: 1, + summary: 'Created Header.tsx with search input, notification dropdown (3 mock items), and user avatar. Fully responsive with mobile hamburger menu.', +}); + +const task2c = await repos.taskRepository.create({ + phaseId: phase2.id, + initiativeId: initiative.id, + name: 'Refactor task list with filters', + category: 'execute', + status: 'completed', + description: 'Rewrite the task list component with status icons, priority badges, and filter controls.', + order: 2, + summary: 'Refactored TaskList.tsx with table layout, status icons, priority badges, assignee avatars, and due dates. Added TaskFilters.tsx with status/priority filter buttons.', +}); + +// Phase 3: Testing & Polish (mixed statuses) +const task3a = await repos.taskRepository.create({ + phaseId: phase3.id, + initiativeId: initiative.id, + name: 'Plan test strategy', + category: 'plan', + status: 'completed', + description: 'Define the testing strategy: unit tests, integration tests, and E2E test coverage targets.', + order: 0, + summary: 'Defined 3-tier strategy: Vitest unit tests (80% coverage), API integration tests with supertest, and Playwright E2E for critical flows.', +}); + +const task3b = await repos.taskRepository.create({ + phaseId: phase3.id, + initiativeId: initiative.id, + name: 'Write integration tests', + category: 'execute', + status: 'in_progress', + description: 'Write integration tests for all API endpoints using supertest.', + order: 1, +}); + +const task3c = await repos.taskRepository.create({ + phaseId: phase3.id, + initiativeId: initiative.id, + name: 'Add error handling', + category: 'execute', + status: 'pending', + description: 'Add global error boundary, API error toasts, and retry logic for failed requests.', + order: 2, +}); + +// ─── Agents ─── + +const agent1 = await repos.agentRepository.create({ + name: 'keen-falcon', + worktreeId: 'worktrees/keen-falcon', + taskId: task1b.id, + initiativeId: initiative.id, + status: 'stopped', + mode: 'execute', + provider: 'claude', +}); + +const agent2 = await repos.agentRepository.create({ + name: 'swift-otter', + worktreeId: 'worktrees/swift-otter', + taskId: task2c.id, + initiativeId: initiative.id, + status: 'stopped', + mode: 'execute', + provider: 'claude', +}); + +const agent3 = await repos.agentRepository.create({ + name: 'bright-heron', + worktreeId: 'worktrees/bright-heron', + taskId: task3b.id, + initiativeId: initiative.id, + status: 'running', + mode: 'execute', + provider: 'claude', +}); + +// ─── Agent Log Chunks ─── + +// Agent 1: keen-falcon (completed backend work) +await repos.logChunkRepository.insertChunk({ + agentId: agent1.id, + agentName: 'keen-falcon', + sessionNumber: 1, + content: [ + JSON.stringify({ type: 'system', session_id: 'session-kf-001' }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "I'll start by reading the existing API structure to understand the current patterns." }, + { type: 'tool_use', id: 'toolu_kf1', name: 'Read', input: { file_path: '/app/src/lib/api.ts' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" }] }, + tool_use_result: { stdout: "const API_URL = '/api';\n\nexport async function fetchTasks(filter: string) {\n const res = await fetch(`${API_URL}/tasks?filter=${filter}`);\n return res.json();\n}" }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Now I'll implement the task CRUD endpoints with proper validation. I'll create a structured API client with error handling." }, + { type: 'tool_use', id: 'toolu_kf2', name: 'Write', input: { file_path: '/app/src/server/routes/tasks.ts', content: '// CRUD endpoints implementation...' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File written successfully' }] }, + tool_use_result: { stdout: 'File written successfully' }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Let me also add the Zod validation schemas and middleware." }, + { type: 'tool_use', id: 'toolu_kf3', name: 'Write', input: { file_path: '/app/src/server/middleware/validate.ts', content: '// Validation middleware...' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File written successfully' }] }, + tool_use_result: { stdout: 'File written successfully' }, + }), + JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.0847, duration_ms: 32000 }), + ].join('\n'), +}); + +// Agent 2: swift-otter (completed UI overhaul) +await repos.logChunkRepository.insertChunk({ + agentId: agent2.id, + agentName: 'swift-otter', + sessionNumber: 1, + content: [ + JSON.stringify({ type: 'system', session_id: 'session-so-001' }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "I'll start by reading the current TaskList component to understand what needs to be refactored." }, + { type: 'tool_use', id: 'toolu_so1', name: 'Read', input: { file_path: '/app/src/components/TaskList.tsx' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { + content: [{ + type: 'tool_result', + content: "import { useTasks } from '../hooks/useTasks';\n\nexport function TaskList({ filter }) {\n const { tasks, loading } = useTasks(filter);\n if (loading) return
Loading...
;\n return (\n \n );\n}", + }], + }, + tool_use_result: { stdout: "import { useTasks } from '../hooks/useTasks';\n..." }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "The current TaskList is very basic — just a plain unordered list with no visual hierarchy. I'll refactor it into a proper table with status icons, priority badges, assignee avatars, and a loading skeleton." }, + { + type: 'tool_use', id: 'toolu_so2', name: 'Edit', + input: { + file_path: '/app/src/components/TaskList.tsx', + old_string: "import { useTasks } from '../hooks/useTasks';", + new_string: "import { useTasks } from '../hooks/useTasks';\n\ninterface Task {\n id: string;\n title: string;\n status: 'active' | 'completed' | 'blocked';\n priority: 'high' | 'medium' | 'low';\n assignee?: string;\n}", + }, + }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] }, + tool_use_result: { stdout: 'File edited successfully' }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Now I'll create the TaskFilters component for status and priority filtering." }, + { type: 'tool_use', id: 'toolu_so3', name: 'Write', input: { file_path: '/app/src/components/TaskFilters.tsx', content: '// TaskFilters component...' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File written successfully' }] }, + tool_use_result: { stdout: 'File written successfully' }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Finally, let me refactor the API client to be type-safe with proper error handling." }, + { type: 'tool_use', id: 'toolu_so4', name: 'Edit', input: { file_path: '/app/src/lib/api.ts', old_string: "const API_URL = '/api';", new_string: 'class ApiClient {\n private baseUrl: string;\n constructor(baseUrl = \'/api\') { this.baseUrl = baseUrl; }' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File edited successfully' }] }, + tool_use_result: { stdout: 'File edited successfully' }, + }), + JSON.stringify({ type: 'result', is_error: false, total_cost_usd: 0.1234, duration_ms: 45000 }), + ].join('\n'), +}); + +// Agent 3: bright-heron (in-progress, no result line) +await repos.logChunkRepository.insertChunk({ + agentId: agent3.id, + agentName: 'bright-heron', + sessionNumber: 1, + content: [ + JSON.stringify({ type: 'system', session_id: 'session-bh-001' }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "I'll start writing integration tests for the task CRUD endpoints. Let me first check the existing test setup." }, + { type: 'tool_use', id: 'toolu_bh1', name: 'Bash', input: { command: 'ls src/test/', description: 'Check existing test files' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'setup.ts\nfixtures/' }] }, + tool_use_result: { stdout: 'setup.ts\nfixtures/' }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Good, there's a test setup file and fixtures directory. Let me read the setup to understand the test configuration." }, + { type: 'tool_use', id: 'toolu_bh2', name: 'Read', input: { file_path: '/app/src/test/setup.ts' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: "import { beforeAll, afterAll } from 'vitest';\nimport { createTestServer } from './fixtures/server';\n\nlet server;\nbeforeAll(async () => { server = await createTestServer(); });\nafterAll(() => server?.close());" }] }, + tool_use_result: { stdout: "import { beforeAll, afterAll } from 'vitest';\n..." }, + }), + JSON.stringify({ + type: 'assistant', + message: { + content: [ + { type: 'text', text: "Now I'll write the integration tests for the tasks API. Starting with the CRUD operations..." }, + { type: 'tool_use', id: 'toolu_bh3', name: 'Write', input: { file_path: '/app/src/test/tasks.test.ts', content: '// Integration tests...' } }, + ], + }, + }), + JSON.stringify({ + type: 'user', + message: { content: [{ type: 'tool_result', content: 'File written successfully' }] }, + tool_use_result: { stdout: 'File written successfully' }, + }), + ].join('\n'), +}); + +// ─── Root Page ─── + +const pageContent = { + type: 'doc', + content: [ + { + type: 'heading', + attrs: { level: 1 }, + content: [{ type: 'text', text: 'Task Manager Redesign' }], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'A comprehensive redesign of the task management application, focusing on improved user experience, type safety, and modern UI patterns.', + }, + ], + }, + { + type: 'heading', + attrs: { level: 2 }, + content: [{ type: 'text', text: 'Goals' }], + }, + { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Responsive layout with mobile-first design' }] }], + }, + { + type: 'listItem', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Type-safe API client with proper error handling' }] }], + }, + { + type: 'listItem', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Status icons and priority badges for visual clarity' }] }], + }, + { + type: 'listItem', + content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Comprehensive test coverage (unit, integration, E2E)' }] }], + }, + ], + }, + { + type: 'heading', + attrs: { level: 2 }, + content: [{ type: 'text', text: 'Progress' }], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Backend API is complete with full CRUD endpoints and validation middleware. The UI overhaul phase is in review — header, task list, and filters have been refactored. Testing phase is actively in progress with integration tests being written.', + }, + ], + }, + ], +}; + +await repos.pageRepository.create({ + initiativeId: initiative.id, + title: 'Task Manager Redesign', + content: JSON.stringify(pageContent), + sortOrder: 0, +}); + +// ─── Review Comments (on Phase 2) ─── + +await repos.reviewCommentRepository.create({ + phaseId: phase2.id, + filePath: 'src/components/TaskList.tsx', + lineNumber: 12, + lineType: 'added', + body: 'Should we memoize the STATUS_ICONS and PRIORITY_COLORS objects since they are static? Moving them outside the component is good, but wrapping the component in React.memo might help with re-renders.', + author: 'you', +}); + +await repos.reviewCommentRepository.create({ + phaseId: phase2.id, + filePath: 'src/components/Header.tsx', + lineNumber: 8, + lineType: 'added', + body: 'The notifications are hardcoded. For the demo this is fine, but we should add a TODO to wire this up to a real notification system.', + author: 'you', +}); + +await repos.reviewCommentRepository.create({ + phaseId: phase2.id, + filePath: 'src/lib/api.ts', + lineNumber: 25, + lineType: 'added', + body: 'Nice error handling pattern. Could we also add request timeout support? Long-running requests should fail gracefully.', + author: 'you', +}); + +// Print project ID for the shell script +process.stdout.write(project.id); diff --git a/docs/preview.md b/docs/preview.md index 66e9d03..c1482d7 100644 --- a/docs/preview.md +++ b/docs/preview.md @@ -358,6 +358,47 @@ Polls `getPreviewStatus` with `refetchInterval: 3000` while active. - `GracefulShutdown` calls `previewManager.stopAll()` during shutdown - `requirePreviewManager(ctx)` helper in `apps/server/trpc/routers/_helpers.ts` +## Codewalkers Self-Preview + +Codewalkers itself has a Dockerfile, `.cw-preview.yml`, and seed script for preview deployments. This lets you demo the full app — initiatives, phases, tasks, agents with output, pages, and review tab with real git diffs. + +### Files + +| File | Purpose | +|------|---------| +| `Dockerfile` | Multi-stage: deps → build (server+web) → production (Node + Caddy) | +| `.cw-preview.yml` | Preview config: build, healthcheck, seed | +| `scripts/Caddyfile` | Caddy config: SPA file server + tRPC reverse proxy | +| `scripts/entrypoint.sh` | Init workspace, start backend, run Caddy | +| `scripts/seed-preview.sh` | Create demo git repo + run Node.js seed | +| `apps/server/preview-seed.ts` | Populate DB with demo initiative, phases, tasks, agents, log chunks, pages, review comments | + +### Container Architecture + +Single container running two processes: +- **Node.js backend** on port 3847 (tRPC API + health endpoint) +- **Caddy** on port 3000 (SPA file server, reverse proxy `/trpc` and `/health` to backend) + +### Seed Data + +The seed creates a "Task Manager Redesign" initiative with: +- 3 phases (completed, pending_review, in_progress) +- 9 tasks across phases +- 3 agents with JSONL log output +- Root page with Tiptap content +- 3 review comments on the pending_review phase +- Git repo with 3 branches and real commit diffs for the review tab + +### Manual Testing + +```sh +docker build -t cw-preview . +docker run -p 3000:3000 cw-preview +# Wait for startup, then seed: +docker exec sh /app/scripts/seed-preview.sh +# Browse http://localhost:3000 +``` + ## Dependencies - `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml` diff --git a/scripts/Caddyfile b/scripts/Caddyfile new file mode 100644 index 0000000..6d5222e --- /dev/null +++ b/scripts/Caddyfile @@ -0,0 +1,15 @@ +:3000 { + handle /trpc/* { + reverse_proxy localhost:3847 + } + + handle /health { + reverse_proxy localhost:3847 + } + + handle { + root * /app/apps/web/dist + try_files {path} /index.html + file_server + } +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100755 index 0000000..81134be --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/sh +set -e + +# 1. Initialize workspace +mkdir -p /workspace/.cw +echo '{"version":1}' > /workspace/.cwrc + +# 2. Git globals +git config --global user.email "preview@codewalkers.dev" +git config --global user.name "Codewalkers Preview" +git config --global protocol.file.allow always +git config --global init.defaultBranch main + +# 3. Start backend +cd /workspace +CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/bin/cw.js --server & +BACKEND_PID=$! + +# 4. Wait for health +echo "Waiting for backend to start..." +TRIES=0 +MAX_TRIES=60 +until curl -sf http://localhost:3847/health > /dev/null 2>&1; do + TRIES=$((TRIES + 1)) + if [ "$TRIES" -ge "$MAX_TRIES" ]; then + echo "Backend failed to start after ${MAX_TRIES}s" + exit 1 + fi + sleep 1 +done +echo "Backend is healthy" + +# 5. Run Caddy in foreground (receives signals) +exec caddy run --config /etc/caddy/Caddyfile diff --git a/scripts/seed-preview.sh b/scripts/seed-preview.sh new file mode 100755 index 0000000..5dfd305 --- /dev/null +++ b/scripts/seed-preview.sh @@ -0,0 +1,577 @@ +#!/bin/sh +set -e + +echo "=== Creating demo git repository ===" + +BARE_REPO="/workspace/demo-repo.git" +TEMP_DIR=$(mktemp -d) + +# Initialize bare repo +git init --bare "$BARE_REPO" + +cd "$TEMP_DIR" +git init -b main +git remote add origin "$BARE_REPO" + +# ─── main branch: initial task manager app ─── + +cat > README.md << 'READMEEOF' +# Task Manager + +A modern task management application built with React and TypeScript. + +## Features + +- Create, update, and delete tasks +- Filter by status and priority +- Real-time updates +- Responsive design + +## Getting Started + +```bash +npm install +npm run dev +``` +READMEEOF + +cat > package.json << 'PKGEOF' +{ + "name": "task-manager", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} +PKGEOF + +mkdir -p src/components src/hooks src/lib + +cat > src/app.tsx << 'APPEOF' +import { useState } from 'react'; +import { TaskList } from './components/TaskList'; +import { Sidebar } from './components/Sidebar'; + +export function App() { + const [filter, setFilter] = useState('all'); + + return ( +
+ +
+

Task Manager

+ +
+
+ ); +} +APPEOF + +cat > src/components/TaskList.tsx << 'TLEOF' +import { useTasks } from '../hooks/useTasks'; + +interface TaskListProps { + filter: string; +} + +export function TaskList({ filter }: TaskListProps) { + const { tasks, loading } = useTasks(filter); + + if (loading) return
Loading...
; + + return ( + + ); +} +TLEOF + +cat > src/components/Sidebar.tsx << 'SBEOF' +interface SidebarProps { + filter: string; + onFilterChange: (filter: string) => void; +} + +export function Sidebar({ filter, onFilterChange }: SidebarProps) { + const filters = ['all', 'active', 'completed']; + + return ( + + ); +} +SBEOF + +cat > src/hooks/useTasks.ts << 'HTEOF' +import { useState, useEffect } from 'react'; +import { fetchTasks } from '../lib/api'; + +export function useTasks(filter: string) { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchTasks(filter).then(data => { + setTasks(data); + setLoading(false); + }); + }, [filter]); + + return { tasks, loading }; +} +HTEOF + +cat > src/lib/api.ts << 'APIEOF' +const API_URL = '/api'; + +export async function fetchTasks(filter: string) { + const res = await fetch(`${API_URL}/tasks?filter=${filter}`); + return res.json(); +} + +export async function createTask(title: string) { + const res = await fetch(`${API_URL}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }), + }); + return res.json(); +} +APIEOF + +git add -A +git commit -m "feat: initial task manager setup" +git push origin main + +# ─── initiative branch (same as main, no extra commits) ─── + +git checkout -b cw/task-manager-redesign +git push origin cw/task-manager-redesign + +# ─── phase branch: UI overhaul with 3 commits ─── + +git checkout -b cw/task-manager-redesign-phase-ui-overhaul + +# Commit 1: responsive header with search and notifications +mkdir -p src/components + +cat > src/components/Header.tsx << 'HDREOF' +import { useState } from 'react'; + +interface HeaderProps { + onSearch: (query: string) => void; +} + +export function Header({ onSearch }: HeaderProps) { + const [query, setQuery] = useState(''); + const [notifications] = useState([ + { id: 1, text: 'Task "Deploy v2" completed', unread: true }, + { id: 2, text: 'New comment on "API redesign"', unread: true }, + { id: 3, text: 'Sprint review tomorrow', unread: false }, + ]); + + const unreadCount = notifications.filter(n => n.unread).length; + + return ( +
+
+

TaskFlow

+
+
+ { + setQuery(e.target.value); + onSearch(e.target.value); + }} + /> +
+
+ +
JD
+
+
+ ); +} +HDREOF + +cat > src/components/TaskFilters.tsx << 'TFEOF' +interface TaskFiltersProps { + activeFilter: string; + onFilterChange: (filter: string) => void; + activePriority: string; + onPriorityChange: (priority: string) => void; +} + +export function TaskFilters({ + activeFilter, + onFilterChange, + activePriority, + onPriorityChange, +}: TaskFiltersProps) { + const statuses = ['all', 'active', 'completed', 'blocked']; + const priorities = ['all', 'high', 'medium', 'low']; + + return ( +
+
+ +
+ {statuses.map(s => ( + + ))} +
+
+
+ +
+ {priorities.map(p => ( + + ))} +
+
+
+ ); +} +TFEOF + +# Update app.tsx to use Header +cat > src/app.tsx << 'APPEOF2' +import { useState } from 'react'; +import { Header } from './components/Header'; +import { TaskList } from './components/TaskList'; +import { TaskFilters } from './components/TaskFilters'; +import { Sidebar } from './components/Sidebar'; + +export function App() { + const [filter, setFilter] = useState('all'); + const [priority, setPriority] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + return ( +
+
+
+ +
+ + +
+
+
+ ); +} +APPEOF2 + +git add -A +git commit -m "feat: add responsive header with search and notifications" + +# Commit 2: enhance task list with status icons and priorities +cat > src/components/TaskList.tsx << 'TL2EOF' +import { useTasks } from '../hooks/useTasks'; + +interface Task { + id: string; + title: string; + status: 'active' | 'completed' | 'blocked'; + priority: 'high' | 'medium' | 'low'; + assignee?: string; + dueDate?: string; +} + +interface TaskListProps { + filter: string; +} + +const STATUS_ICONS: Record = { + active: '🔵', + completed: '✅', + blocked: '🔴', +}; + +const PRIORITY_COLORS: Record = { + high: 'priority-high', + medium: 'priority-medium', + low: 'priority-low', +}; + +export function TaskList({ filter }: TaskListProps) { + const { tasks, loading } = useTasks(filter); + + if (loading) { + return ( +
+ {[1, 2, 3].map(i => ( +
+ ))} +
+ ); + } + + if (tasks.length === 0) { + return ( +
+

No tasks found

+ +
+ ); + } + + return ( +
+
+ Task + Priority + Assignee + Due Date + Status +
+ {(tasks as Task[]).map(task => ( +
+
+ {STATUS_ICONS[task.status]} + {task.title} +
+ + {task.priority} + + + {task.assignee ? ( + {task.assignee.slice(0, 2).toUpperCase()} + ) : ( + + )} + + {task.dueDate ?? '—'} + + {task.status} + +
+ ))} +
+ ); +} +TL2EOF + +git add -A +git commit -m "refactor: enhance task list with status icons and priorities" + +# Commit 3: type-safe API client with error handling +cat > src/lib/api.ts << 'API2EOF' +interface ApiError { + message: string; + status: number; +} + +interface Task { + id: string; + title: string; + status: 'active' | 'completed' | 'blocked'; + priority: 'high' | 'medium' | 'low'; + assignee?: string; + dueDate?: string; +} + +interface CreateTaskInput { + title: string; + priority?: Task['priority']; + assignee?: string; + dueDate?: string; +} + +class ApiClient { + private baseUrl: string; + + constructor(baseUrl = '/api') { + this.baseUrl = baseUrl; + } + + private async request(path: string, options?: RequestInit): Promise { + const response = await fetch(`${this.baseUrl}${path}`, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers, + }, + }); + + if (!response.ok) { + const error: ApiError = { + message: `Request failed: ${response.statusText}`, + status: response.status, + }; + throw error; + } + + return response.json(); + } + + async getTasks(filter?: string): Promise { + const params = filter && filter !== 'all' ? `?filter=${filter}` : ''; + return this.request(`/tasks${params}`); + } + + async getTask(id: string): Promise { + return this.request(`/tasks/${id}`); + } + + async createTask(input: CreateTaskInput): Promise { + return this.request('/tasks', { + method: 'POST', + body: JSON.stringify(input), + }); + } + + async updateTask(id: string, updates: Partial): Promise { + return this.request(`/tasks/${id}`, { + method: 'PATCH', + body: JSON.stringify(updates), + }); + } + + async deleteTask(id: string): Promise { + await this.request(`/tasks/${id}`, { method: 'DELETE' }); + } +} + +export const api = new ApiClient(); +API2EOF + +cat > src/hooks/useTasks.ts << 'HT2EOF' +import { useState, useEffect, useCallback } from 'react'; +import { api } from '../lib/api'; + +interface Task { + id: string; + title: string; + status: 'active' | 'completed' | 'blocked'; + priority: 'high' | 'medium' | 'low'; + assignee?: string; + dueDate?: string; +} + +interface UseTasksResult { + tasks: Task[]; + loading: boolean; + error: string | null; + refresh: () => void; + createTask: (title: string) => Promise; + deleteTask: (id: string) => Promise; +} + +export function useTasks(filter: string): UseTasksResult { + const [tasks, setTasks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadTasks = useCallback(async () => { + try { + setLoading(true); + setError(null); + const data = await api.getTasks(filter); + setTasks(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to load tasks'); + } finally { + setLoading(false); + } + }, [filter]); + + useEffect(() => { + loadTasks(); + }, [loadTasks]); + + const createTask = useCallback(async (title: string) => { + await api.createTask({ title }); + await loadTasks(); + }, [loadTasks]); + + const deleteTask = useCallback(async (id: string) => { + await api.deleteTask(id); + await loadTasks(); + }, [loadTasks]); + + return { tasks, loading, error, refresh: loadTasks, createTask, deleteTask }; +} +HT2EOF + +git add -A +git commit -m "refactor: type-safe API client with error handling" + +# Push all branches +git push origin cw/task-manager-redesign-phase-ui-overhaul + +# Clean up +cd / +rm -rf "$TEMP_DIR" + +echo "=== Demo git repository created at $BARE_REPO ===" + +# ─── Step 2: Run Node.js seed ─── + +echo "=== Running database seed ===" +PROJECT_ID=$(CW_DB_PATH=/workspace/.cw/cw.db node /app/apps/server/dist/preview-seed.js) +echo "=== Seed complete, project ID: $PROJECT_ID ===" + +# ─── Step 3: Set up local branches in the server's clone dir ─── + +CLONE_DIR="/workspace/repos/demo-app-${PROJECT_ID}" +echo "=== Cloning demo repo to $CLONE_DIR ===" + +mkdir -p /workspace/repos +git clone "$BARE_REPO" "$CLONE_DIR" + +echo "=== Setting up local branches in $CLONE_DIR ===" + +cd "$CLONE_DIR" +git checkout -b cw/task-manager-redesign origin/cw/task-manager-redesign +git checkout -b cw/task-manager-redesign-phase-ui-overhaul origin/cw/task-manager-redesign-phase-ui-overhaul +git checkout main + +echo "=== Preview seed complete ==="