some planning and issue finding

This commit is contained in:
2026-05-04 17:15:53 +02:00
parent 08cb668bab
commit 650f3456cb
7 changed files with 237 additions and 269 deletions

View File

@@ -1,161 +0,0 @@
# Implementation Plan: Attendance Tracking Tool (Continuation)
**Objective:** Complete the backend API, scaffold the SvelteKit frontend, implement all frontend views based on the provided design handoff, and set up deployment manifests. This plan picks up from where the `.worktrees/feature-tutortool` workspace left off (Tasks 1-8 completed).
**Scope:**
- Complete backend APIs for attendance, notes, and exports.
- Serve SvelteKit SPA fallback via Axum.
- Scaffold SvelteKit frontend.
- Implement UI pages for Admin tools (Dashboard, Courses, Rooms, Sessions, Attendance, Notes, Export) and Student Check-in.
- Configure local development (Makefile, Docker Compose) and K8s manifests.
---
## 1. Backend Completion
### Task 9: Admin Attendance & Notes APIs
**Files to Create/Modify:**
- `backend/src/routes/attendance.rs`
- `backend/src/routes/notes.rs`
**Implementation Steps:**
- Add `POST /api/admin/slots/:id/attendance` (manual entry) and `DELETE /api/admin/slots/:slot_id/attendance/:student_id`.
- Add `GET /api/admin/sessions/:id/attendance` (per-week matrix) and `GET /api/admin/students/:id/attendance`.
- Add `PUT /api/admin/slots/:slot_id/notes/:student_id` (upsert note).
- Add `GET /api/admin/slots/:slot_id/notes` and `GET /api/admin/students/:id/notes`.
- Write corresponding unit tests.
### Task 10: Export API
**Files to Create/Modify:**
- `backend/src/routes/export.rs`
**Implementation Steps:**
- Add `GET /api/admin/export/session/:id/csv` and `/md` (merged per-session weekly attendance).
- Add `GET /api/admin/export/course/:id/csv` and `/md` (full course matrix with Bonus points calculation: +3 if unexcused absences <= 1).
- Add `GET /api/admin/backup` (using `VACUUM INTO '/tmp/backup-<timestamp>.sqlite'` then streaming as `application/octet-stream`).
- Ensure all endpoints verify `TutorClaims` and course access.
### Task 11: Static File Serving & Route Assembly
**Files to Modify:**
- `backend/src/main.rs`
- `backend/src/routes/mod.rs`
**Implementation Steps:**
- Merge `attendance`, `notes`, and `export` routers in `routes/mod.rs`.
- Configure `tower_http::services::ServeDir` in `main.rs` to serve the SvelteKit static build.
- Set up `ServeFile::new(format!("{static_dir}/index.html"))` as the SPA fallback service.
---
## 2. Frontend Development
### Task 12: Scaffold SvelteKit Frontend
**Files to Create/Modify:**
- `frontend/package.json`, `svelte.config.js`, `vite.config.ts`, `src/app.html`, `src/lib/types.ts`, `src/lib/api.ts`, `src/lib/auth.ts`
**Implementation Steps:**
- Initialize SvelteKit with `@sveltejs/adapter-static` configured for SPA fallback.
- Setup Vite proxy `/api` -> `http://localhost:3000`.
- Create TypeScript types mirroring the backend database models.
- Implement API client fetch wrapper (`api.ts`).
- Set up Svelte store for JWT auth (`auth.ts`).
### Task 13: Login Page & Admin Auth Guard
**Files to Create/Modify:**
- `frontend/src/routes/login/+page.svelte`
- `frontend/src/routes/admin/+layout.svelte`
**Implementation Steps:**
- Implement the tutor login form and API integration.
- Protect `/admin` routes using an `onMount` check redirecting unauthenticated users to `/login`.
### Task 14: Dashboard & Slot Management
**Files to Create/Modify:**
- `frontend/src/routes/admin/+page.svelte`
**Implementation Steps:**
- Display all sessions/slots.
- Implement toggles for slot status (`closed`, `open`, `locked`).
- Display check-in link and copy button when a slot is `open`.
### Task 15: Courses & Students UI
**Files to Create/Modify:**
- `frontend/src/routes/admin/courses/+page.svelte`
**Implementation Steps:**
- List courses and forms to create new courses.
- Per-course student management: list students, add individual student, import from CSV, and delete students.
### Task 16: Room Layout Editor
**Files to Create/Modify:**
- `frontend/src/lib/RoomCanvas.svelte`
- `frontend/src/routes/admin/rooms/+page.svelte`
**Implementation Steps:**
- Implement SVG-based `RoomCanvas.svelte` supporting interactive (draggable/editable) mode, student check-in mode, and tutor notes mode.
- Build room management UI: list rooms, create rooms, and edit JSON room layouts visually.
### Task 17: Sessions & Slots UI
**Files to Create/Modify:**
- `frontend/src/routes/admin/sessions/+page.svelte`
**Implementation Steps:**
- Form to create sessions (course, week_nr, date).
- Form to add slots within a session (room, tutor, start_time, end_time).
### Task 18: Student Check-in Page
**Files to Create/Modify:**
- `frontend/src/routes/s/[code]/+page.svelte`
**Implementation Steps:**
- Fetch slot info. If no identity cookie exists, show name dropdown filtered to the course.
- Display `RoomCanvas` indicating free/occupied seats.
- Handle FCFS seat locking API calls (`POST /api/checkin`) and update UI.
- Support read-only mode for locked/closed slots.
### Task 19: Attendance & Notes UI
**Files to Create/Modify:**
- `frontend/src/routes/admin/attendance/+page.svelte`
- `frontend/src/routes/admin/notes/+page.svelte`
**Implementation Steps:**
- Build matrix tables (per-week and per-student) for manual attendance marking/removal.
- Build notes UI utilizing `RoomCanvas` for clicking on seats to leave inline text notes.
### Task 20: Export UI
**Files to Create/Modify:**
- `frontend/src/routes/admin/export/+page.svelte`
**Implementation Steps:**
- Provide buttons to download weekly CSV/Markdown, full course matrix, and SQLite backup directly from the admin interface.
---
## 3. DevOps & Deployment
### Task 21: Local Dev Environment
**Files to Create/Modify:**
- `Makefile`
- `docker-compose.yml`
**Implementation Steps:**
- Write `Makefile` with commands: `dev`, `dev-backend`, `dev-frontend`, `test`, `build`, `compose-up`.
- Create `docker-compose.yml` for testing the production image locally using SQLite mounted via volume.
### Task 22: Dockerfile & K8s Manifests
**Files to Create/Modify:**
- `Dockerfile`
- `k8s/deployment.yaml`, `k8s/service.yaml`, `k8s/ingress.yaml`, `k8s/pvc.yaml`, `k8s/cronjob.yaml`
**Implementation Steps:**
- Write a 3-stage `Dockerfile` (frontend build, backend build, alpine + sqlite runtime).
- Write `pvc.yaml` for SQLite persistent storage.
- Write `deployment.yaml`, `service.yaml`, and `ingress.yaml` (`tutor.puchstein.dev`).
- Write `cronjob.yaml` running at 3 AM daily, executing `sqlite3 /data/attendance.db "VACUUM INTO '/data/backup-$(date +%F).sqlite'"` and pruning files older than 7 days.
---
## Verification Strategy
1. **Unit Tests:** Execute `cargo test` in `backend/` to verify all new endpoints (Tasks 9-11).
2. **End-to-End Test:** Start `make dev` and manually verify all critical paths: tutor login, session/slot creation, student check-in with cookie persistence, FCFS seat collision handling, manual attendance, and exporting.
3. **Deployment Test:** Run `make compose-up` to ensure the built Docker container operates as expected, serving Svelte SPA fallback via Axum properly.

