Player on a character sheet can click 'Stufe steigen' (Sparkles icon) and a modal wizard opens, ranging from 4 to 11 steps depending on level/class/FA/caster, ending in a Review step.
Wizard chrome (header, stepper with progress label, body, footer) matches `01-UI-SPEC.md` exactly — no redesign.
Choice-cards show source-color badges (Klassen/Abstammung/etc. — reuse existing featSourceColors) and a yellow AlertTriangle for non-evaluable prereqs (D-03).
Boost step uses +/- counters at h-11 w-11 (44×44 touch targets), shows 'wird {newScore}' live with cap-bei-18 chip when applicable.
Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links to revise upstream choices.
Bestätigen runs the commit; wizard closes; toast shows; WebSocket level_up_committed event arrives at all other open clients of the character within ~1s.
DRAFT-Resume banner appears at the top of the character-sheet when an open DRAFT exists, with Fortsetzen + Verwerfen actions.
Pathbuilder-import-violations banner appears below the avatar header when Character.prereqViolations is non-null.
All UI text is in German; no emojis; only Lucide icons; mobile-first (44px touch targets).
Human verification checkpoint passes — user walks through the wizard for a real character on a mobile viewport (375×667) and confirms each step matches UI-SPEC.
Adds 'level_up_committed' to CharacterUpdateType union + onLevelUpCommitted callback
Ändern (revision) does NOT auto-clear downstream choices that depended on the revised upstream choice. Per D-12 (review-only recompute, no live per-step recompute) the wizard intentionally keeps stale downstream picks; commit-time validation in Plan 04 (LevelingService.commit + isValidBoostSet/prereq guards) is the source of truth and rejects invalid combinations with a German BadRequestException that the wizard surfaces inline. Per-dependency clearing is a v2 enhancement.
Build the React wizard UI exactly as specified in `01-UI-SPEC.md` — no redesign. The wizard is the user-facing surface that drives the LevelingService REST endpoints (Plan 04). It is mobile-first (Bottom-Sheet on small screens, centered modal on `sm:` and up), German throughout, and reuses existing project primitives (`Button`, `Card`, `Spinner`, `ActionIcon`, `featSourceColors`). The plan also extends the character-sheet page with the "Stufe steigen" button, the DRAFT-Resume banner, and the Pathbuilder-Import-Violations banner.
Purpose: Without this plan, the entire backend (Plans 01-04) has no consumer. The wizard is the product surface — it converts the regelkonform engine into a usable feature for the player and GM at the table.
cd client && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "level-up-(choice-card|prereq-confirm|resume-banner|violations-banner)" || echo "tsc clean"
- All 4 files exist in `client/src/features/characters/components/level-up/`
- `level-up-choice-card.tsx` exports `ChoiceCard` and `ChoiceCardOption` interface
- `level-up-choice-card.tsx` contains `Lock`, `AlertTriangle`, `Check` icon imports + the conditional-render block for each
- `level-up-prereq-confirm-dialog.tsx` exports `PrereqConfirmDialog` + uses `z-[60]`
- `level-up-prereq-confirm-dialog.tsx` contains the German strings: `Voraussetzung nicht prüfbar`, `Trotzdem wählen`, `Abbrechen`
- `level-up-resume-banner.tsx` exports `LevelUpResumeBanner` + contains German `Du hast eine offene Stufenaufstiegs-Session`
- `level-up-violations-banner.tsx` exports `LevelUpViolationsBanner` + contains the German violations heading
- All 4 files use Lucide icons only (no emojis); verify with `grep -E "[\\u{1F000}-\\u{1FFFF}]"` returning no matches
- All 4 files use `Button` from `@/shared/components/ui`
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
Four shared UI primitives exist matching UI-SPEC; ready for consumption by step components and the wizard container.
Task 4: 12 step components (class-features, class-feature-choice, boost, skill-increase, 5×feat, spellcaster, review)
client/src/features/characters/components/level-up/level-up-step-class-features.tsx,
client/src/features/characters/components/level-up/level-up-step-class-feature-choice.tsx,
client/src/features/characters/components/level-up/level-up-step-boost.tsx,
client/src/features/characters/components/level-up/level-up-step-skill-increase.tsx,
client/src/features/characters/components/level-up/level-up-step-feat-class.tsx,
client/src/features/characters/components/level-up/level-up-step-feat-skill.tsx,
client/src/features/characters/components/level-up/level-up-step-feat-general.tsx,
client/src/features/characters/components/level-up/level-up-step-feat-ancestry.tsx,
client/src/features/characters/components/level-up/level-up-step-feat-archetype.tsx,
client/src/features/characters/components/level-up/level-up-step-spellcaster.tsx,
client/src/features/characters/components/level-up/level-up-step-review.tsx
- .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (per-step contracts):
- Class-Features step: §Wizard-step screen headings (line 156)
- Class-Feature-Choice (D-19): §Component Contract — Choice-Klassenmerkmal Sub-Step (lines 480-489)
- Boost: §Component Contract — Boost-Set Step (lines 362-423) — FULL JSX provided
- Skill-Increase: §Component Contract — Skill-Increase Step (lines 426-450)
- Feat steps: §Component Contract — Choice-Card (lines 308-359) — all 5 feat steps reuse ChoiceCard with different filters
- FA-Slot: §Component Contract — Free-Archetype-Slot Step (lines 454-463)
- Spellcaster: §Component Contract — Spellcaster Step (lines 466-477)
- Review: §Component Contract — Review Step (lines 492-565) — Section A choices summary + Section B Vorher/Nachher cards + Section C spellcaster diff + Ändern revision contract
- client/src/features/characters/components/level-up/level-up-choice-card.tsx (Task 3 — primitive)
- client/src/features/characters/components/level-up/wizard-state-reducer.ts (Task 2 — events to dispatch)
- client/src/features/characters/components/feat-detail-modal.tsx (lines 22-29 — featSourceColors map to REUSE)
- client/src/features/characters/components/hp-control.tsx (analog: +/- counter pattern for Boost step)
Implement each of the 11 step components per UI-SPEC. Each component:
- Receives `(state: WizardState, dispatch: (ev: WizardEvent) => void, characterId: string, ...)` as props OR uses a context provider — planner discretion.
- Uses the ChoiceCard primitive from Task 3 for talent picks (5 feat steps + class-feature-choice + spellcaster repertoire picks).
- Uses the existing FeatDetailModal (or a new `level-up-detail-modal.tsx` if needed) for the Mehr / detail-popover, opened at `z-60` per UI-SPEC.
**Critical implementation notes per step:**
**A. `level-up-step-class-features.tsx`** — Auto-summary, no input. Renders the `ClassProgression.grants` list for the new level as a read-only list. Header + sub-line per UI-SPEC. Server endpoint to fetch the list: GET ClassProgression for `(className, targetLevel)` — Plan 04 may need a small read endpoint OR the wizard can fetch from a public ClassProgression endpoint. If neither exists, the wizard receives the data via the LevelUpSession's preview endpoint.
**B. `level-up-step-class-feature-choice.tsx`** -- D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Calls `api.getLevelUpClassFeatureOptions(characterId, sessionId, optionsRef)` (added in Task 1 alongside the other api.* methods) which hits `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. No server-side edits in this plan -- the endpoint is already present.
**C. `level-up-step-boost.tsx`** — UI-SPEC lines 362-423 give the FULL JSX. Use it verbatim with these wires:
- State source: `state.choices.boostTargets` (current selection — array of 0..4 strings)
- Per attribute, count = 1 if in boostTargets else 0
- Dec: removes the ability from boostTargets. Inc: adds (only if length < 4 and not already present)
- Dispatches `SET_BOOST_TARGETS`
- Live-preview "wird {newScore}" calls a local `applyBoostLocal(currentScore)` helper (mirror of server's applyAttributeBoost — could import shared lib if a shared package were set up; for now, inline a 5-line client helper)
- Footer-helper "X von 4 Boosts gewählt"
**D. `level-up-step-skill-increase.tsx`** — UI-SPEC lines 426-450. Renders all skills as compact rows. For each row: current rank → next rank (or "Cap erreicht" chip if `canIncreaseSkill` would return false; mirror Plan 02 logic client-side). Dispatch `SET_SKILL_INCREASE` on click.
**E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) -- All five feat steps share the same shape:
- Fetch filtered feat list via `api.getLevelUpFeats(characterId, sessionId, slot, includeUnavailable)` (added in Task 1 alongside the other api.* methods). It hits `GET /characters/:characterId/level-up/:sessionId/feats?slot=class|skill|general|ancestry|archetype&includeUnavailable=true|false` returning `FeatWithEval[]` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). No server-side edits in this plan.
- Render each feat as a ChoiceCard.
- For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) -> on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
- For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfuellbare anzeigen" passes `includeUnavailable=true` to the api call to reveal them grayed.
- Slot-specific: feat-archetype only renders if `state.steps.includes('feat-archetype')` (FA enabled); shows the "vor/nach Dedication" filter per UI-SPEC line 462.
**F. `level-up-step-spellcaster.tsx`** — UI-SPEC lines 466-477. Two sub-shapes:
- Prepared caster: info strip "+N Slot auf Grad M (automatisch)" + immediate `Weiter` enable.
- Spontaneous caster: info strip + repertoire-pick sub-section. Renders spell ChoiceCards filtered by tradition. Dispatch `SET_REPERTOIRE_PICKS`.
**G. `level-up-step-review.tsx`** — UI-SPEC lines 492-565. THIS IS THE LOAD-BEARING STEP:
- **Section A** — Wahlen-Zusammenfassung: Card with one row per choice. Each row has an `Ändern` link that dispatches `GO_TO_STEP_FROM_REVIEW` (entering revision mode).
- **Section B** — Vorher/Nachher cards: Use `useLevelUpPreviewQuery(characterId, sessionId, enabled=true)` from Task 2. Render two `Card`s side-by-side on `sm:`, stacked on mobile. Display `before` / `after` HP-Max, RK, Klassen-DC, Wahrnehmung, Saves. Use `font-mono text-2xl` for stat numbers. Show success-tinted delta chips (`+X`).
- **Section C** — Spellcaster diff (only if `preview.spellcaster` is non-null): table of slot increments + repertoire delta.
- **Bestätigen button** in the wizard footer (handled by the wizard container, NOT this step) calls `useCommitLevelUpMutation(characterId, sessionId).mutateAsync(...)`.
**Constraint specific to all step files:**
- Each is a function component with named export.
- Props interface ends with `Props`.
- All copy in German (UI-SPEC §Wizard-step screen headings line 156 supplies the exact strings).
- All touch targets ≥ 44px.
- No `: any`.
**Server endpoints used by these steps (already provided by Plan 04 -- no patches in this plan):**
- `GET /characters/:characterId/level-up/:sessionId/feats?slot=<kind>&includeUnavailable=<bool>` -> `FeatWithEval[]`
- `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -> `ClassFeatureOption[]`
- `GET /characters/:characterId/level-up` -> open DRAFT or 404 (used by character-sheet-page banner detection)
All three are declared in 01-04-PLAN.md Task 5 (LevelingController) and 01-04-PLAN.md Task 4 (LevelingService).
LevelUpSessionDto already includes `steps: StepKind[]` (Plan 04 Task 1) -- the wizard reads it directly from the start/resume response.
cd client && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "level-up-step" || echo "tsc clean"
- All 11 step files exist in `client/src/features/characters/components/level-up/`
- Each file uses `function` export with PascalCase name (e.g. `LevelUpStepBoost`)
- Each file imports `Button` and other primitives from `@/shared/components/ui`
- Each file uses German strings matching UI-SPEC §Copywriting (line 156)
- `level-up-step-boost.tsx` contains the literal string `wird` (live preview text)
- `level-up-step-boost.tsx` contains `h-11 w-11` (44px +/- buttons)
- `level-up-step-boost.tsx` contains the literal string `Cap bei 18` (warning chip)
- `level-up-step-skill-increase.tsx` contains the literal string `Cap erreicht`
- `level-up-step-review.tsx` uses `useLevelUpPreviewQuery` (Task 2 hook)
- `level-up-step-review.tsx` contains the literal string `Vorher` AND `Nachher`
- All step files contain NO `: any` outside comments
- All step files contain NO emoji literals
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
- `cd server && npm run build` exits 0 (no server changes in this plan; the build remains green from Plan 04)
All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query. Server endpoints consumed are owned by Plan 04 -- no server-side edits in this plan.
Task 5: Wizard container (level-up-wizard.tsx) — chrome, stepper, motion, footer wiring
client/src/features/characters/components/level-up/level-up-wizard.tsx
- .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (entire — chrome/header/stepper/footer all specified):
- Chrome §Component Contract — Wizard Chrome (lines 221-305)
- Motion §Motion (lines 654-665)
- States §States Inventory (lines 671-687)
- client/src/features/characters/components/level-up/wizard-state-reducer.ts (Task 2)
- client/src/features/characters/components/level-up/use-level-up-session.ts (Task 2)
- client/src/features/characters/components/level-up/level-up-step-*.tsx (Task 4 — all 11)
- client/src/features/characters/components/level-up/level-up-prereq-confirm-dialog.tsx (Task 3)
- client/src/features/characters/components/rest-modal.tsx (analog: REST + commit lifecycle)
- client/src/features/characters/components/add-feat-modal.tsx (analog: modal chrome)
Create `level-up-wizard.tsx` — the outer container that:
1. Renders the modal chrome per UI-SPEC §Component Contract — Wizard Chrome.
2. Loads the session via `useStartLevelUpMutation` on mount (start-or-resume).
3. Reads the step list from the session response: `LevelUpSessionDto.steps: StepKind[]` is populated server-side by Plan 04 (LevelingService.startOrResume runs `computeApplicableSteps` on the character + targetLevel and stores the result in the response payload). The wizard simply uses `session.steps` directly -- no client-side computation, no extra round-trip.
4. Initializes the reducer via `initWizardState(session, steps)`.
5. Renders the active step component based on `state.steps[state.currentIdx]`.
6. PATCHes the DRAFT after each meaningful state change (debounced — every 500ms after a SET_* event).
7. Wraps the active step in `` + `` for slide transitions per UI-SPEC §Motion.
8. Renders the stepper, header (with X close button + character name), and footer (Zurück / Weiter / Bestätigen / Zurück zur Übersicht based on state).
9. Handles the backdrop-click and X-close → opens the "Wizard mid-flow abbrechen" confirm dialog (reuse PrereqConfirmDialog with different copy, OR a separate small dialog component — planner discretion).
10. On Bestätigen: `useCommitLevelUpMutation.mutateAsync()` → on success, close the wizard, fire `onCommitted` callback (parent invalidates character query + shows toast).
cd client && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "level-up-wizard" || echo "tsc clean"
- File `level-up-wizard.tsx` exists with `LevelUpWizard` named export
- File contains `useReducer(wizardReducer` (uses Task 2 reducer)
- File contains `useStartLevelUpMutation` AND `useCommitLevelUpMutation` (uses Task 2 hooks)
- File contains `` (motion per UI-SPEC §Motion)
- File contains `useReducedMotion` (accessibility per UI-SPEC line 660)
- File contains `Stufenaufstieg — Stufe` (header German copy)
- File contains `Schritt {state.currentIdx + 1} von {state.steps.length}` or close (always-visible progress label per UI-SPEC line 257)
- File contains `h-11 w-11` (stepper hit-zone) AND `-m-1` (negative margin to compensate)
- File contains `aria-modal="true"` and `role="dialog"` (a11y)
- File contains `Bestätigen`, `Zurück`, `Weiter`, `Zurück zur Übersicht` German strings
- File contains NO emoji
- File contains NO `: any` outside comments
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
Wizard container compiles, mounts, manages reducer, drives steps via switch, runs motion, exposes Bestätigen wired to commit mutation, abort flow handled.
Task 6: character-sheet-page.tsx — Stufe-steigen button + 2 banner mounts + wizard mount
client/src/features/characters/components/character-sheet-page.tsx
- client/src/features/characters/components/character-sheet-page.tsx (entire file — header cluster lines 1607-1626; modal mount cluster lines 1652-1666; state pattern lines 122-132)
- .planning/phases/01-level-up-pf2e-regelkonform/01-UI-SPEC.md (lines 208-217 — Stufe-steigen button states; lines 588 — banner positions)
- .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 728-765 — exact insert positions)
Make four additions to `character-sheet-page.tsx`:
**Add 1 — State variables** (around lines 122-132 with the other useState's):
```tsx
const [showLevelUpWizard, setShowLevelUpWizard] = useState(false);
const [hasOpenDraft, setHasOpenDraft] = useState(false);
const [openDraftMeta, setOpenDraftMeta] = useState<{ targetLevel: number; updatedAt: string } | null>(null);
```
**Add 2 — Effect to detect open DRAFT on character load** (after the character query):
```tsx
useEffect(() => {
if (!character) return;
// GET /characters/:id/level-up should return the open DRAFT or 404
// Implementation: call api.startLevelUp() with no targetLevel — if a DRAFT exists, it's returned;
// if not, a new DRAFT is created. To avoid spurious DRAFT creation on a fresh load, instead
// add a separate read-only endpoint. Planner: extend Plan 04 with GET /characters/:id/level-up
// (returns DRAFT or 404). For now: lazily check by leaving hasOpenDraft = false until the user
// clicks 'Stufe steigen' and the start endpoint resolves it. The banner then hydrates on commit/discard mutations.
}, [character]);
```
Note for executor: DRAFT detection uses the `GET /characters/:characterId/level-up` endpoint already provided by Plan 04 (LevelingController.getOpenDraft -- 404 when none, 200 with the DRAFT row when present). Wire it via `api.getOpenLevelUpDraft(characterId)` (added in Task 1) and use `useQuery({ queryKey: ['levelUpDraft', characterId], queryFn: () => api.getOpenLevelUpDraft(characterId), retry: false })`. The banner reads `data` and `setHasOpenDraft(!!data)`. No server-side changes in this plan.
**Add 3 — Header button** (in the header cluster around lines 1607-1626 — INSERT AS FIRST in the cluster, left of Download):
```tsx
{(isOwner || isGM) && character.level < 20 && (
<Button
variant="default"
size="sm"
onClick={() => setShowLevelUpWizard(true)}
>
{hasOpenDraft ? <RotateCcw className="h-4 w-4" /> : <Sparkles className="h-4 w-4" />}
<span className="ml-1">{hasOpenDraft ? 'Stufe fortsetzen' : 'Stufe steigen'}</span>
</Button>
)}
```
Add the icon imports: `import { Sparkles, RotateCcw } from 'lucide-react';` (or extend existing import).
**Add 4 — Banner mounts** (above the avatar header AND below the avatar header for the violations banner):
```tsx
{/* Resume banner — above avatar */}
{hasOpenDraft && openDraftMeta && (
<LevelUpResumeBanner
targetLevel={openDraftMeta.targetLevel}
lastEditedRelative={formatRelativeDate(openDraftMeta.updatedAt)}
onResume={() => setShowLevelUpWizard(true)}
onDiscard={async () => {
// Use useDiscardLevelUpMutation
// ... wire it
}}
/>
)}
{/* Avatar header (existing) */}
{/* Violations banner — below avatar, above tabs */}
{character.prereqViolations?.violations && character.prereqViolations.violations.length > 0 && (
<LevelUpViolationsBanner violations={character.prereqViolations.violations} />
)}
{/* Tab navigation (existing) */}
```
Add a tiny `formatRelativeDate(iso: string): string` helper using `Intl.RelativeTimeFormat`:
```tsx
function formatRelativeDate(iso: string): string {
const diffMs = Date.now() - new Date(iso).getTime();
const days = Math.floor(diffMs / 86400000);
const rtf = new Intl.RelativeTimeFormat('de-DE', { numeric: 'auto' });
if (days === 0) return rtf.format(0, 'day'); // 'heute'
return rtf.format(-days, 'day'); // 'vor 3 Tagen'
}
```
**Add 5 — Wizard mount** (in the modal mounts area lines 1652-1666):
```tsx
{showLevelUpWizard && (
<LevelUpWizard
character={character}
onClose={() => setShowLevelUpWizard(false)}
onCommitted={() => {
setShowLevelUpWizard(false);
// refetchCharacter or rely on react-query invalidate from the commit mutation
}}
/>
)}
```
**Add 6 — Wire WebSocket callback** to invalidate character query when level_up_committed arrives:
Find where `useCharacterSocket` is called (probably elsewhere in the page or a parent). Add the new callback:
```tsx
useCharacterSocket({
characterId: character.id,
// ... existing callbacks ...
onLevelUpCommitted: () => {
queryClient.invalidateQueries({ queryKey: ['character', character.id] });
},
});
```
**Constraints:**
- Do NOT change any existing rendering logic for HP, conditions, inventory, etc. The additions are strictly additive.
- All visible new copy German.
- Use existing icon imports where possible; extend the `lucide-react` import line.
cd client && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "character-sheet-page" || echo "tsc clean"
- File `character-sheet-page.tsx` contains the literal string `Stufe steigen` (or `Stufe fortsetzen`)
- File contains the literal string `
Character sheet now has the entry point + the resume banner + the violations banner + the wizard mount + the WebSocket callback wired.
Task 7: Human verification — walk through the wizard on mobile viewport
The full Level-Up wizard (Plans 01-05). Player can click "Stufe steigen" on a character sheet, walk through 4-11 steps depending on level/class/FA/caster, see Vorher/Nachher in Review, click Bestätigen, and have the character mutated atomically with a single WebSocket broadcast.
Specifically:
- Server: 5 REST endpoints + 1 WebSocket event
- Client: 1 modal wizard + 11 step components + 2 banners + character-sheet integration
- DB: 4 new tables, partial unique index, ClassProgression seeded with 320+ rows
- Tests: ≥60 unit + integration tests passing
Manual verification is required for the UI surface because:
1. There is no client-side test framework yet (per VALIDATION.md decision — Phase 1 doesn't introduce vitest).
2. UI-SPEC compliance is visual.
3. The "feel" of the wizard on mobile (375×667) is a product judgment.
1. Start the dev environment:
```bash
# Terminal 1
cd server && npm run start:dev
# Terminal 2
cd client && npm run dev
```
2. Open Chrome DevTools, set viewport to **iPhone SE (375×667)** (Mobile-First per CLAUDE.md).
3. Log in as a user who owns at least one character below level 20.
4. Open that character's character sheet.
5. **Verify the "Stufe steigen" button appears** at the FIRST position in the header button cluster (left of Download), with `Sparkles` icon and German label.
6. Click "Stufe steigen". **Verify the wizard opens as a bottom-sheet** (mobile) with:
- Header: `Stufenaufstieg — Stufe N`, character name beneath, X close button on right.
- Stepper: dot row at top, with always-visible `Schritt 1 von M — Merkmale` progress label.
- Body: first step (Klassenmerkmale auf Stufe N) heading + sub-line.
- Footer: `Zurück` (disabled on first step) + `Weiter`.
7. **Walk through each applicable step.** For a Fighter L4→L5 (boost level), the steps are: Klassenmerkmale → Boost → Skill-Increase → Klassentalent → Fertigkeitstalent → Abstammungstalent → Übersicht. For each step, verify:
- The step heading + sub-line match `01-UI-SPEC.md` §Wizard-step screen headings (line 156).
- Touch targets are ≥44px (use DevTools "Inspect" → check `Computed → height/width`).
- Choice-cards have the correct source-color badges (Klasse=red, Abstammung=blue, etc.) per UI-SPEC §Color §Inherited Exceptions.
- For Boost step: 6 attribute rows with +/- buttons; tapping `+` increments the boost count, shows "wird {newScore}" preview, "+1 (Cap bei 18)" chip appears for STR if STR is at 18. Footer hint "X von 4 Boosts gewählt" updates.
- The Weiter button is disabled when the step is invalid (e.g. fewer than 4 boosts).
8. **Verify the prereq-confirm dialog (D-03):** if any feat in the Klassentalent / Fertigkeitstalent step has a yellow `AlertTriangle`, click it. A z-60 layered dialog appears with the raw prereq quote. `Trotzdem wählen` selects it; `Abbrechen` cancels.
9. **Reach the Review step.** Verify:
- Section A: Wahlen-Zusammenfassung lists every choice made, each with `Ändern` link.
- Section B: Vorher / Nachher cards with HP-Max, RK, Klassen-DC, Wahrnehmung, Saves. Numbers are in `font-mono`. `Sparkles` icon next to "Nachher". Positive deltas show as green chips.
- Section C: only appears for caster characters; shows slot/cantrip increment.
10. **Test the Ändern revision contract:** click `Ändern` on the Boost row. Wizard navigates back. Footer button changes from `Weiter` to `Zurück zur Übersicht` (with `ArrowLeft` icon). Change a boost. Click `Zurück zur Übersicht`. Wizard returns to Review.
11. **Click Bestätigen.** Verify:
- Spinner appears on the button.
- Wizard closes.
- Toast appears (planner discretion on toast lib): `Stufenaufstieg bestätigt — Stufe N.`
- Character header now shows the new level.
- HP-Max updated. AC, Saves, Klassen-DC updated.
- `hpCurrent` UNCHANGED (Pitfall #9 — verify by checking HP shows e.g. 12/63 NOT 63/63).
12. **Open a second browser tab** on the same character (different user with access — e.g. GM viewing a player's sheet). Confirm the new level + stat block appears within ~1s without a page reload (WebSocket sync).
13. **Test DRAFT-Resume:**
- Start a level-up, make some choices, close the wizard via X (the abort dialog appears: confirm with `Als Entwurf speichern und schließen`).
- Verify the DRAFT-Resume Banner now appears at the top of the character sheet with `Du hast eine offene Stufenaufstiegs-Session — Stufe N.`, `Zuletzt bearbeitet: vor wenigen Minuten.`
- Click `Verwerfen`. Confirm the discard dialog. The DRAFT row disappears.
14. **Test Pathbuilder-Import-Violations Banner (D-06):**
- Re-import a Pathbuilder JSON whose feats violate prereqs. Open the character sheet. The yellow violations banner appears below the avatar header. Click `Liste anzeigen` to expand the list of violating feats.
15. **Verify accessibility basics:**
- Tab through the wizard with the keyboard. All controls reachable.
- The stepper-dot wrappers are keyboard-focusable.
- The choice-cards announce their selected/locked/warning state via aria-label.
- In DevTools "Rendering" panel, enable `Emulate CSS prefers-reduced-motion: reduce`. Re-walk the wizard. Step transitions should fade only (no horizontal slide).
16. **Verify TypeScript + build green** (executor confirms before this checkpoint):
```bash
cd server && npm run build && npm test -- --testPathPattern=leveling
cd client && npx tsc --noEmit -p tsconfig.app.json && npm run build
```
17. If everything checks out, type `approved` to mark the checkpoint passed.
18. If any item fails, describe the issue and the executor revises.
Type "approved" or describe issues found
(no file writes — manual UI verification only)
Pause execution and present the <what-built> and <how-to-verify> sections to the developer. Wait for the <resume-signal>. Do not modify any files during this checkpoint.
Developer types "approved" — or describes issues found that block approval.
Developer has walked through the wizard on a 375×667 mobile viewport per the <how-to-verify> steps and replied "approved".
<threat_model>
Trust Boundaries
Boundary
Description
browser → REST
All wizard interactions cross HTTP via Plan 04 endpoints; client trusts server's authoritative responses.
browser → WebSocket
level_up_committed event arrives from server; client invalidates query — no inbound user-tampering surface.
translated text → React render
German prereq strings + class-feature descriptions from TranslationsService render in the prereq-confirm dialog and ChoiceCard.
STRIDE Threat Register
Threat ID
Category
Component
Disposition
Mitigation Plan
T-1-W4-01
Tampering
Player crafts a custom POST commit bypassing the wizard's validation
mitigate
Plan 04 server-side validation (DTO + commit guards) is the source of truth; the wizard is convenience UI. Any client tampering is rejected by the server.
T-1-W4-02
Injection
Stored XSS via German feat-prereq translation text rendered in PrereqConfirmDialog or ChoiceCard
mitigate
All German strings rendered via React text-binding ({prereqText} inside <p>), never dangerouslySetInnerHTML. Verified by code review — no usage of dangerouslySetInnerHTML in any of the new client files.
T-1-W4-03
Injection
Stored XSS via class-feature description rendered in ChoiceCard.description
mitigate
Same pattern — text-binding only. Description is line-clamp-2 truncated CSS-style; full text in the FeatDetailModal also uses text-binding.
T-1-W4-04
Information Disclosure
Wizard JSON state PATCHed to server contains user-typed text (e.g. notes) that surfaces to other character viewers
accept
Phase 1 wizard state contains only IDs (featId, optionKey, etc.) and structured choices — no user-typed text. Future steps that add notes need to reconsider.
T-1-W4-05
DoS
Rapid-fire PATCHes from the wizard overload the server
mitigate
The patchLevelUp call is debounced 500ms client-side (Task 5 implementation). Server endpoint is light (single update).
</threat_model>
After Task 6 (before checkpoint):
# Client TS cleancd client && npx tsc --noEmit -p tsconfig.app.json
# Client production build cleancd client && npm run build
# Server still builds (no server changes in this plan -- sanity check only)cd server && npm run build
# Full server test suite still greencd server && npm test
All four commands must exit 0 before the human checkpoint.
<success_criteria>
18 new client files exist in client/src/features/characters/components/level-up/
Review step uses font-mono for stat numbers, two-column Vorher/Nachher cards
Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button — chain re-validation deferred to v2 per must_haves.gotchas)
DRAFT-Resume banner implemented per UI-SPEC §Component Contract — DRAFT-Resume Banner
Pathbuilder-Import-Violations banner implemented per UI-SPEC §Component Contract — Pathbuilder-Import-Violations Banner
WebSocket level_up_committed callback wired to invalidate character query
Motion respects prefers-reduced-motion
TypeScript strict — no : any
Client production build clean (cd client && npm run build exits 0)
Server still builds and tests green (this plan adds no server changes)
Human verification checkpoint approved
</success_criteria>
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-05-SUMMARY.md` documenting:
- Final file list (all 18 new client files + 4 extended)
- Confirmation that the wizard consumes the Plan 04-owned endpoints (GET open-draft, GET feats, GET class-feature-options) without any server-side edits in this plan
- Toast library used (if any introduced) or pattern chosen (browser alert is NOT acceptable; planner discretion: react-hot-toast, sonner, or hand-rolled context — record the decision)
- Any deviations from UI-SPEC noted with rationale
- Result of the human-verification checkpoint