feat: Add Dockerfile, preview config, and seed script for self-preview deployments

Containerize Codewalkers with a multi-stage Docker build (Node + Caddy) and
add a seed script that populates the database with a demo initiative, 3 phases,
9 tasks, 3 agents with JSONL log output, a root page, review comments, and a
git repo with real branch diffs for the review tab.
This commit is contained in:
Lukas May
2026-03-05 14:26:22 +01:00
parent 714262fb83
commit 66605da30d
8 changed files with 1220 additions and 0 deletions

11
.cw-preview.yml Normal file
View File

@@ -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

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
node_modules/
.cw/
workdir/
.git/
apps/server/dist/
apps/web/dist/
coverage/
*.log
.env*
.DS_Store
.screenshots/
.cw-previews/

48
Dockerfile Normal file
View File

@@ -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"]

482
apps/server/preview-seed.ts Normal file
View File

@@ -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 <div>Loading...</div>;\n return (\n <ul className=\"task-list\">\n {tasks.map(task => (\n <li key={task.id}>\n <span>{task.title}</span>\n <span className=\"status\">{task.status}</span>\n </li>\n ))}\n </ul>\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);

View File

@@ -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 <container> sh /app/scripts/seed-preview.sh
# Browse http://localhost:3000
```
## Dependencies
- `js-yaml` + `@types/js-yaml` — for parsing `.cw-preview.yml`

15
scripts/Caddyfile Normal file
View File

@@ -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
}
}

34
scripts/entrypoint.sh Executable file
View File

@@ -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

577
scripts/seed-preview.sh Executable file
View File

@@ -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 (
<div className="app">
<Sidebar filter={filter} onFilterChange={setFilter} />
<main>
<h1>Task Manager</h1>
<TaskList filter={filter} />
</main>
</div>
);
}
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 <div>Loading...</div>;
return (
<ul className="task-list">
{tasks.map(task => (
<li key={task.id}>
<span>{task.title}</span>
<span className="status">{task.status}</span>
</li>
))}
</ul>
);
}
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 (
<aside className="sidebar">
<h2>Filters</h2>
<ul>
{filters.map(f => (
<li key={f}>
<button
className={f === filter ? 'active' : ''}
onClick={() => onFilterChange(f)}
>
{f}
</button>
</li>
))}
</ul>
</aside>
);
}
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<any[]>([]);
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 (
<header className="header">
<div className="header-brand">
<h1>TaskFlow</h1>
</div>
<div className="header-search">
<input
type="search"
placeholder="Search tasks..."
value={query}
onChange={e => {
setQuery(e.target.value);
onSearch(e.target.value);
}}
/>
</div>
<div className="header-actions">
<button className="notification-bell" aria-label="Notifications">
🔔 {unreadCount > 0 && <span className="badge">{unreadCount}</span>}
</button>
<div className="avatar">JD</div>
</div>
</header>
);
}
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 (
<div className="task-filters">
<div className="filter-group">
<label>Status</label>
<div className="filter-buttons">
{statuses.map(s => (
<button
key={s}
className={s === activeFilter ? 'active' : ''}
onClick={() => onFilterChange(s)}
>
{s}
</button>
))}
</div>
</div>
<div className="filter-group">
<label>Priority</label>
<div className="filter-buttons">
{priorities.map(p => (
<button
key={p}
className={p === activePriority ? 'active' : ''}
onClick={() => onPriorityChange(p)}
>
{p}
</button>
))}
</div>
</div>
</div>
);
}
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 (
<div className="app-layout">
<Header onSearch={setSearchQuery} />
<div className="app-body">
<Sidebar filter={filter} onFilterChange={setFilter} />
<main className="main-content">
<TaskFilters
activeFilter={filter}
onFilterChange={setFilter}
activePriority={priority}
onPriorityChange={setPriority}
/>
<TaskList filter={filter} />
</main>
</div>
</div>
);
}
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<string, string> = {
active: '🔵',
completed: '✅',
blocked: '🔴',
};
const PRIORITY_COLORS: Record<string, string> = {
high: 'priority-high',
medium: 'priority-medium',
low: 'priority-low',
};
export function TaskList({ filter }: TaskListProps) {
const { tasks, loading } = useTasks(filter);
if (loading) {
return (
<div className="task-list-skeleton">
{[1, 2, 3].map(i => (
<div key={i} className="skeleton-row" />
))}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="empty-state">
<p>No tasks found</p>
<button className="btn-primary">Create your first task</button>
</div>
);
}
return (
<div className="task-list">
<div className="task-list-header">
<span>Task</span>
<span>Priority</span>
<span>Assignee</span>
<span>Due Date</span>
<span>Status</span>
</div>
{(tasks as Task[]).map(task => (
<div key={task.id} className="task-row">
<div className="task-title">
<span className="status-icon">{STATUS_ICONS[task.status]}</span>
<span>{task.title}</span>
</div>
<span className={`priority-badge ${PRIORITY_COLORS[task.priority]}`}>
{task.priority}
</span>
<span className="assignee">
{task.assignee ? (
<span className="avatar-small">{task.assignee.slice(0, 2).toUpperCase()}</span>
) : (
<span className="unassigned">—</span>
)}
</span>
<span className="due-date">{task.dueDate ?? '—'}</span>
<span className={`status-badge status-${task.status}`}>
{task.status}
</span>
</div>
))}
</div>
);
}
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<T>(path: string, options?: RequestInit): Promise<T> {
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<Task[]> {
const params = filter && filter !== 'all' ? `?filter=${filter}` : '';
return this.request<Task[]>(`/tasks${params}`);
}
async getTask(id: string): Promise<Task> {
return this.request<Task>(`/tasks/${id}`);
}
async createTask(input: CreateTaskInput): Promise<Task> {
return this.request<Task>('/tasks', {
method: 'POST',
body: JSON.stringify(input),
});
}
async updateTask(id: string, updates: Partial<Task>): Promise<Task> {
return this.request<Task>(`/tasks/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates),
});
}
async deleteTask(id: string): Promise<void> {
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<void>;
deleteTask: (id: string) => Promise<void>;
}
export function useTasks(filter: string): UseTasksResult {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 ==="