- Java 53%
- TypeScript 41.9%
- JavaScript 4%
- CSS 0.6%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .claude | ||
| .run | ||
| backend | ||
| data/handbooks | ||
| docs | ||
| frontend | ||
| tools/campus | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .mcp.json | ||
| CLAUDE.md | ||
| Dockerfile | ||
| img.png | ||
| img_1.png | ||
| img_2.png | ||
| img_3.png | ||
| notizen.md | ||
| README.md | ||
| release.sh | ||
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, jsoupfrontend/: Vite 8, React 19 mit React Compiler, shadcn/ui (Base UI, Tailwind 4), TanStack Router + Query, Rechartsdata/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:
Dockerfileim Repo-Root (Node 24 baut das Frontend nachstatic/, Temurin 25 baut das Jar, Laufzeiteclipse-temurin:25-jreals Nutzerapp, Layer perjarmode=tools). Ein SPA-Fallback liefert für Deep Links (/module/…)index.html, nicht aber unter/api,/mcp,/actuatoroder 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 alsstudienplaner_app, nur DML) undSPRING_FLYWAY_USER/_PASSWORD(Migrationen alsstudienplaner_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:
- Gesamte Testsuite grün (
cd backend && ./mvnw verify,cd tools/campus && npm test,cd frontend && npm run build). ./release.sh: bricht bei nicht sauberem Arbeitsbaum ab, bautlinux/amd64, schiebtgit.leonard-georg.de/leo/studienplaner:<sha>(12 Zeichen, kein:latest) und gibt den sha aus.- 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 = alterTAGplus Restore des Dumps. - Auf CT 106 in
/opt/stacks/studienplaner/.envTAG=<sha>setzen, danndocker 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_lpdeckelt 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_untilam 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.sqlstammen 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
RulePropertiesund 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/campusper 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);rematchordnet dann alle Serien des Semesters neu zu und pflegt je (Serie, Teilleistung) einen Stammtermin mit stabiler ID. Nach einem neuen Handbuch genügtPOST /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 inPlanEvent; 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.