Created
January 31, 2026 23:10
-
-
Save jalehman/8deb1ed2c4ddfdc4707d147be1247a6b to your computer and use it in GitHub Desktop.
Martian Todos Workshop Demo - Feb 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Martian Todos β Workshop Demo Repo</title> | |
| <style> | |
| :root { | |
| --bg: #0d1117; | |
| --bg-secondary: #161b22; | |
| --border: #30363d; | |
| --text: #c9d1d9; | |
| --text-muted: #8b949e; | |
| --accent: #58a6ff; | |
| --green: #3fb950; | |
| --red: #f85149; | |
| --yellow: #d29922; | |
| --purple: #a371f7; | |
| --orange: #db6d28; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; | |
| background: var(--bg); | |
| color: var(--text); | |
| line-height: 1.6; | |
| padding: 2rem; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| h1, h2, h3 { color: #fff; margin-bottom: 1rem; } | |
| h1 { font-size: 2rem; border-bottom: 1px solid var(--border); padding-bottom: 1rem; } | |
| h2 { font-size: 1.5rem; margin-top: 2.5rem; color: var(--accent); } | |
| h3 { font-size: 1.2rem; margin-top: 1.5rem; color: var(--purple); } | |
| p { margin-bottom: 1rem; } | |
| a { color: var(--accent); text-decoration: none; } | |
| a:hover { text-decoration: underline; } | |
| .badge { | |
| display: inline-block; | |
| padding: 0.25rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| margin-right: 0.5rem; | |
| } | |
| .badge-green { background: rgba(63, 185, 80, 0.2); color: var(--green); } | |
| .badge-yellow { background: rgba(210, 153, 34, 0.2); color: var(--yellow); } | |
| .badge-red { background: rgba(248, 81, 73, 0.2); color: var(--red); } | |
| .badge-purple { background: rgba(163, 113, 247, 0.2); color: var(--purple); } | |
| .card { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| margin: 1rem 0; | |
| } | |
| .tree { | |
| font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; | |
| font-size: 0.85rem; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| overflow-x: auto; | |
| white-space: pre; | |
| } | |
| .tree .folder { color: var(--accent); } | |
| .tree .file { color: var(--text); } | |
| .tree .config { color: var(--yellow); } | |
| .tree .ts { color: var(--green); } | |
| .tree .md { color: var(--purple); } | |
| pre { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| overflow-x: auto; | |
| font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; | |
| font-size: 0.8rem; | |
| line-height: 1.5; | |
| margin: 1rem 0; | |
| } | |
| code { | |
| font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace; | |
| background: var(--bg-secondary); | |
| padding: 0.15rem 0.4rem; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| } | |
| .highlight { background: rgba(248, 81, 73, 0.15); border-left: 3px solid var(--red); padding-left: 0.5rem; margin-left: -0.5rem; } | |
| .comment { color: var(--text-muted); } | |
| .keyword { color: var(--purple); } | |
| .string { color: var(--green); } | |
| .function { color: var(--accent); } | |
| .type { color: var(--yellow); } | |
| .api-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 1rem 0; | |
| } | |
| .api-table th, .api-table td { | |
| text-align: left; | |
| padding: 0.75rem; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .api-table th { color: var(--text-muted); font-weight: 500; } | |
| .method { font-weight: 600; font-family: monospace; } | |
| .method-get { color: var(--green); } | |
| .method-post { color: var(--accent); } | |
| .method-patch { color: var(--yellow); } | |
| .method-delete { color: var(--red); } | |
| .imperfection { | |
| background: rgba(248, 81, 73, 0.1); | |
| border: 1px solid rgba(248, 81, 73, 0.3); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin: 1rem 0; | |
| } | |
| .imperfection h4 { | |
| color: var(--red); | |
| margin-bottom: 0.5rem; | |
| font-size: 1rem; | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 1rem; | |
| margin: 1rem 0; | |
| } | |
| .stat { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: 1rem; | |
| text-align: center; | |
| } | |
| .stat-value { font-size: 2rem; font-weight: 700; color: var(--accent); } | |
| .stat-label { font-size: 0.8rem; color: var(--text-muted); } | |
| footer { | |
| margin-top: 3rem; | |
| padding-top: 1rem; | |
| border-top: 1px solid var(--border); | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>π Martian Todos</h1> | |
| <p>Demo todo app for <strong>AI engineering workshops</strong>. A production-ready pnpm TypeScript monorepo following Connected Play patterns.</p> | |
| <p><span class="badge badge-green">Workshop Ready</span> <span class="badge badge-purple">Feb 10 2026</span> <span class="badge badge-yellow">Renew Home</span></p> | |
| <div class="stats"> | |
| <div class="stat"><div class="stat-value">47</div><div class="stat-label">Files</div></div> | |
| <div class="stat"><div class="stat-value">5</div><div class="stat-label">Workers Built</div></div> | |
| <div class="stat"><div class="stat-value">3</div><div class="stat-label">Deliberate Bugs</div></div> | |
| <div class="stat"><div class="stat-value">12</div><div class="stat-label">API Endpoints</div></div> | |
| </div> | |
| <h2>π Directory Structure</h2> | |
| <div class="tree"><span class="folder">martian-todos/</span> | |
| βββ <span class="config">.env.example</span> | |
| βββ <span class="config">.gitignore</span> | |
| βββ <span class="md">CLAUDE.md</span> | |
| βββ <span class="config">Makefile</span> | |
| βββ <span class="md">README.md</span> | |
| βββ <span class="md">WORKSHOP_FACILITATOR_NOTES.md</span> <span class="comment">β Deliberate imperfections documented here</span> | |
| βββ <span class="folder">apps/</span> | |
| β βββ <span class="folder">backend/</span> | |
| β β βββ Dockerfile | |
| β β βββ package.json | |
| β β βββ <span class="folder">src/</span> | |
| β β β βββ <span class="ts">config.ts</span> | |
| β β β βββ <span class="folder">db/</span> | |
| β β β β βββ <span class="ts">database.ts</span> | |
| β β β β βββ <span class="ts">migrate.ts</span> | |
| β β β β βββ <span class="ts">schema.ts</span> | |
| β β β βββ <span class="ts">index.ts</span> | |
| β β β βββ <span class="folder">middleware/</span> | |
| β β β β βββ <span class="ts">auth.ts</span> | |
| β β β βββ <span class="folder">routes/</span> | |
| β β β βββ <span class="ts">auth.ts</span> <span class="comment">β JWT + bcrypt + refresh tokens</span> | |
| β β β βββ <span class="ts">todos.ts</span> <span class="comment">β CRUD + search + bulk ops (has bug!)</span> | |
| β β βββ tsconfig.json | |
| β βββ <span class="folder">frontend/</span> | |
| β βββ Dockerfile | |
| β βββ index.html | |
| β βββ package.json | |
| β βββ <span class="folder">src/</span> | |
| β β βββ <span class="ts">App.tsx</span> | |
| β β βββ <span class="folder">api/</span> | |
| β β β βββ <span class="ts">auth.ts</span> | |
| β β β βββ <span class="ts">todos.ts</span> | |
| β β βββ <span class="folder">components/</span> | |
| β β β βββ <span class="ts">AddTodoForm.tsx</span> | |
| β β β βββ <span class="ts">ErrorBoundary.tsx</span> | |
| β β β βββ <span class="ts">FilterBar.tsx</span> | |
| β β β βββ <span class="ts">KeyboardShortcuts.tsx</span> | |
| β β β βββ <span class="ts">LoginForm.tsx</span> | |
| β β β βββ <span class="ts">TodoItem.tsx</span> | |
| β β β βββ <span class="ts">TodoList.tsx</span> <span class="comment">β Has prop mutation bug!</span> | |
| β β βββ <span class="folder">hooks/</span> | |
| β β β βββ <span class="ts">useAuth.ts</span> | |
| β β βββ index.css | |
| β β βββ <span class="ts">main.tsx</span> | |
| β βββ tsconfig.json | |
| β βββ tsconfig.node.json | |
| β βββ vite.config.ts | |
| βββ <span class="config">docker-compose.yml</span> <span class="comment">β Hot reload local dev</span> | |
| βββ <span class="config">mise.toml</span> | |
| βββ package.json | |
| βββ <span class="folder">packages/</span> | |
| β βββ <span class="folder">shared/</span> | |
| β βββ package.json | |
| β βββ <span class="folder">src/</span> | |
| β β βββ <span class="ts">index.ts</span> | |
| β β βββ <span class="ts">types.ts</span> <span class="comment">β Shared types</span> | |
| β βββ tsconfig.json | |
| βββ <span class="folder">platform/</span> | |
| β βββ <span class="folder">terraform/</span> | |
| β βββ <span class="md">README.md</span> | |
| β βββ <span class="folder">environments/</span> | |
| β β βββ <span class="folder">dev/</span> | |
| β β βββ main.tf | |
| β β βββ terraform.tfvars.example | |
| β βββ <span class="folder">modules/</span> | |
| β βββ <span class="folder">ecs/</span> main.tf | |
| β βββ <span class="folder">rds/</span> main.tf | |
| β βββ <span class="folder">vpc/</span> main.tf | |
| βββ <span class="config">pnpm-workspace.yaml</span> | |
| βββ tsconfig.json</div> | |
| <h2>π API Endpoints</h2> | |
| <h3>Auth</h3> | |
| <table class="api-table"> | |
| <tr><th>Method</th><th>Endpoint</th><th>Description</th></tr> | |
| <tr><td class="method method-post">POST</td><td><code>/auth/register</code></td><td>Create account</td></tr> | |
| <tr><td class="method method-post">POST</td><td><code>/auth/login</code></td><td>Get JWT token</td></tr> | |
| <tr><td class="method method-post">POST</td><td><code>/auth/refresh</code></td><td>Refresh access token</td></tr> | |
| <tr><td class="method method-post">POST</td><td><code>/auth/logout</code></td><td>Revoke refresh token</td></tr> | |
| </table> | |
| <h3>Todos (authenticated)</h3> | |
| <table class="api-table"> | |
| <tr><th>Method</th><th>Endpoint</th><th>Description</th></tr> | |
| <tr><td class="method method-get">GET</td><td><code>/todos</code></td><td>List todos (pagination, filtering, search, sorting)</td></tr> | |
| <tr><td class="method method-get">GET</td><td><code>/todos/:id</code></td><td>Get single todo</td></tr> | |
| <tr><td class="method method-post">POST</td><td><code>/todos</code></td><td>Create todo</td></tr> | |
| <tr><td class="method method-patch">PATCH</td><td><code>/todos/complete-all</code></td><td>Mark all as completed</td></tr> | |
| <tr><td class="method method-patch">PATCH</td><td><code>/todos/:id</code></td><td>Update todo</td></tr> | |
| <tr><td class="method method-delete">DELETE</td><td><code>/todos/completed</code></td><td>Delete completed todos</td></tr> | |
| <tr><td class="method method-delete">DELETE</td><td><code>/todos/:id</code></td><td>Delete todo</td></tr> | |
| </table> | |
| <h2>π Deliberate Imperfections</h2> | |
| <p>These bugs are <strong>intentionally inserted</strong> for workshop demonstrations. They're designed to be:</p> | |
| <ul style="margin-left: 1.5rem; margin-bottom: 1rem;"> | |
| <li>Safe and non-destructive</li> | |
| <li>Easy to find with AI code review</li> | |
| <li>Educational examples of common mistakes</li> | |
| </ul> | |
| <div class="imperfection"> | |
| <h4>π΄ Bug #1: Prop Mutation in TodoList.tsx</h4> | |
| <p><strong>File:</strong> <code>apps/frontend/src/components/TodoList.tsx</code></p> | |
| <p><strong>Issue:</strong> Sorting mutates the props array in place, causing unstable ordering across renders.</p> | |
| <pre><span class="keyword">const</span> priorityWeights: <span class="type">Record</span><<span class="type">string</span>, <span class="type">number</span>> = { | |
| high: <span class="string">0</span>, | |
| medium: <span class="string">1</span>, | |
| low: <span class="string">2</span>, | |
| }; | |
| <span class="comment">// TODO: This mutates props and creates unstable ordering; copy before sorting.</span> | |
| <div class="highlight"><span class="keyword">const</span> sortedTodos = todos.<span class="function">sort</span>( | |
| (left, right) => | |
| (priorityWeights[left.priority] ?? <span class="string">99</span>) - | |
| (priorityWeights[right.priority] ?? <span class="string">99</span>) | |
| );</div></pre> | |
| <p><strong>Fix:</strong> Use <code>[...todos].sort(...)</code> or <code>useMemo</code> to avoid mutating props.</p> | |
| </div> | |
| <div class="imperfection"> | |
| <h4>π΄ Bug #2: Pagination Off-by-One in todos.ts</h4> | |
| <p><strong>File:</strong> <code>apps/backend/src/routes/todos.ts</code></p> | |
| <p><strong>Issue:</strong> Using <code>Math.floor</code> instead of <code>Math.ceil</code> undercounts the last page.</p> | |
| <pre><span class="keyword">const</span> response: <span class="type">PaginatedResponse</span><<span class="type">Todo</span>> = { | |
| items: todos.<span class="function">map</span>(mapTodo), | |
| total, | |
| page, | |
| pageSize, | |
| <span class="comment">// TODO: Revisit pagination math to avoid off-by-one behavior.</span> | |
| <div class="highlight"> totalPages: <span class="type">Math</span>.<span class="function">floor</span>(total / pageSize),</div>};</pre> | |
| <p><strong>Fix:</strong> Use <code>Math.ceil(total / pageSize)</code> to include partial last pages.</p> | |
| </div> | |
| <div class="imperfection"> | |
| <h4>π‘ Code Smell: Recreating on Every Render</h4> | |
| <p><strong>File:</strong> <code>apps/frontend/src/components/TodoList.tsx</code></p> | |
| <p><strong>Issue:</strong> The <code>priorityWeights</code> object and sorting logic are recreated on every render.</p> | |
| <p><strong>Fix:</strong> Extract to a module-level constant or use <code>useMemo</code>.</p> | |
| </div> | |
| <h2>π» Key Source Files</h2> | |
| <h3>Auth Routes (JWT + Refresh Tokens)</h3> | |
| <p><code>apps/backend/src/routes/auth.ts</code> β Full auth flow with bcrypt password hashing and secure refresh tokens.</p> | |
| <pre><span class="comment">/** | |
| * Generates a cryptographically secure refresh token. | |
| */</span> | |
| <span class="keyword">function</span> <span class="function">generateRefreshToken</span>(): <span class="type">string</span> { | |
| <span class="keyword">return</span> <span class="function">randomBytes</span>(REFRESH_TOKEN_BYTES).<span class="function">toString</span>(<span class="string">"hex"</span>); | |
| } | |
| <span class="comment">/** | |
| * Hashes a refresh token for storage. | |
| */</span> | |
| <span class="keyword">function</span> <span class="function">hashRefreshToken</span>(token: <span class="type">string</span>): <span class="type">string</span> { | |
| <span class="keyword">return</span> <span class="function">createHash</span>(<span class="string">"sha256"</span>).<span class="function">update</span>(token).<span class="function">digest</span>(<span class="string">"hex"</span>); | |
| }</pre> | |
| <h3>Todos API (Search + Bulk Ops)</h3> | |
| <p><code>apps/backend/src/routes/todos.ts</code> β Full CRUD with search, filtering, sorting, and bulk operations.</p> | |
| <pre><span class="comment">// Supported query params:</span> | |
| <span class="keyword">const</span> ListTodosSchema = z.<span class="function">object</span>({ | |
| page: z.<span class="function">coerce</span>.<span class="function">number</span>().<span class="function">int</span>().<span class="function">min</span>(<span class="string">1</span>).<span class="function">default</span>(<span class="string">1</span>), | |
| pageSize: z.<span class="function">coerce</span>.<span class="function">number</span>().<span class="function">int</span>().<span class="function">min</span>(<span class="string">1</span>).<span class="function">max</span>(<span class="string">100</span>).<span class="function">default</span>(<span class="string">20</span>), | |
| status: z.<span class="function">enum</span>([<span class="string">"pending"</span>, <span class="string">"in_progress"</span>, <span class="string">"completed"</span>]).<span class="function">optional</span>(), | |
| priority: z.<span class="function">enum</span>([<span class="string">"low"</span>, <span class="string">"medium"</span>, <span class="string">"high"</span>]).<span class="function">optional</span>(), | |
| search: z.<span class="function">string</span>().<span class="function">trim</span>().<span class="function">min</span>(<span class="string">1</span>).<span class="function">max</span>(<span class="string">200</span>).<span class="function">optional</span>(), | |
| sortBy: z.<span class="function">enum</span>([<span class="string">"createdAt"</span>, <span class="string">"updatedAt"</span>, <span class="string">"dueDate"</span>, <span class="string">"priority"</span>, <span class="string">"status"</span>, <span class="string">"title"</span>]) | |
| .<span class="function">default</span>(<span class="string">"createdAt"</span>), | |
| sortOrder: z.<span class="function">enum</span>([<span class="string">"asc"</span>, <span class="string">"desc"</span>]).<span class="function">default</span>(<span class="string">"desc"</span>), | |
| });</pre> | |
| <h2>π³ Docker Compose</h2> | |
| <p><code>docker-compose.yml</code> β Local dev with hot reload for both frontend and backend.</p> | |
| <pre><span class="keyword">services:</span> | |
| <span class="function">postgres:</span> | |
| image: postgres:16-alpine | |
| environment: | |
| POSTGRES_USER: martian | |
| POSTGRES_PASSWORD: martian | |
| POSTGRES_DB: martian_todos | |
| ports: | |
| - <span class="string">"5432:5432"</span> | |
| volumes: | |
| - postgres_data:/var/lib/postgresql/data | |
| <span class="function">backend:</span> | |
| build: ./apps/backend | |
| ports: | |
| - <span class="string">"3001:3001"</span> | |
| environment: | |
| DATABASE_URL: postgres://martian:martian@postgres:5432/martian_todos | |
| volumes: | |
| - ./apps/backend/src:/app/src <span class="comment"># Hot reload</span> | |
| depends_on: | |
| - postgres | |
| <span class="function">frontend:</span> | |
| build: ./apps/frontend | |
| ports: | |
| - <span class="string">"5173:5173"</span> | |
| volumes: | |
| - ./apps/frontend/src:/app/src <span class="comment"># Hot reload</span></pre> | |
| <h2>π οΈ Tech Stack</h2> | |
| <div class="card"> | |
| <ul style="list-style: none;"> | |
| <li><span class="badge badge-green">Runtime</span> Node.js 22, pnpm workspaces</li> | |
| <li><span class="badge badge-purple">Backend</span> Fastify, Kysely (PostgreSQL), JWT auth, bcrypt</li> | |
| <li><span class="badge badge-yellow">Frontend</span> React 18, Vite, TypeScript</li> | |
| <li><span class="badge badge-red">Infra</span> Terraform, AWS ECS Fargate, RDS PostgreSQL</li> | |
| </ul> | |
| </div> | |
| <footer> | |
| <p>Generated for <strong>Renew Home AI Workshop</strong> β Feb 10, 2026</p> | |
| <p>Built by 5 parallel Codex workers via claude-team π€</p> | |
| </footer> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment