fix: address review findings — error handling, migration safety, CI audit
CI / test (push) Failing after 3m6s
CI / test (pull_request) Failing after 3m22s

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
This commit is contained in:
2026-05-05 01:28:40 +02:00
parent 3b9c755e39
commit 827eb63bab
9 changed files with 97 additions and 37 deletions
@@ -22,6 +22,7 @@
let loading = $state(true);
let pollInterval: ReturnType<typeof setInterval> | null = null;
let loadError = $state<string | null>(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 @@
<div style="padding:28px 36px;display:flex;flex-direction:column;gap:22px">
{#if loadError}
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px">
Fehler beim Laden: {loadError}
</div>
{/if}
{#if loading}
<div style="padding:48px;text-align:center">
<span class="small" style="color:var(--ink-4)">Wird geladen…</span>
+21 -3
View File
@@ -5,19 +5,31 @@
let rooms = $state<Room[]>([]);
let newRoomName = $state('');
let errorMsg = $state<string | null>(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 @@
<h1 class="h1" style="font-family:var(--serif)">Raumlayout-Editor</h1>
</div>
{#if errorMsg}
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px;margin-bottom:12px">
{errorMsg}
</div>
{/if}
<div class="card" style="overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--rule);display:flex;align-items:center;justify-content:space-between">
<div class="serif" style="font-size:18px;font-weight:500">Räume</div>
@@ -1,6 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import RoomCanvas from '$lib/RoomCanvas.svelte';
import type { Room, LayoutElement } from '$lib/types';
@@ -12,13 +11,13 @@
let errorMsg = $state<string | null>(null);
let snapToGrid = $state(true);
onMount(async () => {
room = await api.admin.rooms.get(roomId);
});
$effect(() => {
if (roomId) {
api.admin.rooms.get(roomId).then((r: Room) => { room = r; });
api.admin.rooms.get(roomId)
.then((r: Room) => { room = r; })
.catch((e: unknown) => {
errorMsg = e instanceof Error ? e.message : 'Raum konnte nicht geladen werden.';
});
}
});
+11 -2
View File
@@ -7,6 +7,7 @@
let tutors = $state<Tutor[]>([]);
let loading = $state(true);
let loadError = $state<string | null>(null);
let newTutor = $state({
name: '',
@@ -23,8 +24,10 @@
async function loadTutors() {
try {
tutors = await api.admin.tutors.list();
loadError = null;
} catch (e) {
console.error(e);
loadError = e instanceof Error ? e.message : 'Tutor:innen konnten nicht geladen werden.';
console.error('[tutors/loadTutors]', e);
}
}
@@ -84,12 +87,18 @@
<UnderlineStroke width={120} />
</div>
{#if loadError}
<div style="background:rgba(138,44,31,0.06);border:1px solid var(--accent);color:var(--accent);padding:10px 14px;border-radius:4px;font-size:13px;margin-bottom:12px">
{loadError}
</div>
{/if}
<div class="card" style="overflow:hidden">
{#if loading}
<div style="padding:32px;text-align:center">
<span class="small" style="color:var(--ink-4)">Wird geladen…</span>
</div>
{:else if tutors.length === 0}
{:else if tutors.length === 0 && !loadError}
<div style="padding:32px;text-align:center">
<span class="small" style="color:var(--ink-4)">Keine Tutor:innen gefunden.</span>
</div>
+11 -7
View File
@@ -82,17 +82,21 @@
if (!selectedStudent) return;
try {
await api.checkin.post(code, selectedStudent.id, seatId);
await loadInfo();
} catch (e) {
if (e instanceof Error) {
if (e.message?.includes('already checked in') || e.message?.includes('409')) {
errorMsg = 'Dieser Platz ist bereits belegt.';
} else {
errorMsg = e.message ?? 'Einchecken fehlgeschlagen.';
}
errorMsg = e.message.includes('seat taken')
? 'Dieser Platz ist bereits belegt.'
: (e.message || 'Einchecken fehlgeschlagen.');
} else {
errorMsg = 'Einchecken fehlgeschlagen.';
errorMsg = 'Einchecken fehlgeschlagen.';
}
return;
}
try {
await loadInfo();
} catch (e) {
step = 'confirmed';
console.error('[checkin/loadInfo]', e);
}
}