No description
  • Java 53%
  • TypeScript 41.9%
  • JavaScript 4%
  • CSS 0.6%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
l.georg 91e15ac53a Document prod operation, release and crawling against prod
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 23:58:19 +01:00
.claude Add the React frontend for planning modules against the rules 2026-10-25 14:05:54 +01:00
.run Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
backend Add prod profile and SPA fallback for client-side routes 2026-11-03 23:58:19 +01:00
data/handbooks Import the KIT module handbook and check study plans against its rules 2026-10-25 13:27:45 +01:00
docs Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
frontend Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
tools/campus Read crawler backend URL from STUDIENPLANER_URL 2026-11-03 23:58:19 +01:00
.dockerignore Add Dockerfile and release script for the amd64 image 2026-11-03 23:58:19 +01:00
.gitattributes Import the KIT module handbook and check study plans against its rules 2026-10-25 13:27:45 +01:00
.gitignore Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
.mcp.json Add an MCP server for querying the catalog and planning modules 2026-10-25 16:24:12 +01:00
CLAUDE.md Document prod operation, release and crawling against prod 2026-11-03 23:58:19 +01:00
Dockerfile Add Dockerfile and release script for the amd64 image 2026-11-03 23:58:19 +01:00
img.png Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
img_1.png Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
img_2.png Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
img_3.png Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
notizen.md Move timetable HTML parsing to Playwright crawler and add plan events 2026-11-03 23:43:35 +01:00
README.md Document prod operation, release and crawling against prod 2026-11-03 23:58:19 +01:00
release.sh Add Dockerfile and release script for the amd64 image 2026-11-03 23:58:19 +01:00

Studienplaner

Studienplanung für den KIT Informatik Master (SPO 2023): Modulhandbuch als Datenbank, Regeln als Business-Logik, später Stundenplan.

  • backend/: Spring Boot 4 / Java 25, Postgres 18, Flyway, jsoup
  • frontend/: Vite 8, React 19 mit React Compiler, shadcn/ui (Base UI, Tailwind 4), TanStack Router + Query, Recharts
  • data/handbooks/: Quelldateien der Modulhandbücher (HTML-Export der Fakultät)

Modulhandbuch importieren

cd backend
./mvnw spring-boot:run -Dspring-boot.run.arguments="--import=../data/handbooks/88-079-H-2023_v1_2026-09-09_de.html --spring.main.web-application-type=none"

Die Entwicklungs-DB startet über compose.yaml automatisch und läuft danach weiter (psql -h localhost -p 15432 -U studienplaner, Passwort studienplaner). Ein erneuter Import derselben Datei ersetzt den alten Stand.

Entwickeln

cd backend && ./mvnw spring-boot:run      # :8080, startet Postgres per compose.yaml
cd frontend && npm install && npm run dev  # :5173, leitet /api an :8080 weiter

Das Frontend nutzt TanStack Router (file-based, frontend/src/routes/, routeTree.gen.ts wird vom Vite-Plugin erzeugt):

Route Inhalt
/ (?tab=semester, stundenplan, vorleistungen) Plan: Übersicht mit Regeln, Semester, Stundenplan mit Angebot, Vorleistungen
/module (?q=…&field=…&term=WS&core=true) Modulkatalog mit Filtern in der URL; q findet auch Lehrveranstaltungen
/module/$code Moduldetails, Einplanen
/ergaenzungsfaecher alle Ergänzungsfächer mit LP-Rahmen und Stand im Plan
/ergaenzungsfaecher/$code Wahlregeln, Ansprechpartner, Module zum Einplanen
/profile alle Studienprofile mit Bedingungen und Fortschritt des gewählten Plans
/profile/$code Bedingungen als Checkliste, Profil wählen, Listen mit Modulen zum Einplanen

Der gewählte Plan gilt für alle Seiten und wird im Browser gemerkt.

