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:
577
scripts/seed-preview.sh
Executable file
577
scripts/seed-preview.sh
Executable 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 ==="
|
||||
Reference in New Issue
Block a user