Skip to content

App-Review: Bugfixes, Härtung und Refactorings aus dem Funktions-Audit - #20

Merged
veniplex merged 7 commits into
mainfrom
claude/app-review-functionality-usb0y6
Jul 18, 2026
Merged

veniplex merged 7 commits into
mainfrom
claude/app-review-functionality-usb0y6

Conversation

@veniplex

Copy link
Copy Markdown
Owner

Kritischer App-Review (UI/UX · Backend · AI) — Befunde & Fixes

Vollständiger Funktions- und Architektur-Review der App aus drei Perspektiven, mit anschließender Umsetzung aller Befunde. Baseline vor den Änderungen war grün (Typecheck, Lint, 102 Tests); nach den Änderungen: Typecheck ✓, Lint ✓, 123 Tests ✓ (21 neue), Produktions-Build ✓.

Kritische Bugs (behoben)

1. Die gesamte PWA-/Push-Story war funktionslos

sw-register.tsx registrierte /sw.jsdie Datei existierte nirgends und wurde von keinem Build-Schritt erzeugt. Folgen: Registrierung 404te in Produktion, navigator.serviceWorker.ready resolvte nie, und der „Push aktivieren"-Button in den Einstellungen hing für immer im Spinner. Push-Notifications (Reminder-Feature!) waren damit komplett tot.
Fix: Echter public/sw.js (Push + Notification-Click + pushsubscriptionchange, Network-first-Navigation mit Offline-Fallback-Seite, Cache-first für immutable Build-Assets, /api/** nie gecacht). Versionierung über die Registrierungs-URL (?v=<App-Version>) — jede Release invalidiert die Caches automatisch, ohne Build-Skript. Die Push-Einstellungen laufen jetzt mit Timeout + verständlicher Fehlermeldung statt Endlos-Spinner.

2. Chat-Route vertraute der Client-History vollständig

/api/ai/chat nahm body.messages komplett vom Client: Assistant-Turns spoofbar, Mode-System-Prompt umgehbar, unbegrenzte Token-Kosten pro Request (kein Cap auf Größe/Anzahl), und bei Client-Disconnect divergierte die gespeicherte History (User-Message persistiert, Assistant-Antwort nicht).
Fix: Die DB ist jetzt Source of Truth. Der Client liefert nur seine letzte Message; Tool-Bestätigungen (executed/rejected) werden gezielt in die gespeicherte Assistant-Message gemerged — Text und Tool-Inputs bleiben serverseitig autoritativ. Caps: 32k Zeichen pro User-Message, 40-Message-Fenster. consumeStream() sorgt dafür, dass Persistenz + Usage-Logging auch bei Abbruch laufen.

3. Batch-Polling konnte Ergebnisse doppelt einfügen

Der 5-Minuten-Cron poll-batches lief ohne Lock. Überlappende Läufe (langsames Apply, oder Web-Tier mit WORKERS_IN_PROCESS=true plus dediziertem Worker) konnten dieselben Batch-Ergebnisse doppelt einfügen und doppelt Usage loggen — der Deduper hilft nicht, weil beide Läufe vor dem Commit des jeweils anderen seeden.
Fix: Atomarer Claim (running → applying) vor dem Ingest; verwaiste Claims werden nach 15 min reklamiert; bei Fehlern wird der Claim freigegeben, damit der nächste Lauf erneut ingestiert.

4. DATABASE_URL fiel in Produktion still auf die Dev-DB zurück

env.ts hatte für DATABASE_URL/APP_URL hartcodierte localhost-Fallbacks auch in Produktion (anders als BETTER_AUTH_SECRET/ENCRYPTION_KEY). Fix: Fail-fast in Produktion.

Weitere Backend-/AI-Härtung

  • Rate-Limiting für /api/ics/[token] (unauthentifizierte Capability-URL → Token-Bruteforce), /api/search, /api/materials/upload, /api/push. Hinweis: Der Login war entgegen erstem Befund bereits geschützt — better-auth limitiert Sign-in/Sign-up in Produktion per Default (3 Req/10 s).
  • Push-Subscription-Übernahme: onConflictDoUpdate auf den unique endpoint konnte die Row eines anderen Accounts überschreiben. Jetzt 409 + clientseitiges Re-Subscribe (Shared-Device-Fall bleibt funktionsfähig).
  • OpenAI-Batches: max_tokensmax_completion_tokens (neuere Modelle lehnen max_tokens ab) und strict: true mit strict-kompatiblem Schema — vorher konnten Items still als „failed" enden.
  • Quiz-Datenqualität: correctIndex wird jetzt an allen drei Persistenzpfaden (Live-Generierung, Batch-Ingest, Chat-Write-Tool) gegen options.length validiert — vorher waren Multiple-Choice-Fragen mit unmöglicher richtiger Antwort speicherbar. endsAt im Kalender-Tool wird wie startsAt validiert.
  • ANN-Reindex bricht bei gemischten Embedding-Dimensionen nicht mehr komplett ab (überspringt + loggt).
  • Reminder-Cron: lud bisher alle wiederkehrenden Events und alle offenen Assignments — jetzt auf das relevante Zeitfenster begrenzt.
  • Extraktion: Size-Caps auch für PDF/DOCX/PPTX/XLSX (liefen ungedeckelt in den Speicher), UTF-8-sichere Truncation (vorher konnte ein Multibyte-Zeichen zerschnitten werden), Upload-Route gibt bei kaputtem x-file-name-Header 400 statt 500.
  • Schema-Integrität: echtes Self-FK für thesisProject.supersededById, der COALESCE-Unique-Index der Ordner-Siblings ist jetzt auch im Drizzle-Schema deklariert, und Generation-Rows (polymorpher targetId, kein FK möglich) werden beim Löschen von Deck/Quiz aufgeräumt (inkl. einmaliger Orphan-Cleanup-Migration).
  • Storage: Pfad-Containment mit Separator-Suffix (startsWith(root) akzeptierte auch root-evil), S3-Keys lehnen Backslashes ab.
  • setMaterials (Assignments) filtert Materialien jetzt explizit auf userId statt nur transitiv über das Modul.

Frontend/UX

  • Fehlermeldungen: Quiz-Generieren-Dialog zeigte als Fehler nur das Wort „Module" (Label statt Meldung); Login verwarf echte Ursachen (unverifizierte E-Mail, Rate-Limit) — beides jetzt differenziert und übersetzt. Chat-UI zeigt keine rohen error.message-Interna mehr.
  • Optimistische Toggles in den Benachrichtigungs-Einstellungen rollen bei Serverfehler zurück.
  • Accessibility: Icon-Buttons (Sidebar-Menüs, Material-Griffe, Resize-Handle) kündigen jetzt die Aktion an statt nur den Entity-Namen; Kalender-Kontextmenü ist keyboard-bedienbar (Escape, Fokus, Menü-Rollen) und wird am Viewport geclampt.
  • Loading-States: loading.tsx-Skeletons für alle Modul-Unterrouten, study/due, Settings, Admin, Material-Viewer und Plan-Seiten (vorher fror die alte Seite beim Navigieren ein).
  • isNetworkError wertete jeden TypeError als „offline" — echte Client-Bugs landeten still in der Offline-Outbox statt sichtbar zu werden.
  • i18n-Reste (Thesis-Placeholder, hartcodiertes aria-label) übersetzt.

Architektur-Refactorings

  • FormDialog: Die Copy-Paste-Hülle der Create/Edit-Dialoge (Deck/Quiz/Plan) ist in eine gemeinsame Komponente extrahiert; die Generate-Dialoge mit Job-Progress behalten bewusst ihre Sonderstruktur.
  • Modul-Tabs: Segmente + Icons als Single Source (src/config/module-tabs.ts) für Sidebar und Tab-Leiste — vorher zwei driftende Kopien.
  • Modul-Übersichtsseite: 6 serielle awaits → Promise.all (TTFB).

Bewusst NICHT geändert (geprüft, begründet belassen)

  • Ownership-Checks: Alle 18 Action-Dateien und der AI-Tool-Executor prüfen Session + Ressourcen-Eigentum konsistent — hier war nichts zu fixen (positiver Befund).
  • RAG-Embedding-Interpolation in SQL: modellgenerierte Floats, kein Injektionsvektor.
  • pptx/xlsx-Extraktion per Regex: bekannte Qualitätsgrenze, echter Parser wäre ein eigenes Projekt.
  • unpack-zip mit retryLimit: 0: dokumentierter Trade-off (nicht idempotent).
  • Monatliches Token-Limit bleibt ein Soft-Limit (jetzt dokumentiert); harte Grenze pro Request kommt über die neuen Message-Caps.

Migration

drizzle/0035_fair_killmonger.sql — bereinigt hängende Thesis-Supersede-Pointer, fügt das FK hinzu, legt den Folder-Index idempotent an (IF NOT EXISTS, existiert auf Bestands-DBs bereits) und räumt Generation-Orphans auf. npm run db:migrate wie üblich.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR


Generated by Claude Code

claude added 7 commits July 18, 2026 07:26
…ntegrity

- env: DATABASE_URL/APP_URL no longer fall back to dev defaults in production
- rate limiting for ics/search/upload/push API routes (in-memory fixed window)
- push subscribe can no longer take over another account's subscription row
- upload: malformed x-file-name header returns 400 instead of 500
- assignment material linking now filters by owner, not just module
- storage: separator-safe path containment (local), reject backslashes (s3)
- reminders: bound recurring-event and assignment queries to the live window
- schema: real FK for thesis supersededById, folder sibling unique index
  mirrored in Drizzle schema, orphan cleanup for polymorphic generation rows
- extraction: size caps for binary document parsers, UTF-8-safe truncation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
…alidation

- batch poller now atomically claims a finished batch (running -> applying)
  before ingesting, so overlapping cron runs can no longer double-insert the
  same results; stale claims are reclaimed after 15 minutes
- OpenAI batch tasks use max_completion_tokens (max_tokens is rejected by
  newer models) and strict json_schema with a strict-compatible schema
- multiple-choice questions with a correctIndex outside their options are
  rejected at every persistence path (live generation, batch ingest, chat
  write tool)
- calendar write tool validates endsAt like startsAt
- ANN reindex skips mixed-dimension embeddings instead of failing the rebuild
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
…e errors

- the chat route now rebuilds the model context from the persisted DB
  history instead of trusting the client-supplied message list; a request
  can no longer spoof prior assistant turns or bypass the mode prompt
- tool-confirmation continuations merge only the executed/rejected outcome
  onto the stored assistant message (text and tool inputs stay authoritative)
- new bounds: 32k chars per user message, 40-message model window
- assistant persistence and usage logging survive client disconnects
  (consumeStream) so stored history matches what was streamed
- incomplete write-tool calls are dropped at model conversion instead of
  producing provider errors for dangling tool_use blocks
- chat UI shows a translated, friendly error instead of raw error.message
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
The app registered /sw.js, but no such file existed and no build step
produced one — the whole PWA story (offline caching, installability
behaviour, web push) was dead, and the push-enable button hung forever
waiting on serviceWorker.ready.

- public/sw.js: versioned via the registration URL (?v=<app version>),
  network-first navigations with offline fallback page, cache-first for
  immutable build assets/icons, push + notification-click +
  pushsubscriptionchange handlers, API traffic never cached
- public/offline.html: minimal theme-aware offline fallback
- sw-register: version-stamped registration, logs failures
- notification settings: serviceWorker.ready raced against a timeout with a
  translated error, optimistic channel toggles roll back on save failure,
  409 from the push API triggers a fresh subscription (shared-device case)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
- quiz generate dialog: 'select a module first' error instead of the bare
  field label as the message
- login: differentiated errors for unverified email and rate limiting;
  passkey failures show a translated message instead of raw error text
- icon buttons (sidebar module/semester menus, materials grips/menus,
  sidebar resize handle) announce their action, not just the entity name
- calendar right-click menu: viewport-clamped position, Escape-only close,
  menu roles and focus so it is keyboard-operable
- outbox: isNetworkError no longer treats every TypeError as offline
- study session logging failures are logged instead of fully swallowed
- loading.tsx skeletons for program/module routes, due review, settings,
  admin, material viewer and plan pages
- thesis-create type placeholder moved to i18n
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
… parallel module page

- new FormDialog extracts the copy-pasted create/edit dialog shell (dialog,
  pending state, error toast, cancel/submit footer) — deck/quiz/plan create
  and edit dialogs plus the plan generate dialog now share it; the two
  generate dialogs with job progress keep their special structure
- module sub-tab list (segments + icons) lives once in
  src/config/module-tabs.ts, consumed by the sidebar tree and ModuleTabs
- module overview page fetches its independent lookups with Promise.all
  instead of six serial awaits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
Resolves the upload-route conflict: keep main's registerUploadedFile-based
route and re-apply the review branch's additions (per-user rate limit and
the malformed x-file-name guard).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UrdkggYqhNCCZVHyJrdwBR
@veniplex
veniplex merged commit 110bc8a into main Jul 18, 2026
2 checks passed
@veniplex
veniplex deleted the claude/app-review-functionality-usb0y6 branch July 18, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants