| / | / \ / | / ___ \ \ / / \ | | | |
| |/| | / _ | | _ | | | \ \ /\ / / _ \ | | | |
| | | |/ ___ \ || || | |_ \ V V / ___ | || |
|| |// __|___ _/_// _____|_____|
MAGIC-Wall ersetzt die Pinnwand mit Karteikärtchen im Lehrerzimmer durch eine digitale, gemeinsam nutzbare Stundenplan-Wand. Mehrere Personen bearbeiten denselben Plan gleichzeitig — mit der Maus am Schreibtisch oder mit dem Finger auf einem großen Touchscreen im Lehrerzimmer. Jede Änderung erscheint sofort bei allen anderen, Konflikte werden live erkannt statt hinterher mühsam gesucht, und ein KI-gestützter Planer kann auf Wunsch ganze Stundenpläne automatisch vorschlagen.
Die folgenden Ausschnitte stammen von einer Beispielschule mit fünf Klassen, sieben Lehrkräften und sechs Räumen — nicht aus der echten Anwendung eines Kunden, aber mit echten Screenshots der laufenden App.
Mit einem Klick auf der Werkzeugleiste wechselt die Wand zwischen Klassen-, Lehrer- und Räume-Ansicht — dieselben Daten, einmal nach Klassen, einmal nach Lehrkräften und einmal nach Räumen sortiert. Wer schnell sehen will, ob ein Raum gerade frei ist oder wie voll der Tag einer Lehrkraft ist, wechselt einfach die Ansicht, ohne irgendetwas neu einzugeben.
Ist eine Lehrkraft versehentlich in zwei Klassen gleichzeitig eingeplant, oder ist ein Raum doppelt belegt? MAGIC-Wall markiert das sofort mit einem roten Rahmen und nennt das betroffene Fach oder den Raum beim Namen — in allen drei Ansichten gleichermaßen (oben zu sehen: Herr Meyer gleichzeitig in 5a und 5b, die Turnhalle gleichzeitig von 5a und 6a belegt).
Beim Ziehen einer Stunde an eine neue Stelle zeigt die Wand außerdem live, ob das Ziel frei ist, dort etwas ersetzt oder getauscht werden kann, oder ob der Drop einen Konflikt erzeugen würde — nicht erst nach dem Loslassen.
Reale Stundenpläne sind selten so einfach wie "eine Klasse, eine Lehrkraft, ein Fach". MAGIC-Wall bildet die üblichen Sonderfälle direkt auf der Wand ab, ohne Sonder-Dialoge:
- Doppelstunden verschmelzen zu einem großen, durchgehenden Kästchen (siehe "1+2" oben links in der Klassen-Ansicht).
- Team-Teaching: zwei Lehrkräfte unterrichten dieselbe Klasse zur gleichen Zeit — beide erscheinen nebeneinander in einer geteilten Zelle.
- Kopplungen: zwei Klassen teilen sich einen Zeit-Slot klassenübergreifend (klassisch: Religion/Ethik) — ein kleines Verbindungssymbol zeigt die Kopplung in beiden Klassen an.
Wer hat noch Kapazität frei, wer liegt schon über der Soll-Stundenzahl? Die Lehrkräfte-Seitenleiste listet für jede Lehrkraft die Differenz zwischen Soll- und Ist-Stunden auf einen Blick — such- und nach Fach filterbar.
Der Planer erstellt auf Wunsch einen kompletten Stundenplan automatisch. Wichtig dabei: Die KI platziert keine einzige Stunde selbst. Das übernimmt ein klassischer Constraint-Solver, der harte Regeln — "keine Freistunden", "Unterricht in Doppelstunden", "an jedem Unterrichtstag mindestens 4 Stunden" und viele mehr — garantiert einhält. Die KI wird nur dazu genutzt, Regeln in normaler Sprache zu verstehen (z. B. „Frau Wolf kann mittwochs nur nachmittags") und Fragen zum bestehenden Plan zu beantworten; ein Regelvorschlag, den die KI nicht sicher einordnen kann, wird nie stillschweigend übernommen, sondern zur Bestätigung vorgelegt.
Der Planer schlägt Lösungen live vor, während im Hintergrund weitergesucht wird: Auf der Wand erscheint sofort eine Vorschau (halb durchsichtige "Geister"-Stunden), inklusive einer kleinen Bewertung, wie gut der Vorschlag die eigenen Wunschregeln erfüllt. Erst ein bewusster Klick auf "Anwenden" übernimmt den Plan — bis dahin ist auf der Wand nichts verändert, und jede Übernahme lässt sich mit Strg+Z rückgängig machen.
Darüber hinaus bringt MAGIC-Wall unter anderem mit: eine frei einstellbare "Eigene Ansicht" mit gemischten Klassen-/Lehrer-/Raum-Spalten, Suchen & Filtern nach Lehrkraft/Raum/Klasse, eine Vorplanung (Soll-Stunden je Klasse unabhängig vom fertigen Plan), Stunden-Attribute für Halbjahr/Wochen-Rhythmus, Undo/Redo für jede Wand-Änderung, und einen Offline-Modus, der auch bei Netzwerkausfall weiterarbeitet und beim Wiederverbinden automatisch abgleicht.
Der folgende Teil richtet sich an Entwicklerinnen und Administratoren (technische Referenz, auf Englisch).
Select "Web (Flutter + Proxy)" in the VS Code launch dropdown and press F5.
This starts a reverse proxy (web-deploy-helper/dev-proxy.js) on port 8080 before launching Flutter. Chrome opens at http://localhost:8080 so that /db routes to CouchDB (port 5984) without CORS issues. Flutter hot-reload WebSockets are forwarded transparently.
CouchDB must be running locally on port 5984 before pressing F5.
First run: npm install is called automatically inside web-deploy-helper/.
Run everything:
flutter test
flutter analyzedart_test.yaml (project root) pins concurrency: 1. Most suites spin up a per-test
LocalDartCouchServer (Drift/SQLite); under high parallel fan-out on
many-core machines the SQLite engines starve and throw flaky
SqliteException(5): database is locked (even at concurrency: 4). Each test
uses its own database file, so this is contention, not a logic bug —
serializing the suites keeps flutter test deterministically green for a
modest wall-clock cost.
Some behaviour only exists under the web compilers — dart2js has JavaScript
number semantics (e.g. bitwise shifts truncate to 32 bits) and the solver's
web path (solveCooperatively / _webCooperative) only runs with kIsWeb.
Browser-compatible suites (no dart:io, e.g. the test/planner/solve_*
tests) can be run compiled to JS in headless Chrome:
CHROME_EXECUTABLE="C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" \
flutter test --platform chrome test/planner/solve_cancel_until_cancelled_test.dartSet CHROME_EXECUTABLE explicitly on Windows — without it the runner can
hang forever in the loading … phase waiting for a browser it never finds
(no error is printed). Expect several minutes of silent dart2js compilation
before the first test line appears. Suites that read fixture files via
dart:io (testdata_replan_test.dart, etechnik_solver_bug_test.dart)
cannot run on the browser platform.
- Model / data-layer unit tests (
test/wall/timetable_data_model_test.dart,test/wall/multi_teacher_group_test.dart,test/db/coupling_consistency_test.dart,test/repositories/…) — the decision logic (drop classification, coupling invariants, conflict detection) and the reactive model. - Undo/redo — model-level journal/replay in
test/wall/undo_redo_test.dartundo_redo_extra_test.dart; widget-level wiring (keys, toolbar buttons, cross-preset repaint) inundo_redo_widget_test.dart.
- Wall widget tests — the full drag/drop interaction matrix and render/tap
surface:
wall_drag_widget_test.dart— solo drags (move/copy/replace/swap/als-2./ uniform-rule/trash/conflict).wall_drag_coupled_test.dart— coupled (full) + split ("Paare öffnen") drags.wall_drag_group_test.dart— multi-teacher group + collapsed-pair drags.wall_drag_misc_test.dart— Lehrer-sidebar drags, coupling⟂multi-teacher exclusivity, conflict-prediction icons, cross-preset consistency.wall_render_widget_test.dart— Plättchen rendering, badges, onboarding, toolbar toggles, teacher picker / context menu, Delete key.wall_golden_test.dart— one golden (test/wall/goldens/wall.png). Regenerate on intentional visual changes:flutter test --update-goldens test/wall/wall_golden_test.dart.
pumpWall(tester, …) seeds a LocalDartCouchServer, registers DI, init()s a
TimetableDataModel, sizes the surface so the whole wall renders at scale 1.0,
and pumps WallScreen.withModel(model) (a @visibleForTesting constructor that
skips the async bootstrap). dragPlaettchen / beginDrag / endDrag drive the
custom ResponsiveDraggable. Cells are located by cellFinder(CellKey) /
plaettchenFinder(stundeId) (stable ValueKeys on _CellView / each teacher
body). State assertions read model.stundeById(...); affordances assert the
stripe/kind icons; blocked targets assert expectGreyed (Opacity 0.35).
Non-obvious rules baked into the harness (change with care):
- Inject a
testChangesstream. Without it the model subscribes to the live continuousdb.changes()feed, which keeps the async loop busy and hangspumpAndSettleforever. The harness never usespumpAndSettle(drag/hover states pulse continuously) — it settles a fixed number of frames. - Real I/O needs
runAsync.LocalDartCouchServerdoes real file I/O, which doesn't progress on bare awaits insidetestWidgets. Boot/seeding, the pointer release on a drop, write-triggering taps/keys (tapAsync/sendKeyAsync), the consistency check, and teardown all run insidetester.runAsync. A drop released during a normalpumpwould stall its optimistic_db.putand get reverted by the model's fail-safe reload. - Sweep the drag, don't jump. Drags move to the target in steps. A single
moveTokeeps the hit-tested drop-target list constant, and Flutter's_DragAvataronly re-runsonWillAcceptwhen that list changes — the first evaluation happens at drag-start with the (touch-deferred) session not yet published, so a straight jump leaves the target rejected. - Consistency check runs in teardown for every test (
SanityChecker.runAll), in addition to explicitassertDbConsistentcalls at key points.
The screenshots used above are generated, not hand-captured — a real-window
GUI walk-through (integration_test, LiveTestWidgetsFlutterBinding) over a
hand-curated demo school, exercised the same way [plan_flow_test.dart]
(integration_test/plan_flow_test.dart) exercises the Planer flow. Regenerate
after an intentional visual change:
flutter test integration_test/readme_screenshots_test.dart -d windowsOutput lands in build/screenshots/*.png; copy the ones you want into
docs/screenshots/.
The compose stack runs CouchDB, an Ollama LLM container, and an nginx
container that serves the Flutter app and proxies /db → CouchDB and
/ollama → Ollama. No separate CORS configuration needed (everything is
same-origin).
1. Build the image + deploy bundle (dev machine)
./build_docker_image.shThis runs flutter pub get + flutter build web --release --wasm, builds
the Docker image, and assembles everything the target server needs into
web_deploy_files/: the image saved as a tarball
(stundenplanner-<version>.tar.gz), docker-compose.yml, the CouchDB config
template, and the helper scripts from web-deploy-helper/
(load_image.sh, start_containers.sh, stop_containers.sh,
update_image_and_restart.sh, check_local_ini.sh, update_local_ini.sh,
ollama_entrypoint.sh).
2. Copy web_deploy_files/ to the target server
3. Before the first deploy, edit docker-compose.yml inside the copied
bundle (not the repo's — the bundle's copy is what actually runs):
COUCHDB_USER/COUCHDB_PASSWORDon thedbservice — the initial CouchDB admin credentials, applied only on the very first start.OLLAMA_API_KEYon thestundenplannerservice — must match whatever gets entered later in the app's Plan-KI settings ("Schulweit geteilt"); this key guards the/ollamaendpoint (see below).- The exposed port (
8110:80), if needed.
4. Install / start (on the server, from inside the bundle directory)
./start_containers.shCreates the bind-mounted data directories, seeds couchdb_config/local.ini
from the bundled template on the very first run (never overwrites it
afterwards — CouchDB writes state, like the hashed admin password, into that
file), and starts the stack with docker compose up -d.
5. Updating later: rebuild (./build_docker_image.sh) and ship a fresh
web_deploy_files/, then on the server run
./update_image_and_restart.shwhich loads the new image and force-recreates only the stundenplanner
container — db/ollama and their data volumes are left untouched. It also
warns (non-blocking) if the deployed couchdb_config/local.ini has drifted
from the bundled template; merge with sudo ./update_local_ini.sh if it
flags a difference (a backup of the old file is kept alongside it).
Stop the stack: ./stop_containers.sh (data volumes are kept).
The automatic-plan AI talks to a local Ollama model through nginx at /ollama.
- Model:
qwen2.5:7b(set viaOLLAMA_MODELon theollamaservice). The firstdocker compose updownloads it (several GB) — expect a long first start. It's stored in./ollama_data, so it downloads only once. - Warm start: ollama_entrypoint.sh
pulls (no-op after the first time) and warms the model into RAM on every
container start;
OLLAMA_KEEP_ALIVE=-1keeps it resident, so the first user request is fast even right after a restart. - Auth: the
/ollamaendpoint is protected by the hard-codedOLLAMA_API_KEYfrom the compose file — nginx rejects any request whoseAuthorization: Bearer <key>header doesn't match (401). Ollama itself has no auth, so it is never exposed directly (no host port). - GPU: CPU-only works but is slow. Uncomment the
deploy.resourcesblock on theollamaservice (needs the NVIDIA Container Toolkit) for GPU acceleration.
Configure the app once (any admin), under Plan-KI / KI Einstellungen:
| Field | Value |
|---|---|
| Anbieter | OpenAI-kompatibel |
| Basis-URL | /ollama |
| Modell | qwen2.5:7b |
| API-Schlüssel | Schulweit geteilt → the same value as OLLAMA_API_KEY |
/ollama is a same-origin relative URL, so it works regardless of the server's
host/port. Use Verbindung testen to confirm before saving.
MIT — see LICENSE.