Terminliste einer Rubrik aus dem Vorlesungsverzeichnis holen (Playwright mit dem installierten Chrome, sonst einmal npx playwright install chromium und channel in fetch.mjs entfernen):

cd tools/campus && npm install
npm test                            # Auslesen der Terminliste gegen gespeicherte Seiten (test/fixtures/)
node fetch.mjs 1.4 WS26             # Rubrik unter „KIT-Fakultät für Informatik“, Semester; JSON landet in out/
node fetch.mjs 1.9 WS26 --preview   # zusätzlich gegen /api/timetable-import/preview prüfen (Backend muss laufen)
node fetch.mjs 1.9 WS26 --import    # Serien speichern und zuordnen
node fetch.mjs 3. WS26 --list       # Veranstaltungsliste statt Terminliste, zusätzlich als JSON
node fetch.mjs 'https://campus.studium.kit.edu/events/catalog.php#!campus/all/field.asp?gguid=…&view=list' --preview
node fetch.mjs overview [SS26]      # Startseite: alle Semester (tguid) und Inhaltsverzeichnis (gguid) als JSON
node fetch.mjs crawl WS26 --test    # alle IDs: Einträge → Rubriken → Veranstaltungen; --test nur je 5 × 5
node fetch.mjs crawl WS26 --import  # alle Fakultäten: Rohdaten je Rubrik ans Backend, am Ende einmal rematch
# Backend-URL per STUDIENPLANER_URL (Standard http://localhost:8080), Prod siehe „Betrieb (Prod)“

Die zweite Run-Konfiguration „Campus Fetch Mathematik“ holt die Veranstaltungsliste „3. Mathematik Lehrveranstaltungen“. Aus einer URL nimmt das Skript nur gguid und tguid; ohne --list kommt immer die Terminliste, egal welche Ansicht verlinkt ist. Die gguid gilt nur für ihr Semester.

crawl geht alle Einträge der Startseite durch, liest aus jedem den Rubrikbaum und aus jeder Rubrik die Veranstaltungsliste (alle Seiten, 200 je Seite) und die Terminliste und schreibt out/WS26-ids.json (mit --test …-ids-test.json): je Rubrik gguid, Titel, Pfad im Baum, die Veranstaltungen mit gguid, LV-Nr., Titel und Art und die Terminserien (slots, wie sie ans Backend gehen). Mit --preview/--import geht jede Terminliste an das Backend; ein Fehler dort bricht den Crawl nicht ab. Zwischen den Abrufen wartet es 300 ms; die Datei wird nach jedem Eintrag geschrieben. Run-Konfiguration „Campus Crawl Test“. Die Zahl am Rubrikbaum kann über der Länge der Liste liegen (z. B. IIIT 20 gegenüber 17), das zählt das Vorlesungsverzeichnis selbst so.

Betrieb (Prod)

