94_Anhang_Glossar_und_Literatur.md enthielt zwei verschiedene Nachschlagewerke
in einer Datei. Jetzt sind es zwei Anhaenge: E = Glossar (92 Eintraege),
F = Literaturverzeichnis (6 Kategorien). Aus 5 Anhaengen werden 6, aus 36
Kapiteldateien 37.
Die Teilung war billig: Die Datei trug bereits zwei eigenstaendige
Ueberschriften mit nichts als einem --- dazwischen, und im ganzen Buch gab es
genau einen {ref:anhang:glossar-literatur} - die Weiter-mit-Zeile im
Spickzettel. Geprueft: keine Inhaltszeile verloren, alle 92 Glossareintraege
auf der neuen Seite, Indexmarken unveraendert bei 328.
Das Seitenleisten-Menue bekommt zwei Eintraege:
Beispielprogramme
Notebooks
Download Notebooks als ZIP <- neu, eingerueckt
Glossar <- neu
Stichwortverzeichnis
Gesamtdokument (eine Seite)
Download als PDF
Der ZIP-Link benutzt dieselbe Konstante wie baue_notebooks_seite() beim
Schreiben des Archivs, damit Link und Datei nicht auseinanderlaufen. Das
Glossar erscheint absichtlich doppelt: hier als Abkuerzung neben dem
Stichwortverzeichnis, und unten in der Gruppe "Anhaenge", weil es ein Anhang
ist.
Die verwaiste anhang-glossar-literatur.html von Hand entfernt - der Build
raeumt alte Seiten nicht ab.
Nebenbefund, in PROGRESS.md festgehalten: OR_HTML_04/assets/site.css ist eine
von Hand gepflegte Quelle, die im als "generiert" beschriebenen Verzeichnis
liegt. Der Build schreibt sie nie. Dasselbe gilt fuer site.js, icons.svg,
highlight.css und plotly.min.js.
--check: 5 Teile, 23 Kapitel, 6 Anhaenge, 296 Abschnitte, 825 Querverweise,
328 Indexmarken, 37 Dateien, 28.544 Zeilen, 305 Hauptueberschriften, keine
Warnung. PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der 403 auf /OR/Notebooks_04/ hatte eine andere Ursache als vermutet: Beim
Eintragen der Website-Adresse ins README hatte ich in der Lesen-Tabelle das
VERZEICHNIS verlinkt. Keine Seite der Website tut das - die Kapitelseiten
zeigen direkt auf Notebooks_04/<name>.ipynb, und das funktioniert. Der
Webserver verweigert Directory-Listing, zu Recht.
Die Frage "gibt es so etwas nicht schon?" war aber berechtigt und hat die
eigentliche Luecke sichtbar gemacht: baue_programme_seite() erzeugt seit
jeher programme.html, eine gestaltete Uebersicht der 76 Programme. Fuer die
25 Notebooks gab es kein Gegenstueck - sie wurden ausgeliefert, aber keine
Seite listete sie.
Neu ist baue_notebooks_seite(), nach demselben Muster und mit denselben
Bausteinen: die Zuordnung Seite -> Notebook kommt fertig aus
baue_notebooks(), Rahmen und Navigation aus baue_seiten_schablone() und
baue_sidebar_html(). Gruppiert nach Vorspann, Kapiteln und Anhaengen; der
einleitende Absatz nennt die drei Wege, ein Notebook auszufuehren.
Dazu ein ZIP mit allen 25 Notebooks (208 KB), byteidentisch ueber zwei
Laeufe. Ein ZIP speichert je Eintrag die Aenderungszeit; ohne festen Wert
entstuende bei jedem Bau eine andere Datei, und da OR_HTML_04/ versioniert
ist, wuechse das Repository bei jedem Lauf. Die Eintraege werden deshalb
sortiert und mit ZipInfo(date_time=(1980,1,1,0,0,0)) geschrieben - dieselbe
Sorgfalt wie svg.hashsalt bei den Diagrammen.
Gegen kuenftige 403 schreiben spiegle_notebooks() und baue_programme_seite()
je eine dreizeilige index.html mit meta refresh in ihr Downloadverzeichnis.
Das behebt zugleich denselben latenten Fall bei programme/.
Ein eigener Fehler, zum zweiten Mal derselbe: Das .replace(",", ".") fuer
deutsche Tausenderpunkte hing am Ende eines mehrzeiligen f-Strings - und in
Python bindet die Methode an die GESAMTE zusammengesetzte Zeichenkette. Aus
"Drei Wege, sie auszufuehren" wurde "Drei Wege. sie auszufuehren". Exakt der
Fehler, vor dem ich in erzeuge_wirkung.py selbst einen Kommentar hinterlassen
hatte. Die Zahl wird jetzt getrennt formatiert.
Geprueft: 25 gelistete Notebooks, 0 tote Links auf der Seite, ZIP mit 25
Eintraegen und identischen Zeitstempeln, Seitenleisteneintrag auf allen 40
Seiten mit Seitenleiste, --check fehlerfrei, PDF unveraendert 759 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sechs Dinge, alle fuer das jetzt oeffentliche Repository.
LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py,
Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer
Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was
sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und
steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit
standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme
haette weiterverwenden duerfen.
COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub,
das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet
Notebooks NUR aus GitHub, Google Drive oder einem Upload. Die URL-Form
colab.research.google.com/github/... ist fest auf GitHub verdrahtet; eine
selbstgehostete Adresse dort einzusetzen ergaebe keinen Link zum eigenen
Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von
GitHub aus und war damit selbst irrefuehrend; er ist ersetzt.
Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen
einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut -
"herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem
Kurs-Image starten".
JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab,
die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht
beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit
Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft,
JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 ->
1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an
127.0.0.1 gebunden.
README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses
und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch
ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container,
schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die
Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links
geprueft: 0 tot.
.gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen
Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in
jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum
System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits
sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die
erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die
Sprachstatistik das Repository als HTML-Projekt.
PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund
festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien
ergaenzt.
Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert,
33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine
weniger - die Colab-Zeile entfaellt in 25 Kapiteln).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Zwei Dinge in einem Commit, weil beide den Plan abschliessen.
ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance
untersuchen" - und sechs davon standen in alter Zaehlung. "8" war in Version
03 das QP/NLP-Kapitel, heute ist es Kapitel 11. Dieselbe Familie wie die
Denkfehler-Verweise aus 6.1a, nur mit einem Wort, das keine der bestehenden
Pruefungen kannte.
Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist
also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus
Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte
Zaehlung bestimmt und am Zusammenhang geprueft.
Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte
CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an
achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde.
Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe
gelandet.
--check kennt jetzt auch "Uebung"/"Übung". Gegengetestet.
PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die
Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen
Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen
finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst,
weil es zusaetzlich Graphviz verlangt.
Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der
Installationstest mit allen drei Solver-Funktionstests bestanden, die 33
pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei -
einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso
traegt wie ausserhalb.
Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image
und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst
"libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler:
USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau
brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im
Dockerfile.
Das Image fuehrt die Programme aus und baut das Buch nicht. Ein
.dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es
enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren
lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest,
nicht durch Weglassen.
Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet.
Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht
zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md,
je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN -
sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel
sind, sondern der Rueckblick auf einen Teil.
Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits
Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung
gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den
Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte
"wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung
gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert.
Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird.
Drei Funde beim Einbau:
* Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3
hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung
blieb stehen.
* 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene
Anwendungen" - sie hat elf.
* Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT
Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf
20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische
direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer
der Kette folgte, uebersprang acht von 23 Kapiteln - darunter
Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das
ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine
Navigationszeile.
Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur
in den Quelldateien. entferne_navigation() streicht sie aus dem
Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus
DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die
Markdown-Dateien im Repository liest, und das wird nach der
Veroeffentlichung der Normalfall sein.
Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check
bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden
Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen.
Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76
Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**",
saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es
Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt.
Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie
gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die
Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen
Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist
keine Tabellenzelle.
Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten
Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n -
nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel
machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also
nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile
schon taten.
nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT
aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte
gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus
{#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23.
Im Quelltext steht "**{loesung} — Titel.**".
Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie
"siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das
QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie
stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden
einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler}
umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12
den Kapitelnamen statt eines Verweises.
Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer.
Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er
steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie
variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch
das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit
dem Titel des Raetsels ankuendigen.
--check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M —
im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele
{loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene
oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit
vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau
null.
Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher
verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118
Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig
schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12
Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder
inhaltlich veraenderte Zeile.
Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz
tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327
Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie
Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit
des Anbieters, nicht an dieser Aenderung).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neuer Abschnitt im Kapitel Unsicherheit plus Chance_Constraints.py (74.
Programm). Das Kapitel hatte Monte-Carlo, Zweistufigkeit und
Worst-Case-Robustheit; die Wahrscheinlichkeitszusage war die fehlende vierte
Antwort - und die, nach der das Management tatsaechlich fragt. Setzt Paket 4
aus Verbesserungen_02.md um.
Beide Wege an DERSELBEN Instanz (Kraftwerkspark, 500 MW gesicherte Zusage):
analytisch als Second-Order-Cone-Bedingung (CLARABEL) und szenariobasiert als
Big-M-MILP (SciPy/HiGHS) - beides in einem Prozess, ohne ortools- oder
highspy-Import.
Drei gemessene Befunde:
* Der Mittelwertplan haelt 50,08 %. Kein Fehler, sondern die Definition des
Erwartungswerts.
* Sicherheit ist konvex bepreist: 56.289 EUR je Prozentpunkt auf dem Weg zu
80 %, 253.848 EUR zwischen 95 und 99 % - das 4,5-fache. Das Programm
rechnet die Tabelle selbst aus, statt sie zu behaupten.
* Die Zusage gilt nur fuer die unterstellte Verteilung: Der 95-%-Plan haelt
gemessen 87,44 %, sobald die Testverteilung eine Kaeltewelle mit
Dunkelflaute enthaelt, in der alles zugleich einbricht - auch das
Gaskraftwerk. In einer reinen Normalwelt liefert derselbe Plan 94,97 %.
Zwei eigene Fehlgriffe, beide durch Messen aufgefallen und korrigiert: Die
erste Kostentarierung ergab eine entartete Loesung (alles ins Gaskraftwerk),
womit die Kovarianzmatrix wirkungslos war - und gerade sie begruendet die
Kegelform. Und der erste Kaelteeinbruch traf nur Wind und Sonne; der SOC-Plan
hatte die ohnehin herausgehalten und war zufaellig robust, das Argument trug
nicht.
Ehrlich berichtet statt geglaettet: Die Szenariomethode ueberanpasst. Ueber
zwoelf Laeufe (S = 200, 400, 800) lag die tatsaechliche Quote zwischen
93,14 % und 96,71 %, und die Spanne wurde von S = 400 auf 800 wieder
breiter. Beide Solver bestaetigen optimal bei identischen Kosten - echte
Ueberanpassung, kein Solverartefakt. Steht als Warnkasten im Abschnitt.
Die Laufzeitmessung ist aus der Ausgabe entfernt: Eine Wanduhrzeit ist nie
byteidentisch reproduzierbar (2,2 s / 2,3 s zwischen zwei Laeufen) und haette
Regel 4 dauerhaft gebrochen. Danach drei Laeufe zeichengleich, und die
abgedruckte Ausgabe stimmt mit dem Lauf des extrahierten Programms ueberein.
Mitgezogen: Kapitelkopf, Lernziele, Uebersichtstabelle (drei -> vier
Ansaetze), Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, eine
neue Uebungsaufgabe und ihre Loesung in Anhang A.
pyproject.toml korrigiert: cvxpy lag in [finance], wird aber von 13
Programmen gebraucht, darunter dreien in diesem Kapitel - jetzt in der
Grundausstattung. Es zieht kein highspy nach, der Solverkonflikt bleibt auf
[large-scale] beschraenkt.
Stand: 293 Abschnitte, 712 Querverweise, 327 Indexmarken, 74 Programme (0
Fehler), 33 pytest-Tests, PDF 725 Seiten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python"
(Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im
uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch
Version_03 (eingefroren) verwaltet und Version_04/ ignoriert.
Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit
der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen
Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist.
Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise,
325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren,
25 Notebooks, PDF mit 715 Seiten.
Zusaetzlich in diesem Commit:
* pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api,
figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt
unveraendert daneben bestehen. ortools steht in der Grundausstattung,
highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der
schlanken Installation gar nicht erst auftreten.
* Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py
importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo
steht in requirements.txt, wird aber von keinem Programm importiert,
sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in
[empfehlungen]).
* NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es
ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden
Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen.
* PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den
tatsaechlichen aus erzeuge_titelseite.py korrigiert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>