All Sessions

Plan: Instructor-Only /roster Page with Student Photos

February 17, 20266:46 PM7:59 PM(1h 13m)8 turns
Model: claude-opus-4-6, claude-sonnet-4-5-20250929
Read20Bash20Grep14TaskUpdate14Edit10TaskCreate7Write4Glob3Task2
6:46 PM

Implement the following plan:

Plan: Instructor-Only /roster Page with Student Photos

Context

The instructor wants a page to help learn student names by showing enrolled students with their university photos. Photos have been extracted from the Siebel School roster to data/spring2026/photos/{uin}.jpg. The DB has enrollment data but no UINs — the roster CSV (data/spring2026/enrollment/roster.csv) bridges the gap via email.

Files to Create/Modify

FileAction
apps/api/src/routes/admin/roster.tsCreate — API routes for roster data + photo serving
apps/api/src/index.tsModify — register new router
apps/web/app/roster/page.tsxCreate — server component page

Step 1: API Route (apps/api/src/routes/admin/roster.ts)

Two endpoints, both instructor-only (using requireRole("instructor")) to protect student photos:

GET /api/admin/roster — returns active student enrollments enriched with UINs:

  • Query enrollments table (active students, current semester) joined with users
  • Parse data/spring2026/enrollment/roster.csv to build email→UIN map
  • Check which UINs have photos in data/spring2026/photos/
  • Return { students: [{ name, email, uin, hasPhoto }] } sorted by name
  • Use git-root-dir to locate data directory (existing pattern from reading-loader.ts, ingest-content.ts)

GET /api/admin/roster/photo/:uin — serves photo JPEG:

  • Read data/spring2026/photos/{uin}.jpg from disk
  • Return with Content-Type: image/jpeg and cache headers
  • 404 if file doesn't exist
  • Validate UIN is digits-only to prevent path traversal

Step 2: Register Router (apps/api/src/index.ts)

Add import and app.route("/api/admin/roster", adminRosterRouter).

Step 3: Web Page (apps/web/app/roster/page.tsx)

Server-rendered, no client component needed:

  • Auth check → redirect to login if not authenticated
  • Fetch from /api/admin/roster via serverApiFetch
  • Render a grid of student cards, each showing:
    • Photo via API URL {API_URL}/api/admin/roster/photo/{uin} (or /icon.png fallback)
    • Student name
    • Email as mailto: link
  • Responsive grid layout

Verification

# Page should require auth (redirects to login)
curl -sk -o /dev/null -w "%{http_code}" https://[local-hostname]/roster
# After logging in as instructor, page shows student photo grid

If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: ~/.claude/projects/-Users-challen-www-usingandunderstanding-ai/cb6d9f71-f6ac-4ebd-9609-ef73d4bece58.jsonl

I'll implement this plan. Let me start by reading the key files I need to understand the existing patterns.

Good. Now let me look at existing admin route patterns and the web page patterns.

Let me look at one more pattern - how git-root-dir is used to find files, and check the enrollments schema.

Now let me check the db exports and look at an existing web page for layout patterns.

Now I have all the patterns I need. Let me check one more thing - the @/auth import path and an existing page layout for reference.

Now I have everything I need. Let me create all three files.

Now let me check the API_URL env var pattern to understand how to build the photo URL in the web page.

The photo URLs will be served from the API, so in the server component I need to use process.env.API_URL for the image src. But since images are rendered in the browser, I need the public API URL. Let me check how this is done elsewhere.

Since the photo <img> tags are rendered in the browser, they need the public API URL. I'll use process.env.API_URL in the server component (which runs on the server during SSR) to build the URLs. Now let me create the web page.

Now let me verify everything compiles.

Quick TS fix needed.

All passing. Let me verify the dev server is running and test the endpoints.

The page correctly redirects unauthenticated users (307 → login). Let me also verify the API endpoint rejects unauthenticated requests.

API correctly returns 401 for unauthenticated requests. Everything is working:

Summary of changes:

  1. apps/api/src/routes/admin/roster.ts (new) — Two instructor-only API endpoints:

    • GET /api/admin/roster — Returns active student enrollments enriched with UINs from the roster CSV, including hasPhoto flag
    • GET /api/admin/roster/photo/:uin — Serves student photos as JPEG with cache headers and path traversal protection (digits-only validation)
  2. apps/api/src/index.ts (modified) — Registered the new router at /api/admin/roster

  3. apps/web/app/roster/page.tsx (new) — Server-rendered instructor-only page showing a responsive grid of student cards with photos, names, and email links. Redirects to login if unauthenticated, shows empty state if no permission.

