# SignalLearn — Vibe Coding Build Prompts
**Version:** 1.0  
**Target:** Tanstack Start + Drizzle ORM + Zustand + shadcn/ui  
**Format:** Copy-paste prompts for Replit/Lovable/v0/Claude Code  
**Timeline:** MVP 1.1 (5 weeks to production)

---

## TABLE OF CONTENTS

1. [Prompt 1: Project Setup & Database](#prompt-1-project-setup--database)
2. [Prompt 2: Authentication (Google OAuth + JWT)](#prompt-2-authentication-google-oauth--jwt)
3. [Prompt 3: Teacher Course CRUD](#prompt-3-teacher-course-crud)
4. [Prompt 4: Session Management (Start/End)](#prompt-4-session-management-startend)
5. [Prompt 5: Join Code Generation & Management](#prompt-5-join-code-generation--management)
6. [Prompt 6: Student Join Flow (Code & Link)](#prompt-6-student-join-flow-code--link)
7. [Prompt 7: Status Events (Polling & Dashboard)](#prompt-7-status-events-polling--dashboard)
8. [Prompt 8: Teacher Dashboard UI](#prompt-8-teacher-dashboard-ui)
9. [Prompt 9: Student Status Board UI](#prompt-9-student-status-board-ui)
10. [Prompt 10: Historical Review & Admin Stats](#prompt-10-historical-review--admin-stats)

---

## IMPORTANT NOTES

- **Do NOT mix prompts** — Execute one prompt at a time, test it, then move to next
- **Test after each prompt** — Make sure endpoints work before moving forward
- **Use the ERD.md & DESIGN.md as reference** — They are your spec documents
- **Database schema is locked** — Don't deviate from ERD.md entities
- **API endpoints are spec'd in DESIGN.md** — Follow exact request/response shapes
- **No shortcuts** — Production-grade code, proper error handling, validation

---

## PROMPT 1: PROJECT SETUP & DATABASE

**Goal:** Initialize Tanstack Start project, install dependencies, set up Drizzle schema, and database migrations.

**Copy-paste this prompt:**

---

### PROMPT 1: Project Initialization & Database Schema

I'm building **Signal-Learn**, a real-time classroom traffic light app using **Tanstack Start** + **Drizzle ORM** + **MySQL** in localhost for development and production in my.php.id hosting/cpanel.

**Tech Stack:**
- Framework: Tanstack Start (latest, file-based routing, fullstack)
- ORM: Drizzle ORM (TypeScript, zero-runtime)
- Database: MySQL 8.4+
- Runtime: Bun
- Styling: Tailwind CSS + shadcn/ui

**Initial Setup:**

1. Create new Tanstack Start project:
   ```bash
   bunx @tanstack/cli create .
   ```

2. Install dependencies:
   ```bash
   bun add drizzle-orm drizzle-kit mysql2
   bun add zustand
   bun add jsonwebtoken
   bun add bcrypt
   bun add uuid
   ```

3. Create database schema file at `src/server/db/schema.ts`.

**Schema Requirements:**
- 8 entities: `users`, `courses`, `course_sessions`, `join_codes`, `participants`, `status_events`, `session_dashboard`, `session_summaries`
- All PKs: UUID (CHAR(36))
- Soft-delete on courses/sessions (deleted_at field)
- Optimistic locking on mutable entities (version field)
- Composite unique on (session_id, code) for join_codes
- Cascade delete relationships as specified in ERD.md

4. Create Drizzle config at `drizzle.config.ts`:
   ```typescript
   import { defineConfig } from 'drizzle-kit';

   export default defineConfig({
     schema: './src/server/db/schema.ts',
     out: './src/server/db/migrations',
     driver: 'mysql2',
     dbCredentials: {
       host: process.env.DB_HOST!,
       user: process.env.DB_USER!,
       password: process.env.DB_PASSWORD!,
       database: process.env.DB_NAME!,
       port: 3306,
     },
   });
   ```

5. Create initial migration:
   ```bash
   bun drizzle-kit generate:mysql
   ```

6. Create database client at `src/server/db/index.ts`:
   ```typescript
   import { drizzle } from 'drizzle-orm/mysql2';
   import mysql from 'mysql2/promise';
   import * as schema from './schema';

   const poolConnection = mysql.createPool({
     host: process.env.DB_HOST!,
     user: process.env.DB_USER!,
     password: process.env.DB_PASSWORD!,
     database: process.env.DB_NAME!,
     waitForConnections: true,
     connectionLimit: 10,
     queueLimit: 0,
   });

   export const db = drizzle(poolConnection, { schema });
   ```

7. Create `.env.local`:
   ```
   DB_HOST=localhost
   DB_USER=root
   DB_PASSWORD=...
   DB_NAME=signal_learn
   JWT_SECRET=your-secret-key-here
   GOOGLE_CLIENT_ID=...
   GOOGLE_CLIENT_SECRET=...
   ```

**Deliverables:**
- ✅ Tanstack Start project scaffolded
- ✅ Drizzle ORM configured
- ✅ Database schema matching ERD.md (all 8 entities)
- ✅ Migration files generated
- ✅ Database client initialized
- ✅ .env.local with required secrets

**Test:**
```bash
bun run dev
# Should start dev server on http://localhost:5173
```

---

## PROMPT 2: AUTHENTICATION (GOOGLE OAUTH + JWT)

**Goal:** Implement Google OAuth login flow and JWT token generation/verification.

**Copy-paste this prompt:**

---

### PROMPT 2: Google OAuth + JWT Authentication

I need to implement Google OAuth + JWT authentication for Signal-Learn.

**Requirements:**

1. Create `src/server/utils/jwt.ts`:
   - `generateTokens(userId, email, role)` → { accessToken, refreshToken }
   - `verifyToken(token)` → decoded payload or throw error
   - Access token: expires in 1 hour
   - Refresh token: expires in 7 days
   - Payload: { sub: userId, email, role, iat, exp, iss: 'signal-learn' }

2. Create `src/server/utils/auth.ts`:
   - `verifyGoogleToken(idToken)` → user info { sub, email, name, picture }
   - Error handling for invalid/expired tokens

3. Create route handler at `src/routes/api/auth/google/routes.ts`:
   - POST endpoint to exchange Google auth code for JWT tokens
   - Create/upsert user in DB (users table)
   - Return { accessToken, refreshToken, user }
   - Set secure HttpOnly cookie for refresh token

4. Create route handler at `src/routes/api/auth/logout/routes.ts`:
   - POST endpoint
   - Requires Authorization header (Bearer token)
   - Clear session data
   - Return { message: "Logged out successfully" }

5. Create middleware at `src/routes/api/_middleware.ts`:
   - Extract JWT from Authorization header
   - Verify token and attach user to request context
   - Allow public routes (/join-by-code, /join-by-link, /status-events) without auth
   - Protect all teacher routes (require valid token)

6. Create `src/server/utils/errors.ts`:
   - APIError class with statusCode, code, message
   - errorResponse helper for consistent error formatting

**API Specification (from DESIGN.md):**

POST /api/auth/google
- Request body: { code, state }
- Response (201): { accessToken, refreshToken, user: { id, email, name, role } }
- Errors: 400 (invalid code), 500 (provider error)

POST /api/auth/logout
- Request: Authorization header required
- Response (200): { message: "Logged out successfully" }
- Errors: 401 (unauthorized)

**Validation:**
- JWT_SECRET and GOOGLE_CLIENT_ID/SECRET required in .env
- Token payload must include sub, email, role
- Refresh tokens should be HttpOnly (secure storage)

**Test:**
```bash
# Test Google OAuth flow
POST /api/auth/google
Body: { "code": "..." }

# Test logout
POST /api/auth/logout
Headers: { "Authorization": "Bearer ..." }
```

---

## PROMPT 3: TEACHER COURSE CRUD

**Goal:** Implement Course creation, reading, updating, deleting for teachers.

**Copy-paste this prompt:**

---

### PROMPT 3: Teacher Course CRUD (Create, Read, Update, Delete)

I need to implement full CRUD for courses in Signal-Learn. Teachers create and manage courses.

**Requirements:**

1. Create route handler at `src/routes/api/courses/routes.ts`:

   **GET /api/courses**
   - Query: { status?: 'draft|active|completed', limit?: 20, offset?: 0 }
   - Return paginated list of courses for logged-in teacher
   - Include: id, title, status, date, duration_minutes, session_code, created_at
   - Filter: only courses where instructor_id = current user
   - Exclude soft-deleted courses (WHERE deleted_at IS NULL)
   - HTTP cache: Cache-Control: "public, max-age=300"

   **POST /api/courses**
   - Request body: { title, description, date, start_time, duration_minutes, allow_anonymous }
   - Create course with auto-generated session_code (format: SIGNAL-ABC-123)
   - Use UUID for course ID
   - Set status: 'draft' by default
   - Set instructor_id from JWT token (req.user.id)
   - Return 201 with full course object
   - Validation:
     * title: required, 1-255 chars
     * duration_minutes: between 30 and 120
     * date: YYYY-MM-DD format, >= today
     * start_time: HH:MM:SS format

   **PATCH /api/courses/:courseId**
   - Update course fields (title, description, date, start_time)
   - Only if course status = 'draft' (can't edit active/completed)
   - Verify instructor_id matches JWT user
   - Use optimistic locking: check version before update, increment on success
   - Return 200 with updated course
   - Errors: 400 (can't edit active), 401 (not instructor), 404 (not found)

   **DELETE /api/courses/:courseId**
   - Soft-delete via deleted_at timestamp
   - Only instructor can delete
   - Return 204 No Content
   - Errors: 401 (not instructor), 404 (not found)

2. Create validation helpers at `src/server/utils/validators.ts`:
   - `validateCourseInput(data)` → throw APIError if invalid
   - Check title, duration_minutes, date, start_time, allow_anonymous

3. Create service layer at `src/server/services/course.service.ts`:
   - `generateUniqueSessionCode()` → SIGNAL-XYZ-123 format, check uniqueness
   - `createCourse(instructor_id, input)` → inserts and returns course
   - `updateCourse(courseId, input, version)` → with optimistic locking
   - `deleteCourse(courseId)` → soft-delete

4. Error handling:
   - 400: Validation failed
   - 401: Unauthorized (not logged in, not instructor)
   - 404: Course not found
   - 409: Version conflict (optimistic locking)

**Database Schema (reference ERD.md):**
- courses table: id, instructor_id, title, description, date, start_time, duration_minutes, session_code, allow_anonymous, status, created_at, updated_at, deleted_at, version

**Test:**
```bash
# Create course
POST /api/courses
Headers: { "Authorization": "Bearer ..." }
Body: {
  "title": "Math 101",
  "description": "Basic algebra",
  "date": "2026-08-20",
  "start_time": "10:00:00",
  "duration_minutes": 60,
  "allow_anonymous": false
}

# List courses
GET /api/courses?status=draft&limit=10

# Update course
PATCH /api/courses/:id
Body: { "title": "Algebra Basics" }

# Delete course
DELETE /api/courses/:id
```

---

## PROMPT 4: SESSION MANAGEMENT (START/END)

**Goal:** Implement session start/end logic with auto-computed summaries.

**Copy-paste this prompt:**

---

### PROMPT 4: Session Management (Start & End Session)

I need to implement session start/end logic for Signal-Learn. Teachers start a session before class, end it after. On end, compute and store session summary.

**Requirements:**

1. Create route handler at `src/routes/api/courses/[courseId]/sessions/[sessionId]/routes.ts`:

   **POST /api/courses/:courseId/sessions/:sessionId/start**
   - Check course exists and instructor_id matches JWT user
   - Create course_sessions record with:
     * id: UUID
     * course_id: from URL param
     * session_start_time: NOW()
     * is_active: true
   - Create session_dashboard record (denormalized for polling):
     * session_id: same
     * red_count, yellow_count, green_count: all 0 initially
     * total_participants: 0
     * active_participants: 0
     * elapsed_seconds: 0
     * last_updated: NOW()
   - Generate shareable link: https://signal-learn.web.id/session/{SIGNAL-ABC-123}
   - Return 200: { session_id, is_active: true, session_start_time, shareable_link }
   - Errors: 400 (session already active), 401 (not instructor), 404 (course not found)

   **POST /api/courses/:courseId/sessions/:sessionId/end**
   - Verify course exists and instructor authorized
   - Check session is_active = true
   - Set session_end_time: NOW(), is_active: false
   - Compute session_summaries:
     * total_participants: COUNT(DISTINCT participants)
     * participants_via_link: COUNT(DISTINCT participants WHERE join_method='link')
     * participants_via_code: COUNT(DISTINCT participants WHERE join_method='code')
     * codes_generated_count: COUNT(DISTINCT join_codes)
     * codes_revoked_count: COUNT(DISTINCT join_codes WHERE status='revoked')
     * final_red_count: COUNT(DISTINCT status_events WHERE status='red' AND is_reset=FALSE)
     * final_yellow_count, final_green_count: similar
     * duration_seconds: EXTRACT(EPOCH FROM (session_end_time - session_start_time))
   - Insert session_summaries record (immutable, one per session)
   - Return 200: { session_id, is_active: false, session_end_time, summary: {...} }
   - Errors: 400 (session not active), 401 (not instructor), 404 (not found)

2. Create service at `src/server/services/session.service.ts`:
   - `startSession(courseId, userId)` → creates course_sessions + session_dashboard
   - `endSession(sessionId, userId)` → computes summary, updates session, creates session_summaries
   - `getSessionSummary(sessionId, userId)` → returns computed summary

3. Database operations:
   - Use transactions for start/end (ensure atomicity)
   - Soft-delete courses, but sessions can be hard-deleted on cascade

4. Error handling:
   - 400: Bad request (session already active, not active, etc.)
   - 401: Unauthorized
   - 404: Course/session not found
   - 500: Database error

**Database Schema (reference ERD.md):**
- course_sessions: id, course_id, session_start_time, session_end_time, is_active, created_at, updated_at
- session_dashboard: id, session_id, red_count, yellow_count, green_count, total_participants, active_participants, elapsed_seconds, last_updated, version
- session_summaries: id, session_id, total_participants, participants_via_link, participants_via_code, codes_generated_count, codes_revoked_count, final_red_count, final_yellow_count, final_green_count, duration_seconds, created_at

**Test:**
```bash
# Start session
POST /api/courses/:courseId/sessions/:sessionId/start
Headers: { "Authorization": "Bearer ..." }

# End session (after some time)
POST /api/courses/:courseId/sessions/:sessionId/end
Headers: { "Authorization": "Bearer ..." }
```

---

## PROMPT 5: JOIN CODE GENERATION & MANAGEMENT

**Goal:** Implement teacher-generated join codes with auto-generate and custom options, revoke functionality.

**Copy-paste this prompt:**

---

### PROMPT 5: Join Code Generation & Management

I need to implement join code generation for teachers in Signal-Learn. Teachers can auto-generate codes or create custom codes, and revoke them anytime.

**Requirements:**

1. Create route handler at `src/routes/api/courses/[courseId]/sessions/[sessionId]/codes/routes.ts`:

   **POST /api/courses/:courseId/sessions/:sessionId/codes**
   - Request body: { code?: "MATH-101", auto_generate?: true }
   - If auto_generate OR code not provided: generate unique code (format: 3-UPPERCASE-3, e.g., "MATH-101")
   - If code provided: use it (validate format: 3-50 chars, alphanumeric + hyphens)
   - Check (session_id, code) composite unique constraint
   - Create join_codes record:
     * id: UUID
     * session_id: from URL param
     * code: the code string
     * is_custom: true if user provided, false if auto-generated
     * status: 'active'
     * generated_at: NOW()
     * created_by: JWT user ID
     * usage_count: 0 initially
   - Return 201: { id, session_id, code, is_custom, status, generated_at, usage_count }
   - Errors: 400 (code already exists), 400 (invalid format), 401 (not instructor), 404 (session not found)

   **GET /api/courses/:courseId/sessions/:sessionId/codes**
   - List all codes for this session
   - Include: id, code, status, usage_count, generated_at, revoked_at
   - Show both active and revoked codes (teacher wants to see history)
   - Return 200: { data: [...] }
   - Errors: 401, 404

   **PATCH /api/courses/:courseId/sessions/:sessionId/codes/:codeId**
   - Request body: { status: "revoked" | "active" }
   - Update join_codes record: status, revoked_at (if revoking)
   - Revoking code prevents NEW joins, but students already in stay
   - Can revoke mid-session
   - Return 200: { id, code, status, revoked_at }
   - Errors: 400 (invalid status), 401, 404

2. Create service at `src/server/services/joinCode.service.ts`:
   - `generateAutoCode()` → returns unique code string (check DB for uniqueness in session)
   - `validateCodeFormat(code)` → throw APIError if invalid
   - `createJoinCode(sessionId, code, isCustom, userId)` → insert + return
   - `listJoinCodes(sessionId)` → query all codes for session
   - `revokeJoinCode(codeId)` → update status + set revoked_at

3. Code format validation:
   - Auto-generated: XXXX-XXX (uppercase, numbers, hyphens, total 7-9 chars)
   - Custom: 3-50 chars, alphanumeric + hyphens, no spaces
   - No special chars except hyphen

4. Database constraints:
   - (session_id, code) UNIQUE
   - status IN ('active', 'revoked')
   - Prevent duplicate codes via unique index

**Database Schema (reference ERD.md):**
- join_codes: id, session_id, code, is_custom, status, generated_at, revoked_at, created_by, usage_count, updated_at

**Test:**
```bash
# Generate auto code
POST /api/courses/:courseId/sessions/:sessionId/codes
Headers: { "Authorization": "Bearer ..." }
Body: { "auto_generate": true }

# Create custom code
POST /api/courses/:courseId/sessions/:sessionId/codes
Body: { "code": "MATH-101" }

# List codes
GET /api/courses/:courseId/sessions/:sessionId/codes

# Revoke code
PATCH /api/courses/:courseId/sessions/:sessionId/codes/:codeId
Body: { "status": "revoked" }
```

---

## PROMPT 6: STUDENT JOIN FLOW (CODE & LINK)

**Goal:** Implement student join via code or direct link (no authentication required).

**Copy-paste this prompt:**

---

### PROMPT 6: Student Join Flow (Code & Link)

I need to implement student join for Signal-Learn. Students join via code (e.g., MATH-101) or direct shareable link. No login required.

**Requirements:**

1. Create route handler at `src/routes/api/join-by-code/routes.ts`:

   **POST /api/join-by-code**
   - Request body: { code: "MATH-101", name?: "Alice" }
   - NO authentication required (public endpoint)
   - Validate code:
     * Lookup join_codes: WHERE code = ? AND status = 'active'
     * Lookup course_sessions: WHERE id = join_code.session_id AND is_active = TRUE
     * Lookup courses: WHERE id = course_sessions.course_id
   - Check if name required: if course.allow_anonymous = false, name is required
   - Create participants record:
     * id: UUID
     * session_id: from join_code.session_id
     * join_code_id: from join_code.id
     * join_method: 'code'
     * name: from request (or NULL if allow_anonymous)
     * join_timestamp: NOW()
     * is_active: true
   - Increment join_code.usage_count
   - Return 200: { participant_id, session_info: { course_title, instructor_name, duration_minutes, allow_anonymous } }
   - Errors: 400 (code not found), 400 (session not active), 400 (name required), 500

2. Create route handler at `src/routes/api/join-by-link/routes.ts`:

   **POST /api/join-by-link**
   - Request body: { session_code: "SIGNAL-ABC-123", name?: "Alice" }
   - Similar logic to join-by-code BUT lookup course first (via session_code)
   - session_code is unique globally (in courses table)
   - create participants with join_method: 'link' (no join_code_id)
   - Return same as join-by-code
   - Errors: 400 (session code not found), 400 (session not active), 400 (name required)

3. Create service at `src/server/services/participant.service.ts`:
   - `joinByCode(code, name)` → validate + create participant
   - `joinByLink(sessionCode, name)` → validate + create participant
   - `getSessionInfo(sessionId)` → return { course_title, instructor_name, duration_minutes }

4. Validation:
   - code: required, 3-50 chars, alphanumeric + hyphens
   - name: optional, but required if course.allow_anonymous = false, max 255 chars
   - name trimmed, no leading/trailing spaces

5. Database operations:
   - Lookup join_codes by code (case-insensitive? or case-sensitive?)
   - Lookup course_sessions by id + is_active = true
   - Lookup courses by id + deleted_at IS NULL
   - Insert participants atomically

6. Error responses (standardized):
   - 400: Code not found or revoked
   - 400: Session not active
   - 400: Name required
   - 400: Validation failed

**Database Schema (reference ERD.md):**
- join_codes: id, session_id, code, status
- course_sessions: id, course_id, is_active
- courses: id, title, allow_anonymous
- participants: id, session_id, join_code_id, join_method, name, join_timestamp, is_active

**Test:**
```bash
# Join by code
POST /api/join-by-code
Body: { "code": "MATH-101", "name": "Alice" }

# Join by link
POST /api/join-by-link
Body: { "session_code": "SIGNAL-ABC-123", "name": "Bob" }
```

---

## PROMPT 7: STATUS EVENTS (POLLING & DASHBOARD)

**Goal:** Implement status event creation (student taps) and real-time dashboard polling.

**Copy-paste this prompt:**

---

### PROMPT 7: Status Events (Student Taps) & Dashboard Polling

I need to implement status events for Signal-Learn. Students tap Red/Yellow/Green buttons, events are logged, and teacher dashboard polls for real-time updates.

**Requirements:**

1. Create route handler at `src/routes/api/status-events/routes.ts`:

   **POST /api/status-events**
   - Request body: { participant_id: "uuid", status: "red" | "yellow" | "green" }
   - NO authentication required (public endpoint)
   - Rate limiting: ADAPTIVE (based on server load)
   - Per-participant limit: max 1 request/sec, fallback 60/min
   - Validate:
     * participant exists + is_active = true
     * session is_active = true
     * status is valid enum
   - Create status_events record:
     * id: UUID
     * participant_id: from request
     * status: from request
     * triggered_at: NOW()
     * auto_reset_at: NOW() + 5 minutes
     * is_reset: false
   - Update session_dashboard (denormalized counts):
     * ATOMIC: Use optimistic locking (version field)
     * Recalculate: red_count, yellow_count, green_count (count all active events in last 5 min)
     * Update: last_updated = NOW(), version = version + 1
   - Return 200: { event_id, participant_id, status, triggered_at, auto_reset_at }
   - Errors: 400 (invalid status), 404 (participant not found), 429 (rate limited)

2. Create route handler at `src/routes/api/courses/[courseId]/sessions/[sessionId]/status-summary/routes.ts`:

   **GET /api/courses/:courseId/sessions/:sessionId/status-summary**
   - Query this endpoint every 2-5 seconds for dashboard updates
   - Requires Authorization header (teacher only)
   - Query session_dashboard (NOT StatusEvent table) for fast reads
   - Return 200: { session_id, red_count, yellow_count, green_count, total_participants, active_participants, elapsed_seconds, last_updated }
   - HTTP caching: Cache-Control: "no-cache, must-revalidate"
   - Execution target: < 5ms (direct query on session_dashboard)
   - Errors: 401 (unauthorized), 404 (session not found)

3. Create service at `src/server/services/statusEvent.service.ts`:
   - `createStatusEvent(participantId, status)` → insert event
   - `updateDashboard(sessionId)` → query counts, update session_dashboard with optimistic locking
   - Retry logic for optimistic locking conflicts (if version mismatch, retry)

4. Polling strategy:
   - Frontend polls every 2-5 seconds
   - Use TanStack Query OR custom useInterval hook
   - Store results in Zustand dashboard store
   - No WebSocket yet (Phase 2)

5. Auto-reset logic (future):
   - Events with is_reset = false AND auto_reset_at < NOW() should be marked is_reset = true
   - Can be batch job OR trigger on dashboard query
   - For MVP, keep simple: don't explicitly reset, just filter in queries

6. Database operations:
   - StatusEvent: append-only log (NEVER update/delete)
   - SessionDashboard: mutable (updated on every status event via optimistic locking)
   - Optimistic locking: check version before UPDATE, increment on success

7. Rate limiting:
   - Implement adaptive rate limiting based on request volume
   - Per-participant sliding window: 60 requests/min max
   - Return 429 with Retry-After header if exceeded

**Database Schema (reference ERD.md):**
- status_events: id, participant_id, status, triggered_at, auto_reset_at, is_reset, created_at
- session_dashboard: id, session_id, red_count, yellow_count, green_count, total_participants, active_participants, elapsed_seconds, last_updated, version

**Denormalized Query (for dashboard):**
```sql
SELECT red_count, yellow_count, green_count, total_participants, elapsed_seconds
FROM session_dashboard
WHERE session_id = ?;
-- Result: < 5ms direct lookup
```

**Test:**
```bash
# Student taps status (high frequency)
POST /api/status-events
Body: { "participant_id": "...", "status": "red" }

# Teacher polls dashboard (every 3 sec)
GET /api/courses/:courseId/sessions/:sessionId/status-summary
Headers: { "Authorization": "Bearer ..." }
```

---

## PROMPT 8: TEACHER DASHBOARD UI

**Goal:** Build teacher dashboard using React + Zustand + shadcn/ui with real-time polling.

**Copy-paste this prompt:**

---

### PROMPT 8: Teacher Dashboard UI

I need to build the teacher dashboard for Signal-Learn using Tanstack Start Latest + Zustand Latest+ shadcn/ui Latest. Dashboard displays real-time Red/Yellow/Green counts, participant info, and code management.

**Requirements:**

1. Create Zustand store at `src/stores/dashboardStore.ts`:
   - State: activeSessions (Map), selectedSessionId, isPolling, pollingError
   - Actions: updateSessionStatus, selectSession, startPolling, stopPolling, clearSessions
   - Use subscribeWithSelector middleware

2. Create custom hook at `src/hooks/usePolling.ts`:
   - Hook: usePolling(sessionId, interval = 3000)
   - Fetch dashboard every N seconds via GET /api/courses/:courseId/sessions/:sessionId/status-summary
   - Update Zustand store on success
   - Handle errors gracefully
   - Stop polling when sessionId = null or isPolling = false

3. Build components:

   **TeacherDashboard.tsx**
   - Layout: Navbar + Session selector + Status summary + Code management section
   - Session selector: List of active courses/sessions
   - On select: trigger usePolling, update selectedSessionId
   - Display: isPolling indicator, pollingError message if any

   **StatusSummary.tsx**
   - 3-column card layout: Red | Yellow | Green
   - Each card shows:
     * Status name (RED/YELLOW/GREEN)
     * Large count (text-4xl, bold)
     * Label (Need Help / Have Questions / Doing Well)
     * Background color: red-100/red-600, yellow-100/yellow-600, green-100/green-600
   - Also show: total_participants, elapsed_seconds (timer)

   **CodeManagement.tsx**
   - Display active/revoked codes for current session
   - Buttons: "Generate Code" → opens modal
   - List codes: code string, usage count, status, revoke button
   - Revoke button: confirm dialog, then PATCH to revoke

   **CodeGeneratorModal.tsx**
   - Modal: "Generate Join Code"
   - Radio: Auto-generate OR Custom code
   - If custom: input field (3-50 chars validation)
   - Button: "Generate"
   - POST /api/courses/:courseId/sessions/:sessionId/codes
   - Close on success, show toast notification
   - Show error if code already exists

   **SessionTimer.tsx**
   - Display elapsed time since session start
   - Format: HH:MM:SS
   - Update every 1 second
   - Show "Start Session" / "End Session" buttons

4. Use shadcn/ui components:
   - Button, Card, Dialog, Input, Select, Badge, Toast

5. Error handling:
   - Show toast on API errors
   - Show error message if polling fails (retry automatically)
   - Disable buttons if loading

6. Styling:
   - Tailwind CSS
   - Production-grade: proper spacing, colors, typography
   - Responsive: works on desktop + tablet
   - No AI slop (clean, minimal design)

7. State management:
   - Use Zustand for dashboard state
   - Use React Query or manual fetch for API calls
   - Debounce rapid status updates

**File Structure:**
```
src/components/teacher/
├─ TeacherDashboard.tsx
├─ StatusSummary.tsx
├─ CodeManagement.tsx
├─ CodeGeneratorModal.tsx
└─ SessionTimer.tsx

src/stores/
└─ dashboardStore.ts

src/hooks/
└─ usePolling.ts
```

**Test:**
```
1. Teacher logs in
2. Navigate to /teacher/dashboard
3. Select active session
4. Verify Real-time counts update every 3 seconds
5. Generate code (auto and custom)
6. Revoke code
7. Verify UI updates on all actions
```

---

## PROMPT 9: STUDENT STATUS BOARD UI

**Goal:** Build student-facing UI for joining session and tapping status.

**Copy-paste this prompt:**

---

### PROMPT 9: Student Status Board UI

I need to build the student-facing UI for Signal-Learn. Students join via code/link and tap Red/Yellow/Green status buttons.

**Requirements:**

1. Create pages:

   **JoinScreen.tsx** (/session/:sessionCode)
   - Display: Session info (course title, instructor, duration)
   - Join code input: if accessed via direct link or needs code entry
   - Optional name input (if course.allow_anonymous = false)
   - "Join" button
   - POST /api/join-by-code OR /api/join-by-link
   - On success: redirect to /session/{participant_id}/status-board
   - Show error if code invalid, session not active, etc.

   **StatusBoard.tsx** (/session/{participant_id}/status-board)
   - Display: Course title, instructor, session info
   - Countdown timer: 5-minute auto-reset
   - 3 large tap buttons: Green ✓ | Yellow ? | Red !
   - Large text, high contrast colors
   - Selected status highlighted (ring, border)
   - On tap:
     * POST /api/status-events { participant_id, status }
     * Show loading state
     * Highlight selected status
     * Reset after 5 minutes (countdown)
   - Display current status if set
   - Show error if POST fails

2. Create components:

   **JoinCodeInput.tsx**
   - Input field: code entry
   - Optional: name input
   - Validation: code format (3-50 chars, alphanumeric + hyphens)
   - Error display: inline error messages
   - Button: "Join Session"
   - Loading state during POST

   **StatusButtons.tsx**
   - 3 large buttons: Red, Yellow, Green
   - Button size: w-24 h-24 rounded-full (or similar)
   - Icons: ! (red), ? (yellow), ✓ (green)
   - Colors: red-500, yellow-400, green-500
   - On click: handleStatusTap → POST /api/status-events
   - Show loading spinner during request
   - Disable buttons if loading or error
   - Selected status: ring-4 or border highlight

   **CountdownTimer.tsx**
   - Display countdown: "Resetting in 4:32"
   - Update every 1 second
   - When countdown reaches 0: reset UI (clear selected status)
   - On new tap: reset countdown

   **SessionInfo.tsx**
   - Display: Course title, instructor name, remaining time (from duration_minutes)
   - Responsive layout

3. Use shadcn/ui components:
   - Button, Input, Card, Badge, LoadingSpinner

4. Error handling:
   - Show toast on errors
   - Retry button on network errors
   - Display code invalid message
   - Display session not active message

5. Styling:
   - Tailwind CSS
   - Large, high-contrast for classroom use
   - Mobile-first (works on phones + tablets)
   - No ai slop

6. Hooks:
   - useCountdown: countdown timer logic
   - useStatusTap: handle status button taps with loading

**File Structure:**
```
src/components/student/
├─ JoinScreen.tsx
├─ StatusBoard.tsx
├─ JoinCodeInput.tsx
├─ StatusButtons.tsx
├─ CountdownTimer.tsx
└─ SessionInfo.tsx
```

**Test:**
```
1. Student joins via code: /session?code=MATH-101
2. Enters name (if required)
3. Clicks "Join"
4. Navigated to status board
5. Taps Green → visible immediately, countdown starts
6. After 5 min: status resets
7. Can tap again
8. Refresh doesn't lose status (until session ends or 5 min)
```

---

## PROMPT 10: HISTORICAL REVIEW & ADMIN STATS

**Goal:** Implement session summary review for teachers and admin stats dashboard.

**Copy-paste this prompt:**

---

### PROMPT 10: Historical Review & Admin Stats

I need to implement session review for teachers and admin stats dashboard for Signal-Learn.

**Requirements:**

1. Create route handler at `src/routes/api/courses/[courseId]/sessions/[sessionId]/summary/routes.ts`:

   **GET /api/courses/:courseId/sessions/:sessionId/summary**
   - Requires Authorization header (teacher only)
   - Lookup session_summaries record (immutable, computed at session end)
   - Return 200: {
       session_id, total_participants, participants_via_link, participants_via_code,
       codes_generated_count, codes_revoked_count,
       final_red_count, final_yellow_count, final_green_count,
       duration_seconds, created_at
     }
   - Include breakdown: percentage of students in each status
   - Errors: 401 (unauthorized), 404 (summary not found)

2. Create admin route handler at `src/routes/api/admin/stats/routes.ts`:

   **GET /api/admin/stats**
   - Requires Authorization header + role = 'admin'
   - Query parameters: { period?: 'day' | 'week' | 'month', limit?: 10 }
   - Return aggregate stats:
     * Total teachers
     * Total sessions this period
     * Total participants this period
     * Avg participants per session
     * Red/Yellow/Green distribution
   - Example: { total_teachers: 42, total_sessions: 156, total_participants: 3240, avg_per_session: 20.77, status_distribution: { red: 15%, yellow: 25%, green: 60% } }
   - Errors: 401 (not admin)

3. Create admin route handler at `src/routes/api/admin/courses/routes.ts`:

   **GET /api/admin/courses**
   - Requires Authorization header + role = 'admin'
   - List all courses (soft-deleted included)
   - For each course: { id, title, status, instructor_email, created_at, session_count, avg_participants }
   - Query parameters: { instructor_id?: uuid, status?: 'draft|active|completed', limit?: 20, offset?: 0 }
   - Paginated response
   - Errors: 401

4. Create UI components:

   **SessionReview.tsx**
   - Teacher views past session summary
   - Display:
     * Course title, session date/time, duration
     * Total participants
     * Breakdown: via code vs via link
     * Status distribution: X% Red, Y% Yellow, Z% Green
     * Codes used: list of codes + usage count
   - Visual: cards, charts (simple bar chart if using recharts)
   - Route: /teacher/session/:sessionId/review

   **AdminDashboard.tsx** (optional for MVP)
   - Display: total teachers, total sessions, participants, status distribution
   - Charts: sessions over time, participant trend
   - List: top courses by participation
   - Route: /admin/dashboard

5. Services at `src/server/services/stats.service.ts`:
   - `getSessionSummary(sessionId)` → query session_summaries
   - `getAdminStats(period)` → aggregate queries
   - `getCourseStats(courseId)` → session count, avg participants

6. Database queries:
   - session_summaries: immutable, one per session, created at session end
   - Aggregations: COUNT, SUM, AVG on session_summaries
   - No real-time updates needed (historical data)

**Database Schema (reference ERD.md):**
- session_summaries: id, session_id, total_participants, participants_via_link, participants_via_code, codes_generated_count, codes_revoked_count, final_red_count, final_yellow_count, final_green_count, duration_seconds, created_at

**Test:**
```bash
# Get session summary (teacher)
GET /api/courses/:courseId/sessions/:sessionId/summary
Headers: { "Authorization": "Bearer ..." }

# Get admin stats (admin only)
GET /api/admin/stats?period=month
Headers: { "Authorization": "Bearer ..." }
```

---

## NEXT STEPS AFTER BUILD

1. **Deploy to Cloudflare Workers** (or Vercel, Railway)
   - Set environment variables
   - Run migrations on production database
   - Test all endpoints

2. **Iterate on Feedback**
   - Collect user feedback from initial testing
   - Document issues/feature requests
   - Create ITERATION_PROMPT.md for next sprint

3. **Monitor Performance**
   - Check dashboard query latency (target: < 5ms)
   - Monitor API error rates
   - Track polling latency (target: 2-5 sec)

4. **Security Hardening** (Phase 2)
   - CORS configuration
   - Rate limiting tuning
   - Input sanitization review
   - SQL injection testing (Drizzle prevents this, but verify)

---

## TROUBLESHOOTING TIPS

**Database Connection Issues:**
- Verify .env.local has correct DB_HOST, DB_USER, DB_PASSWORD, DB_NAME
- Test connection: `mysql -h <host> -u <user> -p<password> <database>`

**JWT Token Invalid:**
- Check JWT_SECRET is set in .env.local
- Verify token hasn't expired (1 hour)
- Check Authorization header format: "Bearer {token}"

**API 404 Errors:**
- Verify route files are in correct directory: `src/routes/api/...`
- Check file naming: routes.ts (lowercase)
- Restart dev server after creating new files

**Polling Not Updating:**
- Check Authorization header is present
- Verify session_id is correct
- Check browser console for fetch errors
- Verify server is returning status-summary endpoint

**Rate Limiting Errors (429):**
- Reduce polling frequency (increase interval)
- Check if participant_id is correct
- Implement exponential backoff in client

---

## SUMMARY

✅ **10 Prompts, Sequential Execution**  
✅ **Production-Grade Code, No Shortcuts**  
✅ **Full Test Coverage**  
✅ **Ready for Deployment**  
✅ **Iterable (feedback loop built-in)**

Each prompt builds on the previous one. Complete them in order, test after each, then move forward.

**Estimated Timeline:** 20-30 hours total (First 20 Hours methodology)

---

**Build Status:** ✅ Ready to Vibe Code  
**Last Updated:** August 2026
