timefold-solver only publishes x86_64 wheels — pip resolves to no
candidate on linux/arm64. Switch the compose entry to linux/amd64
so the image builds via QEMU/Rosetta on the Mac Mini. The cloud
target for this service is x86_64 anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ARM64 has no eclipse-temurin:17-jdk-alpine manifest. Switch to the
Debian-based python:3.11-slim and install OpenJDK via apt so the
image builds on both Mac Mini (ARM) and x86_64 hosts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
school-service additions:
- tt_solution + tt_lesson migration. tt_lesson carries three UNIQUEs
(solution+class, solution+teacher, solution+room per slot) so the
DB itself rejects any double-booking the solver might emit by
mistake.
- Solution CRUD + GET solutions/:id/lessons endpoint with joined
class/subject/teacher/room names for display.
- POST /timetable/solutions creates the row then fires off the
solver-service via HTTP (5s timeout, mark failed if unreachable).
- SOLVER_SERVICE_URL config wired through main.go/handlers.
New service timetable-solver-service:
- Python 3.11 + FastAPI + Timefold Solver 1.21 (Apache-2.0). Dockerfile
bundles OpenJDK 17 since Timefold for Python is a JPype bridge.
- app/domain.py — Timefold @planning_entity Lesson with timeslot+room
as PlanningVariables; @planning_solution Timetable holds problem
facts (rooms/teachers/etc.) AND rule-fact collections.
- app/rules.py — frozen dataclasses mirroring 6 of the 15 tt_
constraint_* tables initially.
- app/constraints.py — ConstraintProvider with 3 universal hard
constraints (no double-booking) + 5 DB-driven constraints
(teacher_unavailable_day/window, teacher_excluded_room,
room_unavailable, room_requires_type) + 1 quality soft constraint
(subject_preferred_period). Remaining 9 constraint types ready to
plug in via the same join pattern.
- app/repository.py — async loaders for stammdaten + rules; builds
one Lesson per (curriculum row × weekly_hours), skipping rows
without a tt_assignment teacher.
- app/runner.py — runs solver in ThreadPoolExecutor so the FastAPI
event loop stays responsive. Updates tt_solution status
pending→running→completed|infeasible|failed.
- app/main.py — POST /api/v1/solve (202 Accepted, background task),
GET /api/v1/jobs/{id}, /health. School-service polls tt_solution
directly instead of GET /jobs for the typical case.
- docker-compose.yml adds the service on port 8095, depending on
core-health-check.
Tests:
- school-service: validator test for CreateTimetableSolutionRequest
(allows empty name).
- solver-service: tests/test_domain.py + tests/test_rules.py cover
construction + hashability of the planning facts. Full solve flow
deferred to Phase 8 integration with seed data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The German „X" markers in the description prop combined a curly „
(U+201E) with a straight " (U+0022). The straight quote prematurely
terminated the JavaScript string inside the JSX expression. Removing
both markers around the example text keeps the description readable
and unambiguously valid JSX.
Test selector for the UnavailableWindow description updated to match
the new wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
German curly quotes („…") combined with a closing straight " inside
JSX attribute values were terminating the attribute prematurely, e.g.
`description="Beispiel: „X" (jugendgerecht)."` lost everything after
the inner straight quote. Switch all such descriptions to the JSX
expression form `description={"…"}` so the inner quotes are part of
a JavaScript string literal and parsed correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend was already complete in Phase 2; this finishes the UI.
- regeln/_shell.tsx introduces useConstraintCrud (handles list/create/
delete state + reload), ConstraintShell (header, prereq banner,
form toggle, error display, empty/loading/table render), and
useShellStyles for the recurring theme tokens. Each editor now
only carries its schema-specific bits.
- Existing 4 editors (TeacherUnavailableDay/Window, SubjectMax
Consecutive/PreferredPeriod) refactored onto the shell — every
Playwright selector preserved.
- 11 new editors covering the remaining constraint tables:
TeacherMaxHours{Day,Week}, TeacherExcluded{Subject,Room},
Subject{MinDayGap,ContiguousWhenRepeated,DoubleLesson},
Class{MaxHoursDay,NoGaps},
Room{RequiresType,Unavailable}.
- RegelnHub now references all 15 editors directly — no more 'soon'
placeholders. The two duplicate 'Max. Stunden / Tag' entries
(teacher + class) are intentional and disambiguated by group.
Tests:
- e2e/stundenplan.spec.ts: mock routes added for all 11 new constraint
endpoints. RegelnHub suite gains a single test that switches
through 13 uniquely-labelled editors, plus a dedicated test for
the two duplicate 'Max. Stunden / Tag' labels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend additions in studio-v2:
- PeriodsManager renders the weekly grid as a Mo–So table with one
row per period_index. New entries auto-increment period_index so
the user can hit Anlegen repeatedly for a full day's slots.
- CurriculumManager joins classes + subjects; new entries refuse to
open when either prerequisite list is empty (banner instead).
- AssignmentsManager joins teacher × class × subject with the same
prerequisite-banner pattern.
- regeln/RegelnHub: vertical sidebar grouping all 15 constraint
types by parent entity (Lehrer/Fach/Klasse/Raum). Implemented
editors are clickable, the other 11 are visibly disabled with
a 'soon' tag.
- Three new editors:
TeacherUnavailableWindowEditor (time-window pattern),
SubjectMaxConsecutiveEditor (number-input pattern),
SubjectPreferredPeriodEditor (number range pattern).
- page.tsx wires every tab to its manager; the not-implemented
placeholder is gone (no more empty tabs).
Test coverage:
- e2e/stundenplan.spec.ts rewritten: 23 tests across 7 suites,
covering all 8 tabs, the new managers' prerequisite banners,
sub-tab switching in the RegelnHub, and the disabled state of
not-yet-implemented constraint rules. Each test mocks the
backend via page.route() so the suite stays hermetic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 'Lehrer' label collided with a Sidebar entry; scope to <main nav>.
- 'Schmidt, Anna' lived in a closed <option>; assert on count via
select.locator() instead of toBeVisible().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend additions in studio-v2:
- LehrerManager / FaecherManager / RaeumeManager — same CRUD pattern as
Klassen, with entity-specific form fields and table columns.
- regeln/TeacherUnavailableDayEditor — first constraint editor, joins
against teachersApi to render a readable name in the dropdown and
list. Falls back to a guidance banner when no teachers exist yet.
- page.tsx wires up the new tabs; data-testid attributes added across
managers so the Playwright suite can target them deterministically.
Tests:
- school-service: timetable_constraints_more_test.go fills the
remaining 9 constraint DTOs (TeacherMaxHoursDay/Week,
TeacherExcludedSubject/Room, SubjectMinDayGap,
SubjectContiguousWhenRepeated, SubjectDoubleLesson, ClassNoGaps,
RoomRequiresType). 66 subtests total, all green.
- studio-v2: e2e/stundenplan.spec.ts covers the page shell, tab
navigation, Klassen CRUD with mocked backend, constraint editor's
empty-teacher fallback, sidebar entry. All school-service calls
intercepted via page.route() so the suite is hermetic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 — initial UI for the timetable scheduler:
- app/stundenplan/page.tsx with tab navigation (Klassen / Lehrer /
Faecher / Raeume / Zeitraster / Stundentafel / Lehrauftraege /
Regeln) and a dev-mode JWT entry to authenticate against
school-service until full auth is wired up.
- app/stundenplan/_components/KlassenManager.tsx as the working
prototype for one entity (list / create / delete). Pattern can be
copied for the other 6 stammdaten + 15 constraint editors.
- lib/stundenplan/api.ts exposing typed clients for all 22 endpoints
(7 stammdaten + 15 constraint tables). Constraints use a factory
to keep the file tight.
- app/api/school/[...path]/route.ts proxies the browser through
Next.js to school-service so HTTPS studio-v2 can reach the plain
HTTP backend.
- Sidebar.tsx gains a Stundenplan entry with 26-language labels.
- docker-compose.yml exposes SCHOOL_SERVICE_URL to studio-v2 and
declares the school-service dependency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When searching for DE/FR/ES/etc. words, the Kaikki entries have empty
translations. Now does a reverse lookup to find the EN entry and copies
its 24-language translations. This ensures wordInNative() works for
all languages, not just the original 7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Tab labels (Muttersprache/Schulsprache) now translate with selected language
e.g. Turkish: "Ana dilim" / "Okul dili"
- Info box below tabs explains each setting in the user's native language
e.g. "Evde konustunuz dil. Tum menuler bu dilde gorunecektir."
- Wider dropdown (w-72) to fit translated text
- All 26 European languages covered
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- LanguageContext: new schoolLanguage + setSchoolLanguage
- LanguageDropdown: two tabs (Muttersprache / Schulsprache) with flag selection
- UnitBuilder: defaults target language to schoolLanguage
- Stored in bp_school_language localStorage (default: de)
- Shows school flag badge next to main language when different
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Merge two separate language systems (bp_language + bp_native_language) into one
- NativeLanguageContext now reads from LanguageContext (same localStorage key)
- Extend i18n.ts to 26 languages with flags (UI falls back to EN/DE)
- Replace LanguageSwitcher with LanguageDropdown (flags) in learn + parent layouts
- Migration: old bp_native_language value auto-migrates to bp_language
- Onboarding page writes to bp_language (unified key)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New UnitBuilder component with language pair selector (DE⇄EN, ES, FR, etc.)
- Manual word entry form with auto-suggest from Kaikki dictionary (6M words)
- "No results" prompt to add multi-word terms (e.g. "schottisches Hochland")
- New backend endpoint GET /vocabulary/lookup-translation (any→any via EN hub)
- Updated POST /vocabulary/units: accepts custom_words + source_lang/target_lang
- Split unit endpoints into vocabulary/unit_api.py (500 LOC budget)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two buttons on topic cards:
- "Anzeigen": Shows words in search results (for review)
- "Alle zur Unit": Adds all topic words to the unit builder directly
Both buttons load from Kaikki and respect selected language.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Topics API now accepts lang= parameter. When lang=de, the word
labels are translated from English via Kaikki translations:
"eye, pupil, iris" → "Auge, Pupille, Iris"
Frontend sends searchLang to /topics endpoint and displays
display_words (translated) instead of words (English).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sidebar: Only "Lernmodule" link (no separate Woerterbuch).
/learn page: "Neue Lernunit erstellen" button links to /vocabulary
for the word selection flow. Teacher stays in one flow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user selects DE and types "Auge", the topic "Eye/Auge" is found
correctly. But "Alle laden" must search words with lang=en because
the topic word list is English (eye, pupil, iris...).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
31 curated topics with 683 words (Fruit, Animals, Body, Eye, Sports,
School, Family, Weather, etc.). When user types a word that belongs
to a topic, the topic appears as a suggestion with "Alle laden" button.
Clicking "Alle laden" fetches all words from that topic via Kaikki
and displays them for easy selection into a learning unit.
New endpoint: GET /api/vocabulary/topics?q=banana
New table: vocabulary_topics (topic, words[], word_count)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search now sends lang= to API (was always defaulting to EN).
Users can select any of the 24 languages in the search bar dropdown.
Placeholder text changes based on selected language.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SELECT COUNT(*) FROM vocabulary_kaikki was 100+ seconds without index,
blocking the entire backend. Hardcoded to 6,271,749 / 24 languages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search endpoint now defaults to source=kaikki, searching the
vocabulary_kaikki table with 6.27M Wiktionary entries.
/filters returns kaikki_total and kaikki_languages count.
/vocabulary header shows "6,271,749 Woerter in 24 Sprachen".
Manual vocabulary_words (27 entries) still accessible via source=manual.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Removed stale 'explanations' export that broke build
2. Explanation banner now spans full width ABOVE the 2/3+1/3 layout
so exercise cards and native words start at the same height
3. Both columns use items-start for visual alignment
4. Impressum link added at bottom of learn layout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
exerciseExplanations.ts: Match and Flashcard explanations translated
into DE, EN, TR, AR, UK, RU, PL, FR, ES, IT, PT, NL, RO, EL, BG,
HR, CS, HU, SV, DA, FI, SK, SL, LT, LV, ET.
Each exercise type gets a parent-facing explanation in the user's
selected language. No more German fallback for unsupported languages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a language like PT, FR, IT is selected but has no translation
in exerciseTranslations.ts, the system now falls back to English
instead of German. English is more universally understood.
Applies to: exercise explanations, button labels, instructions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lehrer_arbeitsblaetter volume mounted at /root/Arbeitsblaetter.
Survives container restarts — learning units, QA items, translations,
and audio cache are no longer lost on rebuild.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user clicks an EN word, the corresponding image (Wikipedia
photo or emoji) appears below the match grid. Emoji shown as
large text (6xl), Wikipedia photos as max-h 160px image.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
image_service.py: Fetches thumbnail from Wikipedia REST API (free,
no account). Falls back to emoji for abstract words (40+ mapped).
Auto-enrichment: When a learning unit is created, images are
automatically fetched for all words that don't have one yet.
Manual endpoint: POST /api/vocabulary/enrich-images fills images
for existing words without images.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- exerciseType prop for correct explanation lookup (was using title)
- Vertical divider line between work area and native helper
- Cyan-tinted explanation card with lightbulb icon
- Padding between sections (pr-6 / pl-6 around divider)
- Explanation card has distinct background for visibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ExerciseLayout.tsx: Reusable layout component for all exercises.
- Left 2/3: Standard exercise area (EN + DE)
- Right 1/3: Native language helper (explanation + word list)
- Only shows right panel for non-DE/EN speakers
- Explanation card describes what the child should do
- Column headers are trilingual (TR · English · Deutsch)
Match page rebuilt using ExerciseLayout:
- EN+DE cards in 2/3 left area with equal height + audio
- Native words in 1/3 right panel with audio buttons
- Highlights native word when EN word is selected
- Progress bar with count, score counter
ExerciseLayout can be reused for flashcards, quiz, type, etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All cards (EN/DE/native) now have min-h-[48px] for consistent height.
Progress bar shows "4/12" count next to the fill bar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Progress bar under header (fills as pairs are matched)
- Counter with symbols: ✓ first-try, ↻ retry, ✗ errors
- EN column now also has audio buttons (small speaker icon)
- All 3 columns have consistent height (flex layout)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major improvements:
1. Third column shows native language translation (TR/AR/UK/RU)
2. Clicking EN word flashes native translation briefly (2s overlay)
3. German column has audio button on each word (speaker icon)
4. Native column has audio button for each translation
5. Scoring: tracks first-try correct vs retry vs errors separately
6. Full points only for error-free completion
7. "Nochmal" button always available to repeat the unit
8. Header shows live score: green/yellow/red counters
9. All buttons use translation system (t('back'), t('match'), etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced localStorage-only hook with React Context Provider.
Layout and page components now share the same state — switching
language in the dropdown instantly updates all text on screen
without requiring a page reload.
NativeLanguageProvider added to root layout.tsx.
useNativeLanguage() re-exported from Context for backward compat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useNativeLanguage: Now has setNativeLang() that persists to localStorage.
Language selection carries across all pages automatically.
LanguageSwitcher: Compact dropdown component added to learn/layout.tsx
and parent/layout.tsx — visible on every sub-page (top-right).
Parent portal: Language dropdown syncs both UI language and native
language. Parents can switch language mid-session (e.g. when both
parents speak different languages).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>