feat(tasks): dedicated /tasks/{key}/run page with per-subject question bank
Before: tasks.modals.runner showed the same 4 multiplication questions
regardless of subject (Mathe / Deutsch / Englisch / Sachkunde / Musik).
Made no sense — Englisch task shouldn't ask 12 × 8.
Now: dedicated full-page route /tasks/{key}/run loads the subject-correct
question bank based on the task's `color` token, with structured URL,
proper auth + license middleware, and Lazy skeleton placeholder.
New artifacts:
config/task-questions.php — Question bank keyed by color/subject:
primary -> Mathematik (5 multiplication / division questions)
rose -> Deutsch (5 grammar/wortart/artikel questions)
violet -> Englisch (5 vocab/grammar in English)
green -> Sachkunde (5 nature/animals/space)
amber -> Musik (5 musical notation / theory)
config/tasks.php — Placeholder task list extracted from Index so
both Tasks\Index (list page) and Tasks\Run (quiz page) share data.
Future: replace with DB-backed Task model.
app/Livewire/Tasks/Run.php
- Lazy page-component, #[Layout('layouts.dashboard', active=tasks)]
- mount(string $key) -> abort 404 if task or question bank missing
- findTask() reads config('tasks')
- questions = config("task-questions.{$task['color']}")
- submitAnswer(int) records lastAnswerIndex + lastAnswerCorrect +
tallies correctAnswers (visual feedback before advance)
- nextStep() advances or sets finished=true on final
- restart() resets all progress
- placeholder() returns livewire.tasks.run-placeholder (skeleton)
resources/views/livewire/tasks/run.blade.php
- Back link + task header card (icon, meta, title, points)
- Progress bar (Frage X / Y + correct counter)
- Question card with 2x2 option grid, A/B/C/D labels
- Per-color theme (primary/rose/violet/green/amber bar + ring)
- Two-step interaction: pick option -> show correct/wrong feedback ->
"Nächste Frage" / "Aufgabe abschließen"
- Finished card: trophy or arrow-path; points earned; restart / done
- All buttons use design tokens + heroicons (no inline SVG)
resources/views/livewire/tasks/run-placeholder.blade.php
- Skeleton with same layout shape (back link, header card, question)
- data-testid="task-run-skeleton"
routes/web.php
Route::get('/tasks/{key}/run', \App\Livewire\Tasks\Run::class)
->whereAlphaNumeric('key')->name('tasks.run')
Behind auth + license group like other dashboard pages.
Tasks/Index::startTask() simplified:
$this->redirectRoute('tasks.run', ['key' => $key], navigate: true)
(Server-side redirect; client navigates without full reload.)
Security:
- whereAlphaNumeric('key') restricts URL param to safe chars
- abort_if($task === null, 404) prevents directory probing
- auth+license middleware applies to whole route group
- Question bank is config (no user-supplied content interpolated)
- Answer index validated against question[correct] only — no dynamic eval
Tests (27 new + 0 broken):
- TaskQuestionBankTest (4): config exists; 5 buckets; required keys;
per-color content differs
- TasksRunPageTest (15): route registered; auth happy path; 404 for
unknown key; per-color question selection (math/deutsch/eng/sachkunde);
submitAnswer correct/wrong; nextStep advances + clears feedback;
finishing 5 steps -> finished=true; restart resets; Index.startTask
redirects; unauth redirected to login; placeholder testid
- TasksStartButtonTest updated: Start now redirects, no longer
dispatches modal
Full verification:
- 186 tests passed, 0 failed, 0 skipped (661 assertions)
- npm build OK, manifest generated
- All 8 page routes return 302 unauth (correct)
- All 5 /tasks/{key}/run routes return 302 unauth (correct)
- All 8 modal aliases still resolve (legacy Runner modal kept)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>