View File

@@ -1,39 +0,0 @@
# Demo Preparation & Seed Data Plan
## Objective
Create an isolated, reproducible demo environment for presenting TutorTool on a Surface Pro 5 (Arch Linux) using Docker. This includes a robust set of seed data to simulate a live application state, which can also be utilized for local end-to-end testing.
## Key Files & Context
- **Environment**: `docker-compose.yml` (existing)
- **Database**: SQLite (`data/attendance.db`)
- **New Files**:
- `backend/migrations/demo_seed.sql`: A standalone SQL script containing isolated test data.
- **Modified Files**:
- `Makefile`: Update to include a `seed-demo` target for easy execution.
## Implementation Steps
### 1. Workspace Isolation via Git Worktree
When implementing this plan, the Gemini CLI will automatically utilize its Git worktree feature to spawn a new isolated workspace (e.g., `feature/demo-seed`). This ensures the backend tooling modifications do not interfere with the `frontend-design-overhaul` worktree or the main branch.
### 2. Create the Seed Data Script (`backend/migrations/demo_seed.sql`)
Create a SQL script that safely injects realistic demo data. It will use `INSERT OR IGNORE` or handle conflicts to ensure it can be run cleanly for both demo and testing purposes.
- **Admin/Tutor Account**:
- Name: "Demo Admin"
- Email: `admin@tutortool.com`
- Password Hash: A pre-calculated bcrypt hash for the password `admin`.
- **Course**: "Demo Course 101" (Semester: "Current").
- **Room Layout**: A valid JSON SVG layout representing a small classroom with a few tables and seats.
- **Students**: Generate ~10 distinct student names linked to the course.
- **Session & Slot**: Create a session for the current date and an "open" slot linked to the demo room, ensuring the check-in feature can be demonstrated immediately without setup.
### 3. Update Makefile
Add a `seed-demo` target to the existing `Makefile`.
- The target will execute the SQLite CLI to run `demo_seed.sql` against the local development database defined by the `DATABASE_URL` environment variable (defaulting to `sqlite:./dev.db` for local dev).
### 4. Demo Run Guide
Provide a short set of instructions on how to start the environment using `docker-compose up` and the new seed command on the Surface Pro 5.
## Verification & Testing
- **Execution**: Run `make seed-demo` against a fresh SQLite database to ensure no foreign key or syntax errors occur.
- **Authentication**: Verify that logging in with `admin@tutortool.com` / `admin` succeeds against the seeded database.

