From 650f3456cb78aed64e008d141961c44f30e6591c Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Mon, 4 May 2026 17:15:53 +0200 Subject: [PATCH] some planning and issue finding --- conductor/attendance-tool.md | 161 --------------- conductor/demo-plan.md | 39 ---- conductor/fix-playwright-mcp-config.md | 13 -- conductor/superadmin-crud.md | 56 ------ deploy/templates/cronjob-backup.yaml | 10 + docs/issues_discovered_20260504.md | 40 ++++ ...-unified-fixes-and-room-editor-refactor.md | 187 ++++++++++++++++++ 7 files changed, 237 insertions(+), 269 deletions(-) delete mode 100644 conductor/attendance-tool.md delete mode 100644 conductor/demo-plan.md delete mode 100644 conductor/fix-playwright-mcp-config.md delete mode 100644 conductor/superadmin-crud.md create mode 100644 docs/issues_discovered_20260504.md create mode 100644 docs/plans/2026-05-04-unified-fixes-and-room-editor-refactor.md diff --git a/conductor/attendance-tool.md b/conductor/attendance-tool.md deleted file mode 100644 index 4b1ada1..0000000 --- a/conductor/attendance-tool.md +++ /dev/null @@ -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-.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. \ No newline at end of file diff --git a/conductor/demo-plan.md b/conductor/demo-plan.md deleted file mode 100644 index cb4def6..0000000 --- a/conductor/demo-plan.md +++ /dev/null @@ -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. diff --git a/conductor/fix-playwright-mcp-config.md b/conductor/fix-playwright-mcp-config.md deleted file mode 100644 index 1e26bae..0000000 --- a/conductor/fix-playwright-mcp-config.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/conductor/superadmin-crud.md b/conductor/superadmin-crud.md deleted file mode 100644 index 96c8c1d..0000000 --- a/conductor/superadmin-crud.md +++ /dev/null @@ -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. diff --git a/deploy/templates/cronjob-backup.yaml b/deploy/templates/cronjob-backup.yaml index 247755a..8aa0444 100644 --- a/deploy/templates/cronjob-backup.yaml +++ b/deploy/templates/cronjob-backup.yaml @@ -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 diff --git a/docs/issues_discovered_20260504.md b/docs/issues_discovered_20260504.md new file mode 100644 index 0000000..c4e817d --- /dev/null +++ b/docs/issues_discovered_20260504.md @@ -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**: `` 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) diff --git a/docs/plans/2026-05-04-unified-fixes-and-room-editor-refactor.md b/docs/plans/2026-05-04-unified-fixes-and-room-editor-refactor.md new file mode 100644 index 0000000..795848f --- /dev/null +++ b/docs/plans/2026-05-04-unified-fixes-and-room-editor-refactor.md @@ -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`; 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` 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>` to avoid an N+1. +- **4.3 Admin live view migration** — `frontend/src/routes/admin/live/[slotId]/+page.svelte:161`: replace `` with ``. 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([])`); 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.