`
+ - Create an `
![]()
` with `src` = `URL.createObjectURL(element.imageBlob)`
+ - Set `alt` = `element.description`
+ - Apply random offset per D-07: `style="transform: translate(${randomOffset}px, ${randomOffset}px)"` where offset is +-10-20px random
+ - If `element.id === newElementId`, add class `forest__element--new` for animation
+ - Append to grid
+
+ 5. Store created Object URLs in a module-level array so they can be revoked on next call (prevent memory leak):
+ ```typescript
+ let activeObjectUrls: string[] = [];
+ ```
+ At start of function, revoke all old URLs via `URL.revokeObjectURL()`, then clear array.
+
+ Update `initForestScreen` to call `renderForestScene(db)` alongside existing `renderLevelMap`.
+
+ The `newElementId` parameter will be used by Plan 04 (reward flow) to trigger animation on the just-added element.
+
+
+ npx tsc --noEmit && npx vitest run
+
+
+ - grep "renderForestScene" src/forest/scene.ts returns export
+ - grep "getForestElements" src/forest/scene.ts returns match (loads from IDB)
+ - grep "createObjectURL" src/forest/scene.ts returns match (blob->URL conversion)
+ - grep "revokeObjectURL" src/forest/scene.ts returns match (memory cleanup)
+ - grep "forest__element--new" src/forest/scene.ts returns match (animation class)
+ - grep "renderForestScene" src/forest/scene.ts shows it's called from initForestScreen
+ - npx tsc --noEmit exits 0
+
+
Forest scene renders all stored elements from IndexedDB as images in the 4x3 grid. New elements get animation class. Object URLs cleaned up on re-render. Level map preserved alongside scene.
+
+
+
+
+
+- `npx tsc --noEmit` — no type errors
+- `npx vitest run` — all tests pass
+- Forest screen has SVG background visible with grid overlay
+- Elements from IndexedDB render as images with random offset
+
+
+
+1. Forest screen shows SVG woodland background (sky, hills, trees, clouds)
+2. 4x3 CSS grid overlays the SVG for element placement
+3. All stored ForestElements load from IndexedDB and display as images
+4. New elements animate with fade-in + scale-up (1.2s ease-out)
+5. Object URLs properly managed (revoked on re-render)
+6. Level map still visible below/alongside the scene
+
+
+
diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md
new file mode 100644
index 0000000..fcb0131
--- /dev/null
+++ b/.planning/phases/03-komplettes-spielerlebnis/03-04-PLAN.md
@@ -0,0 +1,515 @@
+---
+phase: 03-komplettes-spielerlebnis
+plan: 04
+type: execute
+wave: 3
+depends_on: [03-01, 03-02, 03-03]
+files_modified:
+ - src/app.ts
+ - src/forest/reward.ts
+ - index.html
+ - src/styles/main.css
+autonomous: true
+requirements: [RWRD-01, RWRD-02, RWRD-03, RWRD-04, TYPE-07]
+must_haves:
+ truths:
+ - "After lesson completion, reward screen shows with new forest element image"
+ - "Image pre-generation starts at lesson begin and is awaited on reward screen"
+ - "Placeholder 'Der Wald denkt nach...' with leaf animation shows while image loads"
+ - "Reward screen has 'Weiter ueben' and 'Zurueck zum Wald' buttons"
+ - "Full flow works: forest -> lesson (3 phases) -> reward -> forest"
+ - "New element persisted in IndexedDB and visible in forest after reward"
+ artifacts:
+ - path: "src/forest/reward.ts"
+ provides: "generateForestReward() pre-generation, initRewardScreen() display"
+ exports: ["startPreGeneration", "initRewardScreen"]
+ - path: "src/app.ts"
+ provides: "Updated flow: lesson -> reward -> forest, review/repeat triggers"
+ contains: "showScreen.*reward"
+ - path: "index.html"
+ provides: "Reward screen HTML structure"
+ contains: "screen-reward"
+ key_links:
+ - from: "src/app.ts"
+ to: "src/forest/reward.ts"
+ via: "startPreGeneration at lesson start, initRewardScreen at lesson end"
+ pattern: "startPreGeneration"
+ - from: "src/forest/reward.ts"
+ to: "src/api/gemini.ts"
+ via: "generateImage() for forest element"
+ pattern: "generateImage"
+ - from: "src/forest/reward.ts"
+ to: "src/storage/db.ts"
+ via: "saveForestElement() to persist"
+ pattern: "saveForestElement"
+ - from: "src/app.ts"
+ to: "src/forest/scene.ts"
+ via: "renderForestScene(db, newElementId) after reward"
+ pattern: "renderForestScene"
+---
+
+
+Wire the complete game flow: lesson -> reward -> forest. Build reward screen with pre-generated forest element image, companion comment, and navigation buttons. Connect review/repetition triggers in app.ts.
+
+Purpose: This closes the game loop — the child finishes a lesson, sees a magical reward, and returns to a growing forest.
+Output: Reward module, reward screen HTML/CSS, updated app.ts with full flow including review/repetition logic.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
+@.planning/phases/03-komplettes-spielerlebnis/03-02-SUMMARY.md
+@.planning/phases/03-komplettes-spielerlebnis/03-03-SUMMARY.md
+@src/app.ts
+@src/api/gemini.ts
+@src/companion/companion.ts
+@src/storage/db.ts
+@src/forest/scene.ts
+@src/types.ts
+@index.html
+@src/companion/fallbacks.ts
+
+
+
+export async function generateImage(prompt: string, referenceImages: Blob[], config: GeminiConfig): Promise;
+export async function generateText(prompt: string, systemPrompt: string, config: GeminiConfig): Promise;
+export async function checkRateLimit(db: IDBDatabase, type: 'text' | 'image'): Promise;
+export async function incrementApiCall(db: IDBDatabase, type: 'text' | 'image'): Promise;
+
+
+export function saveForestElement(db: IDBDatabase, element: ForestElement): Promise;
+export function getForestElements(db: IDBDatabase): Promise;
+export function getStyleReference(db: IDBDatabase): Promise;
+
+
+export async function getForestComment(characterType: CompanionType, elementDescription: string, db: IDBDatabase): Promise;
+
+
+export function getRandomFallbackForestComment(): string;
+import { getRandomFallbackImage } from './fallbacks'; // returns SVG blob
+
+
+export async function renderForestScene(db: IDBDatabase, newElementId?: number): Promise;
+export async function initForestScreen(db: IDBDatabase, onStartLesson: (level: number) => void): Promise;
+
+
+export function initLessonScreen(
+ db: IDBDatabase, level: number, layout: KeyboardLayout,
+ onComplete: (level: number, stats: { totalCorrect: number; totalErrors: number; errorKeys: Record }) => void,
+ onCancel?: () => void,
+ isReview?: boolean, showEncouragement?: boolean,
+): () => void;
+
+
+export interface Progress {
+ ...
+ totalCorrect: number;
+ totalErrors: number;
+ errorKeyCounts: Record;
+ lastPlayedLevels: number[];
+}
+
+
+
+
+
+
+ Task 1: Reward module with pre-generation and reward screen
+ src/forest/reward.ts, index.html, src/styles/main.css
+ src/api/gemini.ts, src/storage/db.ts, src/companion/companion.ts, src/companion/fallbacks.ts, src/types.ts, index.html, src/styles/main.css
+
+ **Create `src/forest/reward.ts`** (per D-10, D-11, D-12):
+
+ ```typescript
+ import { loadConfig } from '../api/config';
+ import { generateImage, checkRateLimit, incrementApiCall } from '../api/gemini';
+ import { getStyleReference, saveForestElement, getForestElements } from '../storage/db';
+ import { getForestComment } from '../companion/companion';
+ import { getRandomFallbackForestComment } from '../companion/fallbacks';
+ import type { CompanionType, ForestElement } from '../types';
+ ```
+
+ **Pre-generation (per D-11, RWRD-02):**
+ ```typescript
+ let pregenPromise: Promise<{ blob: Blob; description: string }> | null = null;
+
+ export function startPreGeneration(db: IDBDatabase): void {
+ pregenPromise = generateForestElement(db);
+ }
+ ```
+
+ `generateForestElement(db)` async function:
+ 1. Check rate limit for 'image': `await checkRateLimit(db, 'image')`
+ 2. If rate-limited, return fallback: use `getRandomFallbackImage()` from fallbacks.ts
+ - NOTE: Check if `getRandomFallbackImage` exists. If not, create inline fallback: load one of the SVGs from `src/assets/fallback-images/` via fetch.
+ - Actually, look at existing fallbacks.ts — it has `getRandomFallbackImage` or similar. If not, we need a simple function that picks a random fallback SVG and returns it as a Blob.
+ 3. Load config via `loadConfig()`. If no config, use fallback.
+ 4. Get style reference via `getStyleReference(db)`. Build referenceImages array (style ref blob if available).
+ 5. Pick random forest element type from a list: `['Blume', 'Pilz', 'Schmetterling', 'Vogel', 'Reh', 'Eichhoernchen', 'Hase', 'Frosch', 'Igel', 'Marienkaefer', 'Libelle', 'Schnecke']`
+ 6. Build prompt: `"Kinderbuch-Illustration, Aquarell-Stil, ${elementType}, magischer Wald, warm, einladend, fuer Kinder, Pastellfarben, kein Text, einzelnes Element auf transparentem Hintergrund"`
+ 7. Call `generateImage(prompt, referenceImages, config)`
+ 8. If result null, use fallback
+ 9. Increment API call: `await incrementApiCall(db, 'image')`
+ 10. Return `{ blob, description: elementType }`
+
+ **Fallback image function:**
+ Add to reward.ts:
+ ```typescript
+ const FALLBACK_SVGS = [
+ '/src/assets/fallback-images/blume.svg',
+ '/src/assets/fallback-images/pilz.svg',
+ '/src/assets/fallback-images/vogel.svg',
+ '/src/assets/fallback-images/schmetterling.svg',
+ '/src/assets/fallback-images/reh.svg',
+ '/src/assets/fallback-images/baum.svg',
+ '/src/assets/fallback-images/bach.svg',
+ ];
+
+ async function getRandomFallbackImage(): Promise<{ blob: Blob; description: string }> {
+ const idx = Math.floor(Math.random() * FALLBACK_SVGS.length);
+ const path = FALLBACK_SVGS[idx]!;
+ const name = path.split('/').pop()!.replace('.svg', '');
+ try {
+ const res = await fetch(path);
+ const blob = await res.blob();
+ return { blob, description: name };
+ } catch {
+ // Ultimate fallback: empty SVG blob
+ const svg = '';
+ return { blob: new Blob([svg], { type: 'image/svg+xml' }), description: 'element' };
+ }
+ }
+ ```
+
+ **initRewardScreen (per RWRD-01, RWRD-03, RWRD-04):**
+ ```typescript
+ export async function initRewardScreen(
+ db: IDBDatabase,
+ level: number,
+ characterType: CompanionType,
+ onContinue: () => void,
+ onBackToForest: () => void,
+ ): Promise // returns saved element ID
+ ```
+
+ 1. Show placeholder immediately: set `#reward-image` area to placeholder HTML:
+ ```html
+
+
🍃
+
🍂
+
🍃
+
Der Wald denkt nach...
+
+ ```
+ 2. Await `pregenPromise` (or `generateForestElement(db)` if promise is null)
+ 3. Get companion comment: `await getForestComment(characterType, result.description, db)`
+ 4. Determine grid position for new element: count existing elements, position = `{ x: col, y: row }` based on next free slot in 4x3 grid (slot index = existingCount % 12). Add random offset +-15px.
+ 5. Save to IndexedDB:
+ ```typescript
+ const element: ForestElement = {
+ levelCompleted: level,
+ imageBlob: result.blob,
+ imagePrompt: prompt,
+ description: result.description,
+ companionText: comment,
+ position: { x: col, y: row },
+ createdAt: new Date().toISOString(),
+ };
+ const elementId = await saveForestElement(db, element);
+ ```
+ 6. Replace placeholder with actual image: `URL.createObjectURL(result.blob)` in `
` tag
+ 7. Show companion comment in `#reward-companion-text`
+ 8. Wire buttons: `#reward-continue-btn` -> onContinue, `#reward-back-btn` -> onBackToForest
+ 9. Return elementId (for forest scene animation)
+
+ **Update index.html** — replace empty ``:
+ ```html
+
+
+
Schau mal!
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+ ```
+
+ **Add CSS to `src/styles/main.css`:**
+ ```css
+ /* Reward Screen */
+ .reward {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 2rem;
+ max-width: 600px;
+ margin: 0 auto;
+ text-align: center;
+ }
+
+ .reward__title {
+ font-family: var(--font-heading);
+ color: var(--accent-gold);
+ font-size: 1.8rem;
+ margin-bottom: 1.5rem;
+ }
+
+ .reward__image-area {
+ width: 280px;
+ height: 280px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 1.5rem;
+ border-radius: 16px;
+ background: var(--bg-forest);
+ overflow: hidden;
+ }
+
+ .reward__image-area img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ animation: reward-image-appear 0.8s ease-out;
+ }
+
+ @keyframes reward-image-appear {
+ from { opacity: 0; transform: scale(0.5); }
+ to { opacity: 1; transform: scale(1); }
+ }
+
+ .reward__placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ color: var(--text-warm);
+ font-family: var(--font-heading);
+ }
+
+ .reward__leaf {
+ font-size: 1.5rem;
+ animation: leaf-float 2s ease-in-out infinite;
+ }
+
+ .reward__leaf--1 { animation-delay: 0s; }
+ .reward__leaf--2 { animation-delay: 0.5s; }
+ .reward__leaf--3 { animation-delay: 1s; }
+
+ @keyframes leaf-float {
+ 0%, 100% { transform: translateY(0) rotate(0deg); }
+ 50% { transform: translateY(-10px) rotate(15deg); }
+ }
+
+ .reward__companion {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ padding: 1rem;
+ background: rgba(255, 255, 255, 0.7);
+ border-radius: 12px;
+ }
+
+ .reward__actions {
+ display: flex;
+ gap: 1rem;
+ }
+
+ .reward__btn {
+ font-family: var(--font-heading);
+ font-size: 1.1rem;
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+
+ .reward__btn:hover {
+ transform: scale(1.05);
+ }
+
+ .reward__btn--continue {
+ background: var(--accent-green);
+ color: white;
+ }
+
+ .reward__btn--back {
+ background: var(--bg-cream);
+ color: var(--text-warm);
+ border: 2px solid var(--accent-green);
+ }
+ ```
+
+
+ npx tsc --noEmit && grep -c "startPreGeneration" src/forest/reward.ts && grep -c "initRewardScreen" src/forest/reward.ts && grep -c "reward-continue-btn" index.html
+
+
+ - grep "startPreGeneration" src/forest/reward.ts returns export
+ - grep "initRewardScreen" src/forest/reward.ts returns export
+ - grep "generateImage" src/forest/reward.ts shows image generation call
+ - grep "saveForestElement" src/forest/reward.ts shows persistence
+ - grep "Der Wald denkt nach" src/forest/reward.ts shows placeholder text
+ - grep "reward-continue-btn" index.html returns match
+ - grep "reward-back-btn" index.html returns match
+ - grep "reward__placeholder" src/styles/main.css returns match
+ - grep "leaf-float" src/styles/main.css returns match (animated leaves)
+ - npx tsc --noEmit exits 0
+
+ Reward module with pre-generation, placeholder, fallback, IndexedDB persistence. Reward screen HTML with image area, companion comment, two action buttons. All CSS styled.
+
+
+
+ Task 2: Wire complete flow in app.ts with review/repeat triggers
+ src/app.ts
+ src/app.ts, src/forest/reward.ts, src/forest/scene.ts, src/ui/screens.ts, src/types.ts, src/storage/db.ts, src/companion/characters.ts
+
+ Rewrite flow logic in `src/app.ts` (per D-10, D-11, D-05, D-06, TYPE-07):
+
+ **Import additions:**
+ ```typescript
+ import { startPreGeneration, initRewardScreen } from './forest/reward';
+ import { renderForestScene } from './forest/scene';
+ ```
+
+ **Update `startLessonFromForest(level: number)`:**
+ 1. Determine if this is a review session (per D-05): `progress.totalSessions > 0 && progress.totalSessions % 3 === 0`
+ 2. Determine if encouragement needed (per D-06): check `progress.lastPlayedLevels` — if last 2 entries are the same level as current, set `showEncouragement = true`
+ 3. Start pre-generation: `startPreGeneration(db)` — fire and forget (per D-11)
+ 4. Call updated `initLessonScreen(db, level, layout, handleLessonComplete, handleLessonCancel, isReview, showEncouragement)`
+
+ **Update `handleLessonComplete`:**
+ Change signature to accept stats:
+ ```typescript
+ async function handleLessonComplete(
+ completedLevel: number,
+ stats: { totalCorrect: number; totalErrors: number; errorKeys: Record }
+ ): Promise
+ ```
+
+ 1. Update progress with stats (per D-15, D-16):
+ ```typescript
+ progress.totalCorrect += stats.totalCorrect;
+ progress.totalErrors += stats.totalErrors;
+ for (const [key, count] of Object.entries(stats.errorKeys)) {
+ progress.errorKeyCounts[key] = (progress.errorKeyCounts[key] ?? 0) + count;
+ }
+ // Track last played levels (keep last 3)
+ progress.lastPlayedLevels.push(completedLevel);
+ if (progress.lastPlayedLevels.length > 3) {
+ progress.lastPlayedLevels = progress.lastPlayedLevels.slice(-3);
+ }
+ ```
+ 2. Existing level advancement logic (keep as-is)
+ 3. Save progress
+ 4. Show REWARD screen instead of going directly to forest:
+ ```typescript
+ showScreen('reward');
+ const elementId = await initRewardScreen(
+ db,
+ completedLevel,
+ progress.selectedCharacter,
+ () => {
+ // "Weiter ueben" — start next lesson
+ startLessonFromForest(Math.min(progress.currentLevel, MAX_LEVEL));
+ },
+ async () => {
+ // "Zurueck zum Wald" — show forest with new element animated
+ await initForestScreen(db, startLessonFromForest);
+ await renderForestScene(db, elementId);
+ showScreen('forest');
+ },
+ );
+ ```
+
+ **Set companion avatar on reward screen:**
+ After `showScreen('reward')`, set the reward companion avatar:
+ ```typescript
+ const companion = companions.find(c => c.type === progress.selectedCharacter);
+ if (companion) {
+ const avatar = document.getElementById('reward-companion-avatar') as HTMLImageElement;
+ avatar.src = companion.avatarUrl;
+ avatar.alt = companion.name;
+ }
+ ```
+
+ **Handle backward compatibility:**
+ The `handleLessonComplete` in `startLessonFromForest` is passed as callback. Make sure the `initLessonScreen` onComplete signature matches (it was updated in Plan 02 to pass stats).
+
+ **Handle missing fields in existing progress data:**
+ When loading progress, ensure new fields have defaults:
+ ```typescript
+ if (!progress.errorKeyCounts) progress.errorKeyCounts = {};
+ if (!progress.lastPlayedLevels) progress.lastPlayedLevels = [];
+ if (progress.totalCorrect === undefined) progress.totalCorrect = 0;
+ if (progress.totalErrors === undefined) progress.totalErrors = 0;
+ ```
+ Add this migration logic after `getProgress(db)` calls.
+
+ **Parent area shortcut:**
+ Add global keydown listener for Ctrl+Shift+E (this will be used by Plan 05, but wire the detection here):
+ ```typescript
+ document.addEventListener('keydown', (e) => {
+ if (e.ctrlKey && e.shiftKey && e.key === 'E') {
+ e.preventDefault();
+ showParentCodePrompt();
+ }
+ });
+ ```
+ For now, `showParentCodePrompt` can be a stub that will be implemented in Plan 05.
+
+
+ npx tsc --noEmit && npx vitest run
+
+
+ - grep "startPreGeneration" src/app.ts returns match (pre-gen at lesson start)
+ - grep "initRewardScreen" src/app.ts returns match (reward after lesson)
+ - grep "showScreen.*reward" src/app.ts returns match (navigate to reward)
+ - grep "renderForestScene" src/app.ts returns match (forest update after reward)
+ - grep "totalSessions.*% 3" src/app.ts returns match (review trigger)
+ - grep "lastPlayedLevels" src/app.ts returns match (repeat detection)
+ - grep "errorKeyCounts" src/app.ts returns match (stats accumulation)
+ - grep "Ctrl.*Shift.*E" src/app.ts or grep "ctrlKey.*shiftKey" src/app.ts returns match
+ - npx tsc --noEmit exits 0
+
+ Complete flow wired: forest -> lesson (with pre-gen) -> reward (with image + comment) -> forest (with animation). Review triggers every 3rd session. Repetition protection tracks last played levels. Stats accumulated to progress.
+
+
+
+
+
+- `npx tsc --noEmit` — no type errors
+- `npx vitest run` — all tests pass
+- Full flow testable: start lesson -> complete 3 phases -> see reward -> navigate back
+
+
+
+1. Reward screen shows generated/fallback image with companion comment after every lesson
+2. Pre-generation starts at lesson begin, reward screen awaits it with placeholder
+3. Placeholder shows "Der Wald denkt nach..." with animated leaves
+4. "Weiter ueben" starts next lesson, "Zurueck zum Wald" returns to forest
+5. New element saved to IndexedDB and animated in forest grid
+6. Every 3rd session triggers review mode
+7. After 2x same level, encouragement displayed
+8. Exercise stats (correct, errors, errorKeys) accumulated in Progress
+
+
+
diff --git a/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md b/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md
new file mode 100644
index 0000000..5832a5d
--- /dev/null
+++ b/.planning/phases/03-komplettes-spielerlebnis/03-05-PLAN.md
@@ -0,0 +1,483 @@
+---
+phase: 03-komplettes-spielerlebnis
+plan: 05
+type: execute
+wave: 2
+depends_on: [03-01]
+files_modified:
+ - src/ui/parent.ts
+ - index.html
+ - src/styles/main.css
+ - src/app.ts
+autonomous: true
+requirements: [PRNT-01, PRNT-02, PRNT-03, PRNT-04]
+must_haves:
+ truths:
+ - "Ctrl+Shift+E opens a code input prompt"
+ - "Entering '1234' navigates to parent area screen"
+ - "Parent area shows current level, completed sessions, practice days as calendar dots"
+ - "Parent area shows average accuracy and most frequent error keys"
+ - "Settings allow changing layout, toggling audio, changing companion, entering API key"
+ artifacts:
+ - path: "src/ui/parent.ts"
+ provides: "initParentScreen() with stats display and settings panel"
+ exports: ["initParentScreen", "showParentCodePrompt"]
+ - path: "index.html"
+ provides: "Parent screen HTML with stats overview and settings form"
+ contains: "parent-stats"
+ key_links:
+ - from: "src/ui/parent.ts"
+ to: "src/storage/db.ts"
+ via: "getProgress, saveProgress, getSettings, saveSettings"
+ pattern: "getProgress"
+ - from: "src/app.ts"
+ to: "src/ui/parent.ts"
+ via: "showParentCodePrompt on Ctrl+Shift+E"
+ pattern: "showParentCodePrompt"
+---
+
+
+Build the parent area: gated access via Ctrl+Shift+E + code "1234", stats overview (level, sessions, practice days, accuracy, error keys), and settings (layout, audio, companion, API key).
+
+Purpose: Parents can see progress and adjust settings without the child being exposed to performance metrics.
+Output: Parent screen with stats and settings, access gate wired into app.ts.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/03-komplettes-spielerlebnis/03-01-SUMMARY.md
+@src/app.ts
+@src/storage/db.ts
+@src/types.ts
+@src/companion/characters.ts
+@index.html
+@src/styles/main.css
+
+
+
+export interface Progress {
+ id: 1;
+ currentLevel: number;
+ completedLevels: number[];
+ totalSessions: number;
+ sessionDates: string[]; // ISO date strings "YYYY-MM-DD"
+ selectedCharacter: CompanionType;
+ selectedLayout: KeyboardLayout;
+ totalCorrect: number;
+ totalErrors: number;
+ errorKeyCounts: Record;
+ lastPlayedLevels: number[];
+}
+
+export interface Settings {
+ id: 1;
+ audioEnabled: boolean;
+ apiKey: string;
+ apiCallsToday: { text: number; image: number };
+ lastApiCallDate: string;
+}
+
+
+export function getProgress(db: IDBDatabase): Promise
+
+
+
+
+
+ Task 1: Parent area HTML, CSS, and access gate
+ index.html, src/styles/main.css, src/ui/parent.ts
+ index.html, src/styles/main.css, src/types.ts, src/storage/db.ts, src/companion/characters.ts
+
+ **Update index.html** — replace empty `` (per D-14, D-17, D-18):
+
+ ```html
+
+
+
+
Elternbereich
+
+
+
+
Bitte Code eingeben:
+
+
Falscher Code
+
+
+
+
+
+
+ Uebersicht
+
+
+ Aktuelle Stufe
+ -
+
+
+ Einheiten
+ -
+
+
+ Genauigkeit
+ -
+
+
+
+
+
Haeufigste Fehlertasten
+
+
+
+
+
+ Einstellungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ **Add CSS to `src/styles/main.css`:**
+ ```css
+ /* Parent Area */
+ .parent {
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 1.5rem;
+ }
+
+ .parent__back-btn {
+ background: none;
+ border: none;
+ font-family: var(--font-heading);
+ font-size: 1rem;
+ color: var(--text-warm);
+ cursor: pointer;
+ margin-bottom: 1rem;
+ }
+
+ .parent__title {
+ font-family: var(--font-heading);
+ color: var(--text-dark);
+ margin-bottom: 1.5rem;
+ }
+
+ .parent__gate {
+ text-align: center;
+ padding: 2rem;
+ font-family: var(--font-body);
+ }
+
+ .parent__code-input {
+ font-size: 2rem;
+ text-align: center;
+ width: 120px;
+ padding: 0.5rem;
+ border: 2px solid var(--accent-purple);
+ border-radius: 8px;
+ font-family: var(--font-heading);
+ letter-spacing: 0.5rem;
+ margin-top: 0.5rem;
+ }
+
+ .parent__gate-error {
+ color: var(--accent-pink);
+ margin-top: 0.5rem;
+ font-size: 0.9rem;
+ }
+
+ .parent__section {
+ margin-bottom: 2rem;
+ }
+
+ .parent__section h2 {
+ font-family: var(--font-heading);
+ color: var(--text-dark);
+ border-bottom: 2px solid var(--bg-forest);
+ padding-bottom: 0.5rem;
+ margin-bottom: 1rem;
+ }
+
+ .parent__stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ }
+
+ .parent__stat {
+ background: var(--bg-forest);
+ border-radius: 12px;
+ padding: 1rem;
+ text-align: center;
+ }
+
+ .parent__stat-label {
+ display: block;
+ font-family: var(--font-body);
+ font-size: 0.85rem;
+ color: var(--text-warm);
+ margin-bottom: 0.25rem;
+ }
+
+ .parent__stat-value {
+ display: block;
+ font-family: var(--font-heading);
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-dark);
+ }
+
+ .parent__calendar-dots {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 0.5rem;
+ }
+
+ .parent__calendar-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--accent-green);
+ }
+
+ .parent__calendar-dot--empty {
+ background: var(--bg-cream);
+ border: 1px solid var(--text-light);
+ }
+
+ .parent__errors h3,
+ .parent__calendar h3 {
+ font-family: var(--font-heading);
+ font-size: 1rem;
+ color: var(--text-warm);
+ margin-bottom: 0.5rem;
+ }
+
+ .parent__error-key {
+ display: inline-block;
+ background: var(--accent-pink);
+ color: white;
+ padding: 0.25rem 0.75rem;
+ border-radius: 8px;
+ margin: 0.25rem;
+ font-family: var(--font-heading);
+ font-size: 0.9rem;
+ }
+
+ .parent__settings {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .parent__setting {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ .parent__setting label:first-child {
+ min-width: 120px;
+ font-family: var(--font-body);
+ color: var(--text-warm);
+ }
+
+ .parent__setting select,
+ .parent__input {
+ font-family: var(--font-body);
+ padding: 0.5rem;
+ border: 1px solid var(--text-light);
+ border-radius: 8px;
+ font-size: 0.95rem;
+ }
+
+ .parent__save-btn {
+ font-family: var(--font-heading);
+ padding: 0.5rem 1rem;
+ background: var(--accent-green);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ }
+ ```
+
+ **Create `src/ui/parent.ts`:**
+
+ ```typescript
+ import { getProgress, saveProgress, getSettings, saveSettings } from '../storage/db';
+ import { companions } from '../companion/characters';
+ import type { CompanionType, KeyboardLayout } from '../types';
+ import { showScreen, getDB } from '../app';
+ ```
+
+ **`showParentCodePrompt()`** (per D-14, PRNT-01):
+ - Show parent screen: `showScreen('parent')`
+ - Show gate, hide content
+ - Focus code input
+ - On input change, if value === '1234': hide gate, show content, call `loadParentData()`
+ - If value.length === 4 but wrong: show error, clear after 1.5s
+
+ **`initParentScreen(db: IDBDatabase, onBack: () => void)`:**
+ Wire back button to `onBack` callback.
+
+ **`loadParentData()`** (per D-17, PRNT-02, PRNT-03):
+ 1. Get progress from IndexedDB
+ 2. Set `#stat-level` to `progress.currentLevel`
+ 3. Set `#stat-sessions` to `progress.totalSessions`
+ 4. Calculate accuracy: `progress.totalCorrect + progress.totalErrors > 0 ? Math.round(progress.totalCorrect / (progress.totalCorrect + progress.totalErrors) * 100) : 0`
+ 5. Set `#stat-accuracy` to `${accuracy}%`
+ 6. Render calendar dots in `#calendar-dots`:
+ - Get last 30 days as date strings
+ - For each day: if in `progress.sessionDates`, render green dot, else empty dot
+ 7. Render error keys in `#error-keys-list`:
+ - Sort `progress.errorKeyCounts` by value descending
+ - Show top 5 keys as colored badges: `E (12x)`
+ - If no errors: show "Noch keine Fehler erfasst"
+
+ **Settings (per D-18, PRNT-04):**
+ 1. Populate companion select from `companions` array
+ 2. Set current values from progress (layout, companion) and settings (audio, apiKey)
+ 3. On layout change: `saveProgress(db, { ...progress, selectedLayout: value })`
+ 4. On companion change: `saveProgress(db, { ...progress, selectedCharacter: value })`
+ 5. On audio toggle: `saveSettings(db, { ...settings, audioEnabled: checked })`
+ 6. On save API key: `saveSettings(db, { ...settings, apiKey: value })`
+ 7. Each save shows brief "Gespeichert!" feedback
+
+
+ npx tsc --noEmit && grep -c "showParentCodePrompt" src/ui/parent.ts && grep -c "parent-gate" index.html && grep -c "parent__calendar-dot" src/styles/main.css
+
+
+ - grep "showParentCodePrompt" src/ui/parent.ts returns export
+ - grep "1234" src/ui/parent.ts returns match (access code)
+ - grep "parent-gate" index.html returns match (gate overlay)
+ - grep "stat-level" index.html returns match (stats display)
+ - grep "stat-accuracy" index.html returns match
+ - grep "calendar-dots" index.html returns match
+ - grep "error-keys-list" index.html returns match
+ - grep "setting-layout" index.html returns match (layout setting)
+ - grep "setting-companion" index.html returns match (companion setting)
+ - grep "setting-apikey" index.html returns match (API key setting)
+ - grep "parent__calendar-dot" src/styles/main.css returns match
+ - grep "parent__error-key" src/styles/main.css returns match
+ - npx tsc --noEmit exits 0
+
+ Parent area gate with code "1234", stats overview with level/sessions/accuracy/calendar-dots/error-keys, settings for layout/audio/companion/API-key. All styled and functional.
+
+
+
+ Task 2: Wire parent area into app.ts
+ src/app.ts
+ src/app.ts, src/ui/parent.ts
+
+ **Wire parent area access in `src/app.ts`** (per D-14, PRNT-01):
+
+ 1. Import: `import { showParentCodePrompt, initParentScreen } from './ui/parent';`
+
+ 2. In `initApp()`, after DB is opened, call:
+ ```typescript
+ initParentScreen(db, async () => {
+ // Back from parent -> return to forest
+ await initForestScreen(db, startLessonFromForest);
+ showScreen('forest');
+ });
+ ```
+
+ 3. Replace the stub `showParentCodePrompt` reference (if Plan 04 added a stub) or add the global keydown listener:
+ ```typescript
+ document.addEventListener('keydown', (e) => {
+ if (e.ctrlKey && e.shiftKey && e.key === 'E') {
+ e.preventDefault();
+ showParentCodePrompt();
+ }
+ });
+ ```
+ Add this in `initApp()` after DB setup. Ensure it's only added once.
+
+ 4. Make sure `showParentCodePrompt` properly uses the DB reference. Since `parent.ts` imports `getDB` from `app.ts`, it can access the DB.
+
+
+ npx tsc --noEmit && grep -c "showParentCodePrompt" src/app.ts && grep -c "initParentScreen" src/app.ts
+
+
+ - grep "import.*showParentCodePrompt" src/app.ts returns match
+ - grep "import.*initParentScreen" src/app.ts returns match
+ - grep "ctrlKey.*shiftKey" src/app.ts returns match (keyboard shortcut)
+ - grep "initParentScreen" src/app.ts shows it's called in initApp
+ - npx tsc --noEmit exits 0
+
+ Parent area fully wired: Ctrl+Shift+E triggers code prompt, "1234" opens parent screen, back button returns to forest. Global listener registered once in initApp.
+
+
+
+
+
+- `npx tsc --noEmit` — no type errors
+- `npx vitest run` — all tests pass
+- Parent area accessible via Ctrl+Shift+E + "1234"
+- Stats show current data, settings persist changes
+
+
+
+1. Ctrl+Shift+E opens code input overlay on parent screen
+2. Code "1234" reveals parent content, wrong code shows error
+3. Overview shows: current level, total sessions, accuracy percentage
+4. Calendar dots show last 30 days with green dots for practice days
+5. Error keys shown as badges, sorted by frequency, top 5
+6. Settings: layout change, audio toggle, companion change, API key save — all persist to IndexedDB
+7. Back button returns to forest screen
+
+
+