From 827eb63bab128b89bb5c7671186ffd76c833c49e Mon Sep 17 00:00:00 2001 From: "s0wlz (Matthias Puchstein)" Date: Tue, 5 May 2026 01:28:40 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20address=20review=20findings=20=E2=80=94?= =?UTF-8?q?=20error=20handling,=20migration=20safety,=20CI=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - migration 003: apply pixel→grid transform per-element (CASE WHEN > 50) instead of per-row, preventing double-conversion of mixed-scale rooms; skip empty arrays via json_array_length guard to avoid NULL assignment - attendance.rs: log layout JSON parse errors instead of silently swallowing them with .ok() - tutors.rs: check rows_affected() in set_tutor_active and return 404 for non-existent IDs; remap FK constraint errors on delete to 409 so concurrent inserts between conflict-check and DELETE don't surface as 500 Frontend: - live/[slotId]: expose polling failures to the tutor via error banner instead of only console.error - s/[code]: split checkin into two try/catch blocks so a successful POST followed by a failed reload doesn't report failure to the student; fix dead '409' string detection to match actual server error 'seat taken' - rooms/[roomId]: remove duplicate onMount fetch; add .catch() to $effect - tutors: expose loadTutors failures via error banner, not just console - rooms: fix bare catch in createRoom (captures error, shows message); add try/catch to onMount rooms load CI: - sync cargo audit --ignore RUSTSEC-2023-0071 with Makefile; the advisory is in rsa which sqlx-mysql retains in the lock file even when the mysql feature is disabled — aws_lc_rs correctly removes it from the active tree --- .gitea/workflows/ci.yml | 2 +- .../003_normalize_room_layout_units.sql | 29 ++++++++++--------- backend/src/routes/attendance.rs | 8 ++++- backend/src/routes/tutors.rs | 19 ++++++++++-- .../routes/admin/live/[slotId]/+page.svelte | 10 ++++++- frontend/src/routes/admin/rooms/+page.svelte | 24 +++++++++++++-- .../routes/admin/rooms/[roomId]/+page.svelte | 11 ++++--- frontend/src/routes/admin/tutors/+page.svelte | 13 +++++++-- frontend/src/routes/s/[code]/+page.svelte | 18 +++++++----- 9 files changed, 97 insertions(+), 37 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 16adb15..2bd4c4b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: - name: Security audit run: | cargo install cargo-audit --locked - cd backend && cargo audit + cd backend && cargo audit --ignore RUSTSEC-2023-0071 - name: Build frontend run: pnpm --dir frontend build diff --git a/backend/migrations/003_normalize_room_layout_units.sql b/backend/migrations/003_normalize_room_layout_units.sql index cd11ae2..38a264e 100644 --- a/backend/migrations/003_normalize_room_layout_units.sql +++ b/backend/migrations/003_normalize_room_layout_units.sql @@ -1,24 +1,27 @@ --- Normalize room layout units: divide all coordinates/dimensions by 40 if they are in pixel-scale (e.g. > 50) --- Note: This is an idempotent approach by only updating rows with large values. +-- Normalize room layout units: divide pixel-scale coordinates by 40. +-- Applied per element with CASE WHEN so the transform is idempotent: +-- coordinates already in grid units (≤50) are left untouched. UPDATE rooms SET layout_json = ( SELECT json_group_array( json_object( 'id', json_extract(value, '$.id'), 'label', json_extract(value, '$.label'), - 'x', ROUND(CAST(json_extract(value, '$.x') AS REAL) / 40.0, 2), - 'y', ROUND(CAST(json_extract(value, '$.y') AS REAL) / 40.0, 2), - 'width', ROUND(CAST(json_extract(value, '$.width') AS REAL) / 40.0, 2), - 'height', ROUND(CAST(json_extract(value, '$.height') AS REAL) / 40.0, 2), + 'x', CASE WHEN CAST(json_extract(value, '$.x') AS REAL) > 50 + THEN ROUND(CAST(json_extract(value, '$.x') AS REAL) / 40.0, 2) + ELSE json_extract(value, '$.x') END, + 'y', CASE WHEN CAST(json_extract(value, '$.y') AS REAL) > 50 + THEN ROUND(CAST(json_extract(value, '$.y') AS REAL) / 40.0, 2) + ELSE json_extract(value, '$.y') END, + 'width', CASE WHEN CAST(json_extract(value, '$.width') AS REAL) > 50 + THEN ROUND(CAST(json_extract(value, '$.width') AS REAL) / 40.0, 2) + ELSE json_extract(value, '$.width') END, + 'height', CASE WHEN CAST(json_extract(value, '$.height') AS REAL) > 50 + THEN ROUND(CAST(json_extract(value, '$.height') AS REAL) / 40.0, 2) + ELSE json_extract(value, '$.height') END, 'type', json_extract(value, '$.type') ) ) FROM json_each(rooms.layout_json) ) -WHERE EXISTS ( - SELECT 1 FROM json_each(rooms.layout_json) - WHERE json_extract(value, '$.x') > 50 - OR json_extract(value, '$.y') > 50 - OR json_extract(value, '$.width') > 50 - OR json_extract(value, '$.height') > 50 -); +WHERE json_array_length(layout_json) > 0; diff --git a/backend/src/routes/attendance.rs b/backend/src/routes/attendance.rs index 333db79..0df8f97 100644 --- a/backend/src/routes/attendance.rs +++ b/backend/src/routes/attendance.rs @@ -94,7 +94,13 @@ async fn get_session_attendance( let mut slots = Vec::new(); for row in slot_rows { let layout: Option> = match row.8 { - Some(json_str) => serde_json::from_str(&json_str).ok(), + Some(ref json_str) => match serde_json::from_str(json_str) { + Ok(v) => Some(v), + Err(e) => { + tracing::error!(slot_id = row.0, err = %e, "failed to deserialize room layout_json"); + None + } + }, None => None, }; slots.push(crate::models::Slot { diff --git a/backend/src/routes/tutors.rs b/backend/src/routes/tutors.rs index 054f4eb..0b8cc2d 100644 --- a/backend/src/routes/tutors.rs +++ b/backend/src/routes/tutors.rs @@ -87,12 +87,16 @@ async fn set_tutor_active( return Err(AppError::Conflict("cannot deactivate yourself".into())); } - sqlx::query("UPDATE tutors SET is_active = ? WHERE id = ?") + let result = sqlx::query("UPDATE tutors SET is_active = ? WHERE id = ?") .bind(req.is_active) .bind(id) .execute(&pool) .await?; + if result.rows_affected() == 0 { + return Err(AppError::NotFound); + } + Ok(StatusCode::NO_CONTENT) } @@ -145,10 +149,19 @@ async fn delete_tutor( ))); } - sqlx::query("DELETE FROM tutors WHERE id = ?") + match sqlx::query("DELETE FROM tutors WHERE id = ?") .bind(id) .execute(&pool) - .await?; + .await + { + Ok(_) => {} + Err(sqlx::Error::Database(e)) if e.message().contains("FOREIGN KEY") => { + return Err(AppError::Conflict( + "Tutor:in hat noch Verweise in der Datenbank.".into(), + )); + } + Err(e) => return Err(AppError::Db(e)), + } Ok(StatusCode::NO_CONTENT) } diff --git a/frontend/src/routes/admin/live/[slotId]/+page.svelte b/frontend/src/routes/admin/live/[slotId]/+page.svelte index 212f27d..185971c 100644 --- a/frontend/src/routes/admin/live/[slotId]/+page.svelte +++ b/frontend/src/routes/admin/live/[slotId]/+page.svelte @@ -22,6 +22,7 @@ let loading = $state(true); let pollInterval: ReturnType | null = null; + let loadError = $state(null); onMount(async () => { await loadData(); @@ -73,7 +74,8 @@ if (found) break; } } catch (e) { - console.error(e); + loadError = e instanceof Error ? e.message : 'Daten konnten nicht geladen werden.'; + console.error('[live/loadData]', e); } } @@ -137,6 +139,12 @@
+ {#if loadError} +
+ Fehler beim Laden: {loadError} +
+ {/if} + {#if loading}
Wird geladen… diff --git a/frontend/src/routes/admin/rooms/+page.svelte b/frontend/src/routes/admin/rooms/+page.svelte index 4e0d0a5..fdd90c6 100644 --- a/frontend/src/routes/admin/rooms/+page.svelte +++ b/frontend/src/routes/admin/rooms/+page.svelte @@ -5,19 +5,31 @@ let rooms = $state([]); let newRoomName = $state(''); + let errorMsg = $state(null); onMount(async () => { - rooms = await api.admin.rooms.list(); + try { + rooms = await api.admin.rooms.list(); + } catch (e) { + errorMsg = e instanceof Error ? e.message : 'Räume konnten nicht geladen werden.'; + } }); async function createRoom() { if (!newRoomName.trim()) return; + errorMsg = null; try { await api.admin.rooms.create(newRoomName, []); newRoomName = ''; + } catch (e) { + errorMsg = e instanceof Error ? e.message : 'Raum konnte nicht erstellt werden.'; + console.error('[rooms/createRoom]', e); + return; + } + try { rooms = await api.admin.rooms.list(); - } catch { - console.error('failed to fetch rooms'); + } catch (e) { + console.error('[rooms/createRoom/list]', e); } } @@ -38,6 +50,12 @@

Raumlayout-Editor

+ {#if errorMsg} +
+ {errorMsg} +
+ {/if} +
Räume
diff --git a/frontend/src/routes/admin/rooms/[roomId]/+page.svelte b/frontend/src/routes/admin/rooms/[roomId]/+page.svelte index ffce2ce..62fe883 100644 --- a/frontend/src/routes/admin/rooms/[roomId]/+page.svelte +++ b/frontend/src/routes/admin/rooms/[roomId]/+page.svelte @@ -1,6 +1,5 @@