Plan: Instructor-Only /roster Page with Student Photos
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
| File | Action |
|---|---|
apps/api/src/routes/admin/roster.ts | Create — API routes for roster data + photo serving |
apps/api/src/index.ts | Modify — register new router |
apps/web/app/roster/page.tsx | Create — 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.csvto 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-dirto locate data directory (existing pattern fromreading-loader.ts,ingest-content.ts)
GET /api/admin/roster/photo/:uin — serves photo JPEG:
- Read
data/spring2026/photos/{uin}.jpgfrom disk - Return with
Content-Type: image/jpegand 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/rosterviaserverApiFetch - Render a grid of student cards, each showing:
- Photo via API URL
{API_URL}/api/admin/roster/photo/{uin}(or/icon.pngfallback) - Student name
- Email as
mailto:link
- Photo via API URL
- 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:
-
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, includinghasPhotoflagGET /api/admin/roster/photo/:uin— Serves student photos as JPEG with cache headers and path traversal protection (digits-only validation)
-
apps/api/src/index.ts(modified) — Registered the new router at/api/admin/roster -
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.
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:
- Add an enrollment seeding E2E endpoint
- Add enrollment cleanup to the E2E cleanup code
- Add
data-testidattributes to the roster page - Create the test file
- 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— AddedPOST /api/e2e/enrollments/seedendpoint that creates a test enrollment for a given user. Also addedenrollmentscleanup to bothcleanupandcleanup-allhandlers 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— Addeddata-testidattributes throughouttesting/e2e/playwright.config.ts— Registeredrostertest 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.
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:
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?
Go for it!
Committed as 890f244. Clean working tree on the photos branch.
Great! Now, can you design a flashcard system to help me learn the student names? It can be launched from the roster page.
Great! Now, can you design a flashcard system to help me learn the student names? It can be launched from the roster page.
Continue!