# 2026-07-17 — Google Meet / Classroom URLs on `lesson_plans` (Sprint A2 / P1-UX-3)

Branch: `feat/p1-ux-3-meet-classroom`
Worktree: `.worktrees/feat-p1-ux-3-meet-classroom`

## Why

The UI/UX feedback PDF §3 (Virtual Classroom Integration) asks teachers to be able to "embed or link out to Google Meet sessions and Google Classroom environments directly from their lesson uploads." Until now, lesson plans had no place to carry those URLs.

## Changes

### `server/src/database/migrations/knex/2026071700000008_lesson_plan_virtual_classroom.js` (new)

Adds two optional columns to `lesson_plans`:

- `meet_url TEXT` — Google Meet session URL for the lesson's live class.
- `classroom_url TEXT` — Google Classroom link for the assignment/unit.

Both default to NULL and are added via `ALTER TABLE … ADD COLUMN`. Each ADD is guarded by try/catch so re-applying the migration against a DB that already has the columns is a no-op. The `down` migration drops both columns (SQLite ≥ 3.35, supported by `better-sqlite3`).

### `server/src/utils/validateUrl.js` (new)

Tiny URL-shape validator used by the controller:

- `isSafeHttpUrl(value)` — returns true for null / undefined / empty (means "no link") and for strings that parse via WHATWG `URL` with `http`/`https` protocol. Rejects everything else including `javascript:`, `data:`, `file:`, `ftp:`, unparseable strings, and non-strings.
- `normalizeHttpUrl(value)` — trims and returns the canonical URL, or `null` when invalid. Used by the controller so a bad paste gets persisted as `null` rather than junk.

### `server/src/controllers/teacher.controller.js` (modified)

- POST `/api/teacher/lesson-plans` now accepts `meet_url` and `classroom_url`. Each is run through `normalizeHttpUrl`; an invalid non-empty value returns 400 with the field name in the error.
- INSERT statement extended to write both columns; existing rows are unaffected (NULL).
- OpenAPI block above the route documents both fields with examples.
- GET `/api/teacher/lesson-plans` already uses `SELECT lp.*` so the new columns ride along automatically.

### `client/src/pages/teacher/TeacherTools.tsx` (modified)

Lesson-plan modal:

- Two new `<input type="url">` fields with placeholders (`https://meet.google.com/...` and `https://classroom.google.com/...`).
- The lesson-plan submission now passes `meet_url` and `classroom_url` through the `createLessonPlan` call.

### `server/tests/validate-url.test.js` (new, 10 specs)

Vitest unit tests for the two helpers:

- `isSafeHttpUrl`: null/undefined/empty valid; non-strings invalid; http/https valid; `javascript:`/`ftp:`/`file:`/`data:` invalid; unparseable strings invalid.
- `normalizeHttpUrl`: trims and returns canonical URL; returns `null` for invalid input (not the original string); handles null/undefined/empty/non-string.

## Out of scope (follow-up)

- Student-side "Join Class" surface. Per PDF §9, this lives inside the **Course Scheme/Plan** page ("Virtual Class Room button" on every lesson row) — that's P1-UX-7. The lesson_plans API now carries the data; P1-UX-7 renders it.
- A PUT/PATCH endpoint on lesson plans. TeacherTools currently does not edit lesson plans in place; if needed, the same validator applies.
- Rate-limiting the URL fields or logging them in audit_logs. Today the columns ride the existing audit story.

## Verification

- `npx vitest run tests/validate-url.test.js` → 10 passed (15 ms).
- `npx vitest run` → **28 passed**, 1 pre-existing failure (`paynow-webhook.test.js` "paid_amount undefined" — same failure as on `dev`; not caused by this diff).
- `node src/database/init.js` against a fresh `data/school.db` → both columns added (`PRAGMA table_info(lesson_plans)` shows `meet_url:TEXT` and `classroom_url:TEXT`).
- Re-running init on the already-migrated DB → "already up to date" — migration is idempotent.
- `node --check` clean on every modified file.