Read11Grep10Bash9TaskUpdate6Glob3TaskCreate3Edit3Write2Task1
6:50 PM

Can we write an e2e test for this functionality? It might require seeding some fake students and I guess we'd only be able to test the fallback photos unless you want to create some fake photos :-).

Let me explore the E2E testing setup to understand the patterns for auth, seeding, and test structure.

Now I have the full picture. Let me check the E2E seeding endpoint to see how to add enrollment seeding, and look at an existing test file for the exact patterns.

Now let me look at an existing test that uses instructor auth, and check the cleanup code to see if enrollments are cleaned up.

The E2E cleanup doesn't handle enrollments yet, so I'll need to add enrollment seeding and cleanup. Let me also check the currentSemester value and how the roster API uses it.

Good. Now I have a clear picture. I need to:

  1. Add an enrollment seeding E2E endpoint
  2. Add enrollment cleanup to the E2E cleanup code
  3. Add data-testid attributes to the roster page
  4. Create the test file
  5. Add the project to playwright config

Let me also check the @repo/db export to make sure enrollments is accessible.

Good, enrollments is exported via @repo/db/schema (and re-exported from @repo/db). Now let me implement everything. I'll start with adding data-testid attributes to the roster page, then the E2E endpoint changes, then the test.

Now I need to add enrollment seeding and cleanup to the E2E routes. I'll add a POST /api/e2e/enrollments/seed endpoint and add enrollment cleanup to the existing cleanup functions.

Now add enrollment cleanup to both cleanup functions. First the per-nonce cleanup:

Now the cleanup-all function:

Now add the enrollment seeding endpoint. I'll add it before the assessment seeding section:

Now create the test file:

Let me verify TypeScript still passes, then run the tests.

All clean. Now let's run the E2E test:

The count test fails because there are already real enrolled students in the dev database. The test students are added on top of those. Let me fix the test to account for this.

All 12 tests pass. Here's a summary of what was added:

E2E infrastructure changes:

  • apps/api/src/routes/e2e.ts — Added POST /api/e2e/enrollments/seed endpoint that creates a test enrollment for a given user. Also added enrollments cleanup to both cleanup and cleanup-all handlers so test enrollments don't leak.

Test file:

  • testing/e2e/tests/roster.spec.ts — 7 tests across two groups:
    • Access Control (3 tests): unauthenticated redirect, student sees empty page, instructor can access
    • Student Grid (4 tests): enrolled students show cards, count is displayed, names are visible, fallback photo (/icon.png) for students without UINs in the roster CSV

Supporting changes:

  • apps/web/app/roster/page.tsx — Added data-testid attributes throughout
  • testing/e2e/playwright.config.ts — Registered roster test project

Since test users aren't in the real roster CSV, they naturally exercise the fallback photo path. Real photos are only served for students whose email matches a UIN in the CSV that has a corresponding .jpg file.

Read8TaskUpdate8Edit6Grep4TaskCreate4Bash3Write2Task1
7:11 PM

Your task

First, run git status, git diff, and git log --oneline -10 to understand the current state.

Then add and commit all modified files with a descriptive message. Break the changes into multiple commits if appropriate. If there are untracked files, ask if they should be added or added to the appropriate .gitignore file. The user may also want to take over at this point, so provide an abort option as well. Always check changes for unnecessary debugging statements or logging that might have been accidentally included. Prompt the user if you find any, and offer to remove each one by one. Do not use interactive git staging (git add -p) as it does not work.

I'll check the current git status and review the changes before committing.

Now let me check the new files to ensure there's no debugging code:

Bash4
7:11 PM

Ah, yes, let's .gitignore the raw photos. They do need to get copied into the container during deployment, but they should not be added to Git since they are sensitive student information.

Good call. Let me add that to .gitignore.

data/spring2026/photos/ is no longer showing as untracked. Used data/*/photos/ to cover any semester. Ready to commit when you'd like — want me to go ahead?

Read1Edit1Bash1
7:15 PM

Go for it!

Committed as 890f244. Clean working tree on the photos branch.

Bash3
7:52 PM

Great! Now, can you design a flashcard system to help me learn the student names? It can be launched from the roster page.

7:58 PM

Great! Now, can you design a flashcard system to help me learn the student names? It can be launched from the roster page.

7:58 PM

Continue!