View File

@@ -1,13 +0,0 @@
# Objective
Fix the Playwright MCP configuration in Gemini CLI by aligning it with the working Claude Code configuration.
# Key Files & Context
- `.gemini/settings.json`: This file currently contains the incorrect configuration pointing to `/opt/google/chrome/chrome`.
# Implementation Steps
1. Update `.gemini/settings.json` to modify the `args` array for the `playwright` mcpServer.
2. Remove the `--browser` and `chrome` arguments.
3. Update `--executable-path` to point to `/home/mpuchstein/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome` (the working binary used by Claude).
# Verification & Testing
After updating the configuration, the user will need to restart the Gemini CLI session to apply the changes and verify that the Playwright MCP tools function correctly.

View File

@@ -1,56 +0,0 @@
# Superadmin CRUD Implementation Plan
**Objective:** Implement a superadmin role to manage courses and tutors, ensuring only authorized users can perform system-wide administrative actions. This feature will be developed in an isolated git worktree.
## Key Context & Decisions
- **Role Strategy:** A new `is_superadmin` boolean column will be added to the `tutors` database table.
- **UI Structure:** A dedicated `/admin/tutors` page will handle tutor management. Course management will remain on `/admin/courses` but will be enhanced with superadmin-only actions (e.g., assigning tutors to courses).
- **Workspace:** Development will be done in `.worktrees/feature-superadmin-crud`.
## Implementation Steps
### 1. Workspace Isolation via Git Worktree
- Create a new git worktree: `git worktree add .worktrees/feature-superadmin-crud -b feature-superadmin-crud`
- All subsequent steps will be performed inside this isolated workspace.
### 2. Database & Models
- Create migration `backend/migrations/002_add_superadmin.sql` to add `is_superadmin BOOLEAN NOT NULL DEFAULT 0` to the `tutors` table.
- Update `backend/demo/demo_seed.sql` to set the default `admin@tutortool.com` as a superadmin (`is_superadmin = 1`).
- Update `backend/src/models.rs` to include `is_superadmin: bool` in the `Tutor` struct.
- Add `CreateTutor` and `TutorResponse` structs to `backend/src/models.rs`.
### 3. Auth & Core Backend
- Modify `backend/src/auth.rs` to include `is_superadmin: bool` in `TutorClaims`. This allows auth guards to check permissions efficiently.
- Update `backend/src/routes/auth_routes.rs` login handler to fetch `is_superadmin` and encode it in the JWT.
- Add a helper function to verify superadmin access to reject unauthorized requests.
### 4. Tutors API
- Create `backend/src/routes/tutors.rs` with endpoints:
- `GET /api/admin/tutors` (list all tutors)
- `POST /api/admin/tutors` (create a tutor, hashing their password)
- `DELETE /api/admin/tutors/:id` (delete a tutor)
- Merge these routes in `backend/src/routes/mod.rs`.
### 5. Course Assignments API
- Modify `backend/src/routes/courses.rs`:
- Enhance `GET /api/admin/courses` to return ALL courses if `claims.is_superadmin` is true, otherwise only return assigned courses.
- Restrict `POST /api/admin/courses` to superadmins only.
- Add `POST /api/admin/courses/:id/tutors` to assign a tutor to a course (superadmin only).
- Add `DELETE /api/admin/courses/:id/tutors/:tutor_id` to remove a tutor from a course (superadmin only).
- Add `GET /api/admin/courses/:id/tutors` to list tutors assigned to a course.
### 6. Frontend Auth & API Client
- Update `frontend/src/lib/types.ts` to include `Tutor` and the new `is_superadmin` flag in token payload or state.
- Add the new endpoints to `frontend/src/lib/api.ts` under `api.admin.tutors` and enhance `api.admin.courses`.
### 7. Frontend UI: Tutors Management
- Update `frontend/src/lib/components/TutorShell.svelte` to conditionally render a "Tutor:innen" link in the sidebar if the user is a superadmin.
- Create `frontend/src/routes/admin/tutors/+page.svelte` following the paper-bg design system. Include a list of tutors and a form to add a new tutor.
### 8. Frontend UI: Courses Enhancements
- Modify `frontend/src/routes/admin/courses/+page.svelte` to show a "Tutor:innen zuweisen" (Assign Tutors) section for each course if the logged-in user is a superadmin.
- Restrict the course creation form to superadmins only.
## Verification & Testing
- Run `cargo test` in the backend to ensure existing tests pass and new route isolation works.
- Perform a manual end-to-end test using the `make dev` script in the new worktree to verify the UI.

