Backend (school-service):
- notification_log table with UNIQUE(event_id, lead_days, audience,
channel) for idempotent re-runs. Status enum sent/failed/skipped.
- internal/notifications/templates.go: per-event-type × audience ×
lead-day-bucket × language templates in 8 languages (de/en/tr/ar/
uk/ru/pl/fr). Fallback chain (lang→de, eventType→andere) so we
never miss a render.
- service.go scans cal_school_event for events whose
(start_date - runDate) appears in notification_lead_days. For each
due (audience, channel) tuple it dispatches via POST to the
Matrix/Email upstreams owned by the colleague's services.
Empty URL → status='skipped', logged for visibility.
- dispatcher.go handles the POST, parent-recipient lookup (joins
parent_account + parent_child + cal_school_event.affected_class_ids),
and writeLog with the unique constraint dropping duplicate runs.
- main.go runs a 1-hour ticker; when time.Hour()==6 it invokes the
scanner for today. Idempotent so transient restarts don't double-
send.
- POST /calendar/notifications/run-now for manual trigger + backfill
(?date=YYYY-MM-DD).
- GET /calendar/events/:id/notifications returns notification_log
rows scoped to the owning teacher.
- MATRIX_SERVICE_URL + EMAIL_SERVICE_URL env vars added (default
empty = stub mode).
Frontend (studio-v2):
- NotificationStatus component fetches /events/:id/notifications and
renders coloured badges per (lead, audience, channel, status).
- DayDetail mounts NotificationStatus inside each event card when
notify_parents or notify_students is set.
Tests:
- 6 new Go unit tests for bucketFor + Render (de/tr/fallback paths)
+ substitute(class_suffix). 89 subtests gesamt.
- 2 new Playwright tests: badge render with mocked log, hidden when
notifications are off.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (school-service):
- parent_account, parent_child, parent_magic_link, parent_session
tables. Tokens are sha256-hashed in DB; raw goes back exactly
once to the inviting teacher.
- InviteParent upserts the parent account, links a child to a tt_
class, mints a 7-day magic link. Returns the link path so the
teacher can paste it into Matrix/Email.
- RedeemMagicLink validates + marks used + mints a 30-day session,
sets HttpOnly bp_parent_session cookie.
- ParentSessionMiddleware reads the cookie and resolves the parent.
Lives in its own router group /api/v1/parent — totally separate
from the teacher JWT path.
- ParentMe returns the account + list of children (with class name).
- ParentTimetable returns the latest completed tt_solution's lessons
for the requested child's class, with full authorization check
(parent must own a child in that class).
Frontend (studio-v2):
- lib/calendar/subject-i18n.ts maps 22 German subject names to 8
parent locales (de/en/tr/ar/uk/ru/pl/fr). Falls back to German
for custom subjects.
- ParentManager component on the Schulkalender page lets the teacher
invite parents via email + child name + class + language. Newly
minted magic-link is shown with a copy-to-clipboard button.
- app/api/parent/[...path]/route.ts proxies parent-side endpoints
via the cookie so HttpOnly survives the Next.js round-trip.
- /eltern/login?token=… redeems and redirects to /eltern.
- /eltern shows a Wochengrid with German days + translated subject
names in the parent's preferred language. Headings and weekday
labels also localised (de/en/tr/ar/uk/ru/pl/fr).
Tests:
- 3 new Go unit tests (random token, hash stability, invite-request
validator). 83 subtests gesamt.
- studio-v2: e2e/eltern.spec.ts mit 7 tests across ParentManager,
/eltern/login, /eltern overview, subject-i18n end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (school-service):
- calendar_events.go — Create/List/Delete on cal_school_event with
UUID[] handling for affected_class_ids. Default lead-days [7,1]
if caller omits the array.
- calendar_rollover.go — single-transaction promotion: graduating
classes (grade >= 13) get deleted first so the +1 update doesn't
bump them to invalid grade 14. defaultSchoolYearDates() picks the
next Aug-Jul pair when the caller doesn't specify.
- Handlers + routes: GET/POST /calendar/events,
DELETE /calendar/events/:id, POST /calendar/school-year-rollover.
Frontend (studio-v2):
- EventModal: form with Title / Typ / Datum/Zeit / unterrichtsfrei /
Beschreibung / Sichtbarkeit + Notification-Checkboxen. Per-Type
Farb-Mapping in types.ts.
- DayDetail: Modal das beim Klick auf einen Kalender-Tag aufgeht und
Feiertage + Schulferien + Schul-Events fuer diesen Tag listet,
inkl. Loeschen-Button pro Event.
- RolloverWizard: zwei-Schritt-Dialog mit Datums-Auswahl + Tipp-
Bestaetigung ("SCHULJAHR WECHSELN") gegen versehentliche Auslo-
sung, danach Ergebnis-Card mit promoted/graduated-Counts.
- MonthView gewinnt onDayClick + onAddEvent + onRollover Props,
rendert farb-codierte Punkte fuer School-Events am Tagesrand.
- Page laed Events parallel mit Holidays und reicht alle Handler
nach unten.
Tests:
- Go: 3 neue Tests fuer defaultSchoolYearDates + parseClassIDs.
Validator-Test fuer CreateSchoolEventRequest existiert bereits.
80 Subtests gesamt, alle gruen.
- Playwright: mockCalendarApi gewinnt Routes fuer events GET/POST/
DELETE und school-year-rollover. 6 neue Tests (EventModal open,
submit, DayDetail open, Rollover-Trigger, Confirm-Schutz,
Ergebnis-Anzeige).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (school-service):
- cal_public_event (region, event_type, name_de, name_en, start/end,
UNIQUE(region, event_type, name_de, start_date)) — global snapshot.
- cal_school_config (user_id PRIMARY KEY, bundesland, school year dates).
- cal_school_event — Schul-eigene Termine; CRUD folgt in 9b.
- GET /calendar/holidays?region=&from=&to= — Range-Query against
cal_public_event, ordered by start_date.
- GET / PUT /calendar/config — upsert Bundesland per User.
- SeedFromSnapshot reads internal/seed/calendar_holidays.json on every
boot; idempotent via the unique constraint. Async goroutine so the
HTTP server starts immediately even if the seed file is large.
Data source:
- scripts/calendar-snapshot.sh ruft openholidaysapi.org fuer alle 16
Bundeslaender x 3 Schuljahre und schreibt
school-service/internal/seed/calendar_holidays.json (854 Events,
Stand Schuljahre 2026-2028).
- Dockerfile kopiert das seed/-Verzeichnis ins Image, damit die
Container-Datenbank beim ersten Start gefuellt wird.
Frontend (studio-v2):
- /schulkalender Page mit Gradient + Blobs wie /stundenplan und
/korrektur — gleicher Visual-Style.
- BundeslandWizard: zeigt alle 16 Laender als Dropdown, speichert
bei Klick die Config und switcht zur Monatsansicht.
- MonthView: 6-Wochen-Grid Mo-So, Feiertage rose-toned, Schulferien
amber-toned, heutiges Datum mit Indigo-Ring. Prev/Next/Heute
Navigation.
- lib/schulkalender/api.ts re-uses the stundenplan JWT helper so
auth-mode wechselt nicht.
- Sidebar bekommt einen Schulkalender-Eintrag (Icon mit Datum-Dots,
Pfad /schulkalender) in allen 26 Sprachen.
Tests:
- Go: 3 neue Validator-Tests (Bundesland len=5, EventType oneof,
Pflichtfelder). 77 Tests gesamt, alle gruen.
- Playwright: e2e/schulkalender.spec.ts mit Wizard, Save-Flow,
MonthView-Render, Heute-Button, Sidebar-Link. Hermetisch via
mockCalendarApi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auth (Test-Mode):
- middleware.AuthMiddleware now takes a devMode flag. In dev,
requests without Authorization fall back to a deterministic dev
UUID (00000000-...-001) and role=teacher. ENVIRONMENT=production
re-enables the strict 401 path.
- main.go wires devMode = cfg.Environment != "production".
- page.tsx replaces the red 'Anmeldung noch nicht integriert' banner
with a softer Testumgebung notice; the manual-token form moves
behind a nested details block.
Export endpoints (school-service):
- LoadExportLessons joins tt_lesson with tt_period for wall-clock
times; one query feeds both CSV and ICS.
- WriteCSV streams 10 columns including pinned flag.
- WriteICS emits one VEVENT per lesson anchored to a Monday — caller
overridable via ?start=YYYY-MM-DD. RFC 5545 escapes for ',', ';',
'\n' in icsEscape().
- NextMonday helper for the default anchor.
- GET /timetable/solutions/:id/export.{csv,ics} handlers attach
Content-Disposition: attachment so browsers download instead of
rendering.
Frontend:
- lib/stundenplan/api.ts downloadSolutionExport() fetches as blob,
triggers a synthetic <a download> click, and forwards the JWT when
present.
- PlanView gains CSV / ICS / Drucken buttons next to the perspective
selector. The toolbar carries class 'no-print' so window.print()
yields only the grid.
- globals.css @media print rule hides chrome, forces white
background, gives the table proper borders for A4.
Docs:
- docs-src/services/stundenplan/{index,architecture,constraints,
solver-tuning,export}.md with nav entry in mkdocs.yml under
Services → Stundenplaner.
- sbom/stundenplan/README.md lists manually-verified key dependencies
and the policy reference. scripts/stundenplan-sbom.sh generates
full machine-readable inventories via go-licenses + pip-licenses
+ license-checker when those tools are available.
Tests:
- internal/services/timetable_exports_test.go: 4 unit tests covering
CSV column layout + quoting, ICS structure + DTSTART formatting,
icsEscape special chars, NextMonday weekday math.
- studio-v2/e2e/stundenplan-export.spec.ts split out of the main spec
file (LOC budget) — 3 tests for button render, CSV download,
ICS download.
- mockSchoolApi extended with export.csv + export.ics routes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend (school-service):
- tt_solution gains parent_solution_id (self-FK, ON DELETE SET NULL)
and seconds_limit columns via ALTER TABLE IF NOT EXISTS.
- CreateTimetableSolutionRequest accepts optional parent_solution_id
and seconds_limit (5-600s) with binding validation.
- CreateSolution checks parent ownership before INSERT so users can't
fork another tenant's plan.
- New PUT /timetable/lessons/:id/pin endpoint; ownership enforced via
the lesson's solution.created_by_user_id JOIN.
Solver:
- Lesson.pinned now carries @PlanningPin so Timefold leaves locked
cells untouched during the search.
- build_problem() takes optional parent_solution_id; if set, copies
pinned (class_id, subject_id, day, period, room) tuples onto fresh
Lesson objects via greedy first-fit matching. Surplus pinned rows
from curriculum changes are silently dropped.
- _build_factory(seconds) replaces the module-level factory so each
job honours its tt_solution.seconds_limit override.
- persist_solution writes lesson.pinned back so subsequent re-solves
inherit it.
Frontend (studio-v2):
- SolutionList grows three knobs in the create-form: Basieren auf
(parent dropdown, only completed solutions, disabled when none),
Sekunden-Limit (5-600), and the existing Name.
- PlanView cells get a pin/unpin button with optimistic update and
rollback on error. Pinned cells gain an amber ring.
- types.ts + api.ts mirror the new fields; lessonsApi.pin(id, bool).
- HelpPanel: collapsible 6-step Bedienungsanleitung explaining the
setup-to-plan workflow. Anchored at the top of /stundenplan above
the dev token banner.
- page.tsx switches to the same gradient + animated-blob background
used on /korrektur so /stundenplan stops looking like a slate-900
test page.
- JWT dev banner gets a step-by-step explanation of how to grab the
token from DevTools and a non-blocking success indicator (no more
alert()).
Tests:
- school-service: 6 new validator cases for parent_solution_id +
seconds_limit boundaries. 73 subtests total, all green.
- studio-v2: mockSchoolApi adds PUT /lessons/:id/pin route. 5 new
Playwright tests across two suites (parent-selector visibility +
options, seconds-limit input, pin button render, pin-icon flip).
Existing tests adjusted to the new help panel + JWT banner wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend additions in studio-v2:
- types.ts adds TimetableSolution, TimetableLesson, SolutionStatus,
CreateTimetableSolution mirroring the Go models.
- lib/stundenplan/api.ts adds solutionsApi with list/get/create/remove/
lessons. Solve trigger is POST /timetable/solutions — school-service
forwards to the solver-service over the Docker network.
- _components/plan/SolutionList: table of past solves with status
badges, hard/soft score, Anzeigen + Loeschen buttons, and a
'Neuen Plan generieren' trigger. Auto-polls every 4 s while any
solution is pending/running, clears the interval otherwise.
- _components/plan/PlanView: Mo–Fr × period weekly grid. Three
perspectives (Klasse / Lehrer / Raum) toggleable via test-id'd
buttons; selector below lists every unique resource with at least
one lesson. Cells colour-coded by tt_subject.color.
- _components/plan/PlanHub orchestrates list + view; default tab in
page.tsx switches from 'klassen' to 'plan'.
Tests:
- mockSchoolApi helper extracted to e2e/_helpers.ts so the spec file
stays under 500 LOC. Helper now also mocks /solutions GET/POST/DELETE
and /solutions/:id/lessons; solutions kept in a closure so POST
appears in the next GET.
- 8 new tests across two suites: SolutionList empty state, list
render, completed-vs-failed Anzeigen visibility, solve trigger;
PlanView placeholder when no selection, grid render, perspective
switching.
- Existing Klassen CRUD tests now click the Klassen tab first
(Plan is the new default landing tab).
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>
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>
- 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>
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>
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>
- 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>
Non-DE/EN users (parents) see a guide panel explaining:
- What this page is (vocabulary exercises from teacher)
- How each exercise type works (cards, quiz, listen, speak)
- All text in parent's native language (TR/AR/UK/RU/PL)
German/English users see the original layout without the guide.
This is the single entry point — no need to duplicate explanations
in every exercise sub-page.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major improvements for non-DE/EN speaking parents:
1. Right panel: Explains in parent's native language what this
exercise is about and how it works (TR/AR/UK/RU/PL translations)
2. Trilingual buttons: "Dogru" (primary) with "Richtig / Correct"
subtitle so parents understand even if language detection is wrong
3. Native word shown prominently: "= elma" next to "apple"
4. Audio buttons labeled EN/DE/TR with language codes
5. Answer card shows all 3 languages: English, Deutsch, native
6. Progress tracker in the explanation panel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vocab units created via /vocabulary/units only have QA items, no
pre-generated MC questions. Quiz now falls back to generating MC
questions client-side from QA items (EN word → 4 DE options).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All exercise pages now use useNativeLanguage() hook:
- Buttons show text in user's native language (Richtig→Dogru, etc.)
- Instructions translated (Geschafft→Bitti, Nochmal→Tekrar, etc.)
- wordInNative() available for vocab translations (needs data)
Pages updated: flashcards, quiz, type, listen, match, pronounce, story.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uses useNativeLanguage() hook for all UI text. Shows native language
translation of vocab word + audio button for native pronunciation.
Removed inline pt translations dict (now in exerciseTranslations.ts).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Binary data (MP3 audio) was corrupted by resp.text(). Now detects
content-type and uses arrayBuffer() for audio/* and image/* responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js route-level layouts provide Sidebar + gradient background
automatically for all sub-pages. Individual pages no longer need
their own wrapper divs or Sidebar imports.
- learn/layout.tsx: Sidebar + purple gradient for all learning pages
- parent/layout.tsx: Same for all parent portal pages
- LearnLayout.tsx: Reusable component for other pages
- Fixed broken <LearnLayout>}> artifacts from previous refactoring
- Removed duplicate Sidebar/wrapper code from 9 sub-pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>