Prod läuft als Container auf CT 106 unter http://10.9.0.2:8090 (nur über VPN), die DB auf dem gemeinsamen Postgres-18-Cluster (CT 107). Die echten Pläne leben in Prod; die Dev-DB (localhost:15432) ist nur eine Kopie zum Ausprobieren und lässt sich bei Bedarf aus einem pg-backup-Dump von CT 107 neu befüllen.

  • Image: Dockerfile im Repo-Root (Node 24 baut das Frontend nach static/, Temurin 25 baut das Jar, Laufzeit eclipse-temurin:25-jre als Nutzer app, Layer per jarmode=tools). Ein SPA-Fallback liefert für Deep Links (/module/…) index.html, nicht aber unter /api, /mcp, /actuator oder für Dateien.
  • Profil prod (application-prod.properties): kein Docker Compose, Forwarded-Header, Pool 10. Zugangsdaten nur per Env: SPRING_DATASOURCE_URL/_USERNAME/_PASSWORD (App als studienplaner_app, nur DML) und SPRING_FLYWAY_USER/_PASSWORD (Migrationen als studienplaner_owner).
  • Der MCP-Server ist auch in Prod an (http://10.9.0.2:8090/mcp), solange die App nur per VPN erreichbar ist.

Release:

  1. Gesamte Testsuite grün (cd backend && ./mvnw verify, cd tools/campus && npm test, cd frontend && npm run build).
  2. ./release.sh: bricht bei nicht sauberem Arbeitsbaum ab, baut linux/amd64, schiebt git.leonard-georg.de/leo/studienplaner:<sha> (12 Zeichen, kein :latest) und gibt den sha aus.
  3. Enthält der Release eine neue Flyway-Migration: vorher auf CT 107 einen Dump ziehen (systemctl start pg-backup.service). Migrationen laufen nur vorwärts; Rollback = alter TAG plus Restore des Dumps.
  4. Auf CT 106 in /opt/stacks/studienplaner/.env TAG=<sha> setzen, dann docker compose pull && docker compose up -d.

Crawler gegen Prod (Termine landen dann direkt in den echten Stammdaten):

cd tools/campus && STUDIENPLANER_URL=http://10.9.0.2:8090 node fetch.mjs crawl WS26 --import

Lokal testen: docker buildx build --platform linux/amd64 -t studienplaner:local --load ., dann mit SPRING_PROFILES_ACTIVE=prod und den Env-Variablen gegen jdbc:postgresql://host.docker.internal:15432/studienplaner.

API

Ohne program-Parameter gilt der Studiengang des zuletzt importierten Handbuchs. Fehler kommen als Problem Details (application/problem+json), Health unter /actuator/health.

Methode Pfad Zweck
GET /api/catalog Handbuch-Stand, Stammmodule
GET /api/catalog/fields, /api/catalog/fields/{code} Fächer mit LP-Rahmen und Wahlblöcken
GET /api/catalog/modules?field=&q=&core=&term=WS Module suchen (q auch in Namen von Lehrveranstaltungen), mit ihren Lehrveranstaltungen
GET /api/catalog/modules/{code} Detail: Teilleistungen, Veranstaltungen, Voraussetzungen, Texte
GET/POST /api/plans Pläne auflisten / anlegen (name, startTerm, startYear)
GET/PUT/DELETE /api/plans/{id} Plan lesen, umbenennen/verschieben, löschen
PUT/DELETE /api/plans/{id}/entries/{moduleCode} Modul einplanen (semesterNo, fieldCode, status, grade, additional für Zusatzleistungen, courses für Lehrveranstaltungen je Semester); semesterNo: 0 = Mastervorzug
PUT/DELETE /api/plans/{id}/prior/{code} Vorleistung aus dem Bachelor (Modul- oder Teilleistungskennung)
GET /api/plans/{id}/evaluation Regelprüfung plus Kennzahlen je Fach und Semester
GET /api/plans/{id}/timetable?semester=N Stundenplan eines Fachsemesters mit Überschneidungen
GET /api/parts/{code}/slots Stammtermine einer Teilleistung (nur lesen, kommen aus dem Crawler)
GET/PUT /api/parts/{code}/schedule Teilleistung „ohne feste Wochentermine“
POST /api/plans/{id}/proposals/adopt, …/dismiss, …/restore Vorschläge eines Fachsemesters (semesterNo + slotIds oder moduleCode) übernehmen, ablehnen, wiederherstellen; eine Übungsgruppe übernehmen ersetzt die bisherige
GET/POST/PUT/DELETE /api/plans/{id}/events[/{eventId}] meine Termine (Plantermine); anlegen mit dates oder date bzw. firstDate/lastDate
POST /api/plans/{id}/events/{eventId}/sync Änderungen aus dem Vorlesungsverzeichnis übernehmen (accept: true) oder ignorieren, keys null = alle
GET/DELETE /api/plans/{id}/events/orphans?moduleCode= Termine eines verschobenen oder entfernten Moduls, die nicht mehr passen
POST /api/timetable-import/preview, /api/timetable-import[?rematch=false] Terminserien einer Rubrik (JSON vom Crawler: term, year, rubric, slots) prüfen bzw. als Rohdaten speichern und zuordnen
POST /api/timetable-import/rematch?term=WS&year=2026 alle gespeicherten Serien des Semesters neu den Teilleistungen zuordnen
PUT/DELETE /api/plans/{id}/blockers/{key} eigene feste Termine (Pendeln, …) und Arbeitszeiten (work: true, je Semester, kollidieren nur weich)
GET /api/profiles, /api/profiles/{code} Studienprofile mit Bedingungen und Listen
PUT /api/plans/{id}/profile Profil wählen (profileCode, null = keins)
PUT/DELETE /api/plans/{id}/profile-confirmations/{profileCode}/{no} nicht prüfbare Bedingung (Thema der Masterarbeit) von Hand bestätigen
GET /api/plans/{id}/profiles Fortschritt des Plans in allen Profilen

Regeln

Jede Regel ist eine Spring-Bean (rules/*Rule.java) und liefert Befunde mit drei Stufen: VIOLATION (nicht durch Hinzufügen behebbar), MISSING (Mindestwert noch nicht erreicht), WARNING.

Quelle ist jeweils das Handbuch; was nur im Fließtext des Studienplans steht, ist mit (Text) markiert, was nur in der Studien- und Prüfungsordnung steht (Amtliche Bekanntmachung 2023 Nr. 22), mit (SPO).

  • studiengang: Pflichtbereiche, genau 2 Vertiefungsfächer, genau 1 Ergänzungsfach
  • fach: LP-Rahmen je Fach (VF 15–58, Wahlbereich 6–49, Ergänzungsfach 9–18, ÜQ 2–6, Masterarbeit 30) und die Wahlblöcke darin. Der Text nennt „bis zu 52 LP“ je VF; es gilt die Tabelle mit 58 LP
  • vorlesungen: mind. 10 LP aus Vorlesungen je Vertiefungsfach ohne Stammmodule, Telematik 8 (Wahlinformationen)
  • randbedingungen: mind. 3 LP Seminare, mind. 6 LP Praktika, zusammen 12–18 LP; nur VF und Wahlbereich (Text)
  • stammmodule: mind. 4 Stammmodule, die nicht schon vor dem Master geprüft wurden (Text)
  • gesamt: genau 120 LP; mehr nur als Zusatzleistung (additional), davon höchstens 30 LP (Text)
  • voraussetzungen: die modellierten Voraussetzungen inkl. Ausschlüssen gegen Bachelor-Vorleistungen
  • eintrag: Modul im Fach anrechenbar, nicht schon vor dem Master geprüft, Erstverwendungsdatum, Turnus WS/SS
  • vertiefungsfaecher: beide VF zusammen höchstens 73 LP, einmal durch das letzte Modul überschreitbar (SPO § 19)
  • fristen: alles bis Ende des 7. Fachsemesters, Hinweis bei Modulen im 7. (SPO § 9)
  • stundenplan: Überschneidungen von Terminen untereinander und mit eigenen Terminen (nur Hinweise)

Bewusst nicht abgebildet: der ILIAS-Kurs „Gute wissenschaftliche Praxis“, Ergänzungsfach „Anderes Fach“ per Antrag, inhaltliche Äquivalenz externer Bachelor-Module (als Vorleistung eintragen), die Empfehlung, LP gleichmäßig auf die Semester zu verteilen (steht als Kennzahl in der Auswertung).

Alle Zahlen sind über studienplaner.rules.* konfigurierbar.

Studienprofile

Acht Profile (z. B. „Master Informatik mit Profil IT-Sicherheit“) bringen ein Zertifikat zusätzlich zum Zeugnis. Sie sind freiwillig: der Fortschritt steht in PlanEvaluation.profile und ändert complete nicht (ProfileEvaluator, keine PlanRule). Die Regeln stehen nicht im Handbuch, sondern auf den Profilseiten der Fakultät (https://www.informatik.kit.edu/masterprofile.php); sie sind in V8__profile.sql von Hand übertragen (Stand 25.10.2026):

  • MODULES: mind. LP und/oder Anzahl aus Listen; ONE_OF: eine der Alternativen (z. B. eines von mehreren Ergänzungsfächern); TOTAL: „insgesamt mind. 45 LP aus 2.–4.“; MANUAL: Thema der Masterarbeit, von Hand bestätigt
  • Ein Modul zählt für höchstens eine MODULES/ONE_OF-Bedingung (wie im Antragsformular); bei Modulen in mehreren Listen wird so verteilt, dass möglichst viele Bedingungen erfüllt sind. LP kommen aus dem Modulhandbuch, nicht aus den Profiltabellen.
  • Listen enthalten Module oder ganze Fächer; max_lp deckelt eine Liste (Robotik: max. 6 LP aus einer Teilliste).
  • Ein Stammmodul, das schon im Bachelor geprüft wurde (Vorleistung, auch unter früherer Kennung), erfüllt seine Bedingung; wo die Profilseite es nennt, steigt dafür das Minimum einer Liste (Software Engineering, Visual Computing: +6 LP).
  • „wählbar bis …“ der Profilseite steht als valid_until am Eintrag; später geplante Module zählen nicht (Hinweis).
  • Nicht übernommen: Module, die nicht mehr im Handbuch 2026 stehen (in der Migration als Kommentar gelistet), und die Einzelfall-Module aus BWL/Recht im Wahlblock IT-Sicherheit (nur Hinweis).

Bekannte Lücken

  • Stammmodule stehen im Handbuch nur als Bild (im HTML-Export ein toter Link). Die 11 Module in V2__stammmodule.sql stammen aus der offiziellen Liste 2026 (data/handbooks/stammmodule_2026.png) und müssen bei einem neuen Handbuch von Hand nachgezogen werden.
  • Regeln aus Fließtext ((Text) oben) sind von Hand nachgebaut. Bei einem neuen Handbuch den Abschnitt „Studienplan und Struktur des Master-Studiengangs“ gegen RuleProperties und die Regeln prüfen.
  • Vorlesungsanteil: Ob eine Teilleistung eine Vorlesung ist, folgt aus den Typen ihrer Lehrveranstaltungen. Sind keine gelistet (Turnus-Lücke im aktuellen Semester), wird aus dem Namen geschätzt und gewarnt.
  • Wahl-Teilleistungen innerhalb eines Moduls werden noch nicht eingeplant; es zählen die Pflicht-Teilleistungen.
  • Lehrveranstaltungen sind über Teilleistung und Namen erkannt (LV-Nummern wechseln jedes Semester). Ohne eigene Wahl liegen die Lehrveranstaltungen eines Moduls, die im Turnus seines Semesters angeboten werden, in diesem Semester. Teile in anderen Semestern (z. B. „Simulation von Verkehr“ aus Verkehrswesen für Informatik I im SS) legt man im Einplanen-Dialog selbst fest, denn meist sind SS- und WS-Einträge einer Teilleistung nur Alternativen. Benennt ein neues Handbuch eine Lehrveranstaltung um, findet eine eigene Wahl sie nicht mehr. Termine ohne eindeutige Lehrveranstaltung (Übung mit unbekannter LV-Nummer bei mehreren Veranstaltungen) gelten für die ganze Teilleistung im Semester des Moduls.
  • Termine enthält weder Handbuch noch SPO. Im Vorlesungsverzeichnis hat jede Rubrik (z. B. „1.4 Stammvorlesungen“) einen Reiter Terminliste. Die liest tools/campus per Headless-Chrome aus (extract.mjs, siehe „Entwickeln“) und schickt die Terminserien als JSON an /api/timetable-import: IDs von Serie, Veranstaltung und Raum, Rhythmus, Wochentag, Zeiten, alle Einzeldaten und ob Termine entfernt wurden (rotes *). Das Backend parst kein HTML. LV-Nummern werden nur innerhalb des gewählten Semesters abgeglichen (KIT vergibt sie je Semester neu). Zuordnung über die LV-Nummer, sonst über den Titel („Übungen zu X“ → X); mehrere Übungstermine einer LV werden zu wählbaren Gruppen (nur wöchentliche/14-tägliche Serien; Einzeltermine sind feste Termine). Der Import speichert zuerst alles unverändert als Rohdaten (campus_series, …_date, …_rubric); rematch ordnet dann alle Serien des Semesters neu zu und pflegt je (Serie, Teilleistung) einen Stammtermin mit stabiler ID. Nach einem neuen Handbuch genügt POST /api/timetable-import/rematch?term=WS&year=2026, ohne neu zu crawlen. Jeder Termin trägt alle konkreten Daten; sie entscheiden über Kollisionen (Weihnachtspause, entfallene Termine), Rhythmus und Wochentag sind nur Beschriftung. Gelb statt rot ist eine Überschneidung, die weniger als ein Drittel der Termine des längeren betrifft (z. B. ein Einzeltermin gegen eine Vorlesung). Blocker und Arbeit haben keine Daten und gelten an jedem passenden Wochentag. Termine gelten nur in ihrem eigenen Semester; aus anderen Jahren wird bewusst nichts angenommen, weil sie sich jedes Semester ändern.
  • Stammdaten und Plantermine (V13): Stammtermine (course_slot) schreibt nur der Import, sie gelten für alle Pläne. Mein Kalender besteht aus Planterminen (plan_event), die zum Plan gehören und frei änderbar sind (Zeit, Raum, einzelne Daten, z. B. nur die Termine der eigenen Praktikumsgruppe). Stammtermine geplanter Module erscheinen als Vorschlag (gestrichelt, zählen schon für Überschneidungen), bis man sie übernimmt (✓) oder ablehnt (✕, dann zählen sie nicht mehr; plan_dismissed_series). Von Hand erfasste Termine sind immer Plantermine: einmalig mit Datum oder jede (zweite) Woche vom ersten bis zum letzten Datum. Übernommene Termine hängen über die Serie (ohne Fremdschlüssel) an ihrem Stammtermin und merken sich dessen zuletzt gesehenen Stand (base_*): Ändert ein Crawl den Stamm, zeigt der Termin einen Hinweis, aber nur, wenn die Änderung ihn betrifft (Dreiwegevergleich in PlanEvent; wer den Raum schon selbst korrigiert hat, bekommt keinen Hinweis, selbst abgewählte Daten ebenso wenig). Verschwindet die Serie, bleibt der Termin mit Hinweis „im Vorlesungsverzeichnis entfernt“. Wird ein Modul verschoben oder entfernt, fragt das Frontend, ob seine Termine aus dem Kalender sollen; sonst bleiben sie mit Hinweis stehen (ORPHAN).
  • Zweisemestrige Module (Practice of Research, Masterarbeit) erscheinen im Stundenplan nur in ihrem Startsemester. Urlaubssemester (Kalendersemester ≠ Fachsemester) sind nicht abgebildet.

MCP-Server

Das Backend bietet unter http://localhost:8080/mcp (Prod: http://10.9.0.2:8090/mcp, nur VPN; Streamable HTTP, ohne Authentifizierung) einen MCP-Server: get_catalog_info, list_fields, search_modules, get_module, list_plans, get_plan, evaluate_plan, get_timetable, get_offer, plan_module, unplan_module, list_profiles, get_profile, set_plan_profile. In diesem Ordner ist er per .mcp.json für Claude Code eingetragen; das Backend muss laufen.