View File

@@ -7,12 +7,22 @@ metadata:
{{- include "tutortool.labels" . | nindent 4 }}
spec:
schedule: "0 3 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
jobTemplate:
spec:
activeDeadlineSeconds: 900
template:
spec:
restartPolicy: OnFailure
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
{{- include "tutortool.selectorLabels" . | nindent 22 }}
topologyKey: kubernetes.io/hostname
containers:
- name: backup
image: alpine:latest

View File

@@ -0,0 +1,40 @@
# Issues Discovered - 2026-05-04
This document summarizes the issues found during the Playwright exploration session on 2026-05-04.
## 1. JSON Parse Error when adding Tutors to Courses
- **Location**: `frontend/src/routes/admin/courses/+page.svelte` (triggered by `api.admin.courses.addTutor`)
- **Symptoms**: Alert box shows `SyntaxError: Failed to execute 'json' on 'Response': Unexpected end of JSON input`.
- **Root Cause**: The backend `POST /api/admin/courses/:id/tutors` handler likely returns a `200 OK` with an empty body. The frontend `api.ts` wrapper attempts to call `.json()` on every response, which fails on empty bodies.
- **Evidence**: Network request #48 in Playwright session showed `status: 200` but `content-length: 0`.
## 2. Attendance Count Missing on Student Check-in Page
- **Location**: `frontend/src/routes/s/[code]/+page.svelte`
- **Symptoms**: The "Anwesende" count shows `0 / 11` even after a successful check-in or when other students are present.
- **Root Cause**: The `loadInfo` function fetches the data but never assigns `res.attendances` to the local `attendances` state variable.
- **Evidence**: `loadInfo` contains `const checkinAttendances = res.attendances ?? [];` but lacks `attendances = checkinAttendances;`.
## 3. Seat Map Empty in Live View
- **Location**: `frontend/src/routes/admin/live/[slotId]/+page.svelte`
- **Symptoms**: All seats show as "frei" in the tutor's live view, even when students are checked in and assigned to seats.
- **Root Cause**: The `SeatMap` component is instantiated without passing `assignments` or `students` props.
- **Evidence**: `<SeatMap variant="tutor" scale={0.78} />` is used without other props in the source code.
## 4. Room Editor Element Selection Broken
- **Location**: `frontend/src/lib/RoomCanvas.svelte`
- **Symptoms**: Clicking an element in the Room Layout Editor does not select it (sidebar continues to show "Element auswählen").
- **Root Cause**: In `handleMouseDown`, if `editable` is true, the function returns early without calling `onElementClick`.
- **Evidence**:
```typescript
function handleMouseDown(e: MouseEvent, el: LayoutElement) {
if (!editable) {
onElementClick?.(el);
return;
}
draggingId = el.id;
// ...
}
```
## 5. Potential UI/UX: Dashboard Slot Status
- **Observation**: Dashboard sometimes shows "Anwesenheit offen" but "Alle Slots 1" and "Abgeschlossene Slots 0". It's a bit confusing if there is only one slot ever. (Minor)

View File

@@ -0,0 +1,187 @@
# TutorTool — Unified Bug Fixes, Tutor Lifecycle, Room Editor Refactor
## Context
A Playwright exploration session on **2026-05-04** (`docs/issues_discovered_20260504.md`) surfaced four production frontend bugs on top of an existing security backlog (RUSTSEC-2023-0071) and a half-finished Room Editor refactor. Three reference plans already exist in `conductor/`:
- `conductor/room-editor-refactor-core.md` — pixel→grid migration + editor robustification
- `conductor/room-editor-refactor-visualization.md` — replace hardcoded `SeatMap.svelte` with dynamic `RoomCanvas`
- `conductor/unified-refactor-and-fixes.md` — already merges the above with security/quick fixes (RUSTSEC, JSON parse, check-in typing, admin logout)
This plan **supersedes** `conductor/unified-refactor-and-fixes.md`, folds in two newly verified items:
1. **Admin/tutor deletion is opaquely broken.** `DELETE /api/admin/tutors/{id}` exists (`backend/src/routes/tutors.rs:74-94`) but the three FK references — `tutor_courses.tutor_id`, `slots.tutor_id`, `notes.tutor_id` (`backend/migrations/001_initial.sql:15,44,65`) — declare no `ON DELETE` clause, so SQLite RESTRICT raises a generic FK error → 500 → `alert("internal error")`.
2. **Issue #4 in the discovered-issues doc has its condition inverted.** `frontend/src/lib/RoomCanvas.svelte:30-38` is `if (!editable) { onElementClick?.(el); return; }`, so click-to-select fails **in edit mode** (the opposite of what the doc says). Fix: in edit mode, also fire `onElementClick` before initiating the drag.
The intended outcome is one continuous batch of work that ships: secure auth (logout + RUSTSEC patch), correct frontend data flow, a real tutor lifecycle (deactivate + delete with safety), and a unified, dynamic room renderer that fixes the silently-broken student check-in.
> **Memory housekeeping (post-approval):** the SLM memory `1728144c207346e2` ("admin CRUD enforces a permanent restriction: no tutors can be deleted") is **stale and contradicted by the code**. Update or remove it after this plan is approved.
---
## Phase 1 — Security & Quick Fixes
### 1.1 RUSTSEC-2023-0071 (Marvin Attack)
- `backend/Cargo.toml` — set `jsonwebtoken` to `features = ["aws_lc_rs"]`.
- `backend/audit.toml` — remove `ignore = ["RUSTSEC-2023-0071"]`.
- `.gitea/workflows/ci.yml` and `.gitea/workflows/release.yml` — drop `--ignore RUSTSEC-2023-0071`.
### 1.2 JSON parse error on empty 200 body
- `frontend/src/lib/api.ts:46` — currently only 204 is short-circuited. Extend the empty-body branch to also handle 200-with-empty-body (probe `content-length === '0'` or fall through `await res.text()` and return `{} as T` if empty). Triggers from `assignTutor`, `unassignTutor`, etc.
### 1.3 Check-in API typing
- `frontend/src/lib/api.ts``checkin.post` currently typed `Promise<Attendance>`; backend returns `{ok: true}`. Change to `Promise<{ok: boolean}>`.
- `frontend/src/routes/s/[code]/+page.svelte:82` — drop the local-state assignment from the response; rely on the subsequent `loadInfo()` to populate `myAttendance`.
### 1.4 Attendance count not assigned (issues doc #2)
- `frontend/src/routes/s/[code]/+page.svelte` `loadInfo` — assign `attendances = checkinAttendances` so the "Anwesende N / M" counter updates.
### 1.5 Admin logout UI
- `frontend/src/lib/components/TutorShell.svelte` (where the actual sidebar nav lives — `frontend/src/routes/admin/+layout.svelte` only renders the shell) — add an "Abmelden" entry at the bottom of the nav. On click: `await api.auth.logout()``auth.logout()``goto('/admin/login')`. Re-export `auth` and `api` if needed.
---
## Phase 2 — Tutor Lifecycle: Deactivate **and** Delete
The user wants **both** soft-deactivate (preserve history, hide from pickers) **and** real hard-delete (with safety pre-check).
### 2.1 Schema migration
- New file `backend/migrations/004_tutor_is_active.sql`:
```sql
ALTER TABLE tutors ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT 1;
```
No backfill needed; existing rows default to active.
### 2.2 Backend changes
**File:** `backend/src/routes/tutors.rs`
- **`Tutor` model** (`backend/src/models.rs:11-17`) — add `is_active: bool`.
- **`list_tutors`** — include `is_active` in SELECT and JSON output. Do **not** filter; the admin needs to see inactive tutors to reactivate them.
- **New: `set_tutor_active(id, {is_active: bool})`** — `PATCH /api/admin/tutors/{id}/active` body `{is_active: bool}`. Superadmin-only. Forbid self-deactivation (mirror the self-delete guard at `tutors.rs:84-86` — return `AppError::Conflict("cannot deactivate yourself")`).
- **`delete_tutor`** (existing handler) — before the `DELETE FROM tutors`, run three `SELECT COUNT(*)` queries inside a single connection (no need for an explicit tx; SQLite SELECTs see committed state):
- `tutor_courses WHERE tutor_id = ?`
- `slots WHERE tutor_id = ?`
- `notes WHERE tutor_id = ?`
If any non-zero, return `AppError::Conflict(format!("Tutor:in hat noch {c} Kurszuordnung(en), {s} Slot(s) und {n} Notiz(en). Bitte zuerst entfernen oder deaktivieren."))`.
`AppError::Conflict` already maps to 409 with `{"error": msg}` (`backend/src/error.rs`).
- **Auth login** (`backend/src/routes/auth_routes.rs:22-96`) — reject inactive tutors with `AppError::Unauthorized` (same response shape as wrong-password to avoid info leakage). Add this check after password verification.
- **Tutor pickers** — grep for `SELECT … FROM tutors` outside of `list_tutors` and `auth_routes.rs`. Likely sites: `backend/src/routes/courses.rs` (tutor assignment list), `backend/src/routes/sessions.rs` or `slots` creation flow. Each must add `WHERE is_active = 1`.
### 2.3 Frontend changes
**File:** `frontend/src/routes/admin/tutors/+page.svelte`
- Show three status pills: `Superadmin` / `Tutor:in` / `Inaktiv` (combine `is_superadmin` + `is_active`).
- Add a per-row "Deaktivieren" button (or "Aktivieren" if already inactive) that calls `api.admin.tutors.setActive(id, !is_active)`.
- Keep the existing "Löschen" button — error message from 409 already surfaces via the existing `catch (err) { alert(err.message) }` because `request<T>` extracts `error.error` (`api.ts:42-43`). Optionally: replace alert with an inline error region above the table for better UX.
- **API client** `frontend/src/lib/api.ts` — add `api.admin.tutors.setActive(id, is_active)` calling `PATCH /admin/tutors/{id}/active`.
### 2.4 Tests (`backend/src/routes/tutors.rs` `#[cfg(test)]` block)
Pattern after `backend/src/routes/rooms.rs:184-322` and use `backend/src/test_helpers.rs`.
- `delete_tutor_with_no_refs_succeeds` → 204, row gone.
- `delete_tutor_with_course_assignment_returns_409` → body contains `Kurszuordnung`.
- `delete_tutor_with_slot_returns_409` → body contains `Slot`.
- `delete_tutor_with_note_returns_409` → body contains `Notiz`.
- `delete_self_returns_409` (existing branch coverage).
- `set_active_false_then_login_fails` — deactivate, attempt login, expect 401.
- `set_active_false_hides_from_pickers` — verify any tutor-picker endpoint doesn't list inactive tutors.
- `cannot_deactivate_self_returns_409`.
---
## Phase 3 — Room Editor Core
Reference: `conductor/room-editor-refactor-core.md`. One-liner per task:
- **3.1 Pixel→grid migration** — new `backend/migrations/003_normalize_room_layout_units.sql`: parse `layout_json` per row; if any of x/y/w/h > 50, divide all four by 40 and rewrite. Update `backend/demo/demo_seed.sql:16-41` to grid units. Update any `rooms.rs` tests asserting pixel-scale numbers.
- **Note on numbering:** this is migration `003`; tutor `is_active` is `004`. Apply order matters because `003` only touches `rooms`, `004` only touches `tutors` — no cross-dependency.
- **3.2 Additive layout validators** — `backend/src/routes/rooms.rs:18-69`: add `MAX_CANVAS = 100` upper-bound + 0.5-step multiple checks; one test per validator.
- **3.3 RoomCanvas drag/resize hardening** — `frontend/src/lib/RoomCanvas.svelte`: bind `mousemove`/`mouseup` to `window` while dragging (currently bound to SVG only at lines 69-71); build resize handles + state from scratch; default snap step 0.5 (configurable via `snapStep` prop).
- **3.4 Fix click-to-select in edit mode (issues #4 corrected)** — `frontend/src/lib/RoomCanvas.svelte:30-38`: when `editable`, still call `onElementClick?.(el)` (so the parent updates `selectedId`) before starting the drag. Do **not** early-return.
- **3.5 Editor UI improvements** — `frontend/src/routes/admin/rooms/[roomId]/+page.svelte`: add X/Y `step="0.5"` numeric inputs, "+ Gap" button, "Snap to Grid" toggle, "Duplicate element" (UUID + 1-unit offset), inline error region for `saveLayout` failures (currently only `console.error`).
- **3.6 Robust seat labelling** — same file: replace the brittle `(filter + 1)` next-label with `max(existingLabels) + 1`.
- **3.7 Room and Element deletion**
- **Element deletion** is already wired in the editor sidebar (`+page.svelte:101`); validate it still works post-migration.
- **Room deletion (new)**:
- Backend: `DELETE /api/admin/rooms/{id}` in `backend/src/routes/rooms.rs`. Pre-check `SELECT COUNT(*) FROM slots WHERE room_id = ?`; if non-zero, 409 with `format!("Raum hat noch {n} Slot(s). Bitte zuerst entfernen.")`. Otherwise plain DELETE.
- Frontend: `frontend/src/routes/admin/rooms/+page.svelte` — add per-row "Löschen" button with `confirm()` dialog; error message surfaces via the existing alert path.
- API client: `api.admin.rooms.delete(id)`.
- Tests: `delete_room_with_no_slots_succeeds`, `delete_room_with_slot_returns_409`.
---
## Phase 4 — Unified Visualization (Dynamic RoomCanvas)
Reference: `conductor/room-editor-refactor-visualization.md`. One-liner per task:
- **4.1 Extend RoomCanvas** — `frontend/src/lib/RoomCanvas.svelte`: add `clickable: boolean = false` prop (orthogonal to 3.4 — `clickable` enables `onElementClick` in **read-only** mode); replace fixed `width="800" height="600"` (line 65) with computed `viewBox` + `preserveAspectRatio="xMidYMid meet"` + CSS `width:100%;height:auto`; verify the editor route still bounds the SVG (wrap with `style="width:800px"` if needed); apply seat/table styling from the design handoff (rounded tables, occupied/mine colour states, label positioning).
- **4.2 Backend: ship layout with attendance** — `backend/src/routes/attendance.rs:62-105` + `backend/src/models.rs`: extend `SessionAttendance` with per-slot `layout: Option<Vec<LayoutElement>>` to avoid an N+1.
- **4.3 Admin live view migration** — `frontend/src/routes/admin/live/[slotId]/+page.svelte:161`: replace `<SeatMap variant="tutor" scale={0.78} />` with `<RoomCanvas elements={slot.layout ?? []} clickable occupiedSeatIds={…} studentNames={…} onElementClick={handleSeatClick} />`. Implement `handleSeatClick(seatId)` mapping seat→student via `attendances.find(a => a.seat_id === seatId)?.student_id` → `selectedStudentId` (drives the existing note editor).
- **4.4 Student check-in migration (4 sites)** — `frontend/src/routes/s/[code]/+page.svelte` lines 210, 248, 316, 368: read `res.layout` in `loadInfo` (`let layout = $state<LayoutElement[]>([])`); replace each call. Lines 210/316 (seat-pick): `clickable` + `onElementClick={selectSeat}`. Lines 248/368 (confirmed): read-only with `mySeatId={myAttendance?.seat_id ?? null}`. Add `const occupiedSeatIds = $derived(attendances.map(a => a.seat_id).filter(Boolean) as string[])`.
- **4.5 Delete `frontend/src/lib/components/SeatMap.svelte`** — only after `grep -rn "SeatMap" frontend/src/` returns zero hits and Playwright passes.
---
## Verification
### Automated
- `make test` — backend suite passes, including:
- Phase 2.4 tutor delete + activate tests
- Phase 3.2 layout validator tests
- Phase 3.7 room delete tests
- `sqlx migrate run` against a clean DB — migrations 003 and 004 apply cleanly.
- `make lint` — zero warnings (per the project's mandate).
- `frontend/tests/rooms.spec.ts` *(new)* — create room, drag/snap, save/reload, assert coords preserved.
- `frontend/tests/checkin-dynamic.spec.ts` *(new)* — custom layout, click seat, `POST /api/checkin` succeeds, seat turns green.
- `frontend/tests/admin-live-dynamic.spec.ts` *(new)* — student appears on correct seat in tutor live view.
- `frontend/tests/admin-tutors.spec.ts` *(new)* — (i) delete tutor with no refs → row disappears; (ii) delete tutor attached to a course → red error mentions `Kurszuordnung`; unassign → retry succeeds; (iii) deactivate → tutor cannot log in; (iv) reactivate → tutor can log in again.
- `frontend/tests/admin-rooms-delete.spec.ts` *(new)* — delete unused room succeeds; delete room attached to a slot → 409 message visible.
### Manual
1. `make seed-demo`. Open `Admin → Rooms → Room A` — all elements at sensible grid positions (no values > 50).
2. Drag and resize an element; release outside the SVG; confirm no stranded drag.
3. Click an element in **edit mode** — sidebar populates (issue #4 regression).
4. Add a Gap; toggle Snap; Duplicate an element; trigger a deliberate save error and confirm the inline message.
5. Create a U-shaped room; attach to a session/slot.
6. Open the student check-in link on a mobile viewport — U-shape renders, seat-click checks in, seat turns green.
7. Open tutor Live View — student name on correct seat; click opens note editor.
8. **Tutor lifecycle:** as superadmin, create a tutor "Test", attach to a course → click `Löschen` → red message lists `1 Kurszuordnung`. Click `Deaktivieren` → status pill flips to `Inaktiv`; attempt to log in as that tutor → fails. Click `Aktivieren` → login succeeds again. Unassign from course → click `Löschen` → succeeds.
9. **Room lifecycle:** delete a room with no slots → row disappears. Delete a room attached to a slot → 409 message visible.
10. Click `Abmelden` in admin sidebar → redirect to `/admin/login`; refresh → still logged out.
11. `cargo audit` (without the ignore flag) — no RUSTSEC-2023-0071 finding.
---
## Critical files
- `backend/Cargo.toml`
- `backend/audit.toml`
- `.gitea/workflows/ci.yml`, `.gitea/workflows/release.yml`
- `backend/migrations/003_normalize_room_layout_units.sql` *(new)*
- `backend/migrations/004_tutor_is_active.sql` *(new)*
- `backend/demo/demo_seed.sql`
- `backend/src/models.rs`
- `backend/src/routes/tutors.rs`
- `backend/src/routes/rooms.rs`
- `backend/src/routes/attendance.rs`
- `backend/src/routes/auth_routes.rs`
- `backend/src/routes/courses.rs` (tutor picker filter)
- `backend/src/routes/sessions.rs` (tutor picker filter)
- `frontend/src/lib/api.ts`
- `frontend/src/lib/types.ts`
- `frontend/src/lib/RoomCanvas.svelte`
- `frontend/src/lib/components/SeatMap.svelte` *(delete in Phase 4.5)*
- `frontend/src/lib/components/TutorShell.svelte` (logout button)
- `frontend/src/routes/admin/tutors/+page.svelte`
- `frontend/src/routes/admin/rooms/+page.svelte`
- `frontend/src/routes/admin/rooms/[roomId]/+page.svelte`
- `frontend/src/routes/admin/live/[slotId]/+page.svelte`
- `frontend/src/routes/s/[code]/+page.svelte`
---
## Post-implementation memory hygiene (do after the plan ships)
- Update SLM memory `1728144c207346e2` to reflect the new lifecycle (deactivate + safe delete) instead of the stale "no tutors can be deleted" claim.
- Store a new SLM memory capturing the deletion-policy decision (deactivate-or-block-with-409) so future sessions don't re-litigate it.