5 plans across 3 waves covering all 17 requirements: - Plan 01 (W1): Word lists, Progress extension, rate limits - Plan 02 (W2): Lesson 3-phase state machine, review/repeat - Plan 03 (W1): Forest visual scene with SVG + grid - Plan 04 (W3): Reward screen, pre-generation, full flow wiring - Plan 05 (W2): Parent area with stats and settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
484 lines
16 KiB
Markdown
484 lines
16 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
|
|
<interfaces>
|
|
<!-- From src/types.ts (after Plan 01): -->
|
|
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<string, number>;
|
|
lastPlayedLevels: number[];
|
|
}
|
|
|
|
export interface Settings {
|
|
id: 1;
|
|
audioEnabled: boolean;
|
|
apiKey: string;
|
|
apiCallsToday: { text: number; image: number };
|
|
lastApiCallDate: string;
|
|
}
|
|
|
|
<!-- From src/storage/db.ts (existing): -->
|
|
export function getProgress(db: IDBDatabase): Promise<Progress | null>;
|
|
export function saveProgress(db: IDBDatabase, progress: Progress): Promise<void>;
|
|
export function getSettings(db: IDBDatabase): Promise<Settings | null>;
|
|
export function saveSettings(db: IDBDatabase, settings: Settings): Promise<void>;
|
|
|
|
<!-- From src/companion/characters.ts (existing): -->
|
|
export interface CompanionDefinition { type: CompanionType; name: string; emoji: string; personality: string; avatarUrl: string; }
|
|
export const companions: CompanionDefinition[];
|
|
|
|
<!-- From src/app.ts (existing): -->
|
|
export function showScreen(name: ScreenName): void;
|
|
export function getDB(): IDBDatabase;
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Parent area HTML, CSS, and access gate</name>
|
|
<files>index.html, src/styles/main.css, src/ui/parent.ts</files>
|
|
<read_first>index.html, src/styles/main.css, src/types.ts, src/storage/db.ts, src/companion/characters.ts</read_first>
|
|
<action>
|
|
**Update index.html** — replace empty `<section id="screen-parent">` (per D-14, D-17, D-18):
|
|
|
|
```html
|
|
<section id="screen-parent" class="screen">
|
|
<div class="parent">
|
|
<button class="parent__back-btn" id="parent-back-btn">← Zurueck</button>
|
|
<h1 class="parent__title">Elternbereich</h1>
|
|
|
|
<!-- Code gate overlay -->
|
|
<div class="parent__gate" id="parent-gate">
|
|
<p>Bitte Code eingeben:</p>
|
|
<input type="password" id="parent-code-input" class="parent__code-input" maxlength="4" placeholder="____" inputmode="numeric">
|
|
<p class="parent__gate-error" id="parent-gate-error" style="display:none">Falscher Code</p>
|
|
</div>
|
|
|
|
<!-- Content (hidden until code entered) -->
|
|
<div class="parent__content" id="parent-content" style="display:none">
|
|
|
|
<section class="parent__section">
|
|
<h2>Uebersicht</h2>
|
|
<div class="parent__stats" id="parent-stats">
|
|
<div class="parent__stat">
|
|
<span class="parent__stat-label">Aktuelle Stufe</span>
|
|
<span class="parent__stat-value" id="stat-level">-</span>
|
|
</div>
|
|
<div class="parent__stat">
|
|
<span class="parent__stat-label">Einheiten</span>
|
|
<span class="parent__stat-value" id="stat-sessions">-</span>
|
|
</div>
|
|
<div class="parent__stat">
|
|
<span class="parent__stat-label">Genauigkeit</span>
|
|
<span class="parent__stat-value" id="stat-accuracy">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="parent__calendar" id="parent-calendar">
|
|
<h3>Uebungstage</h3>
|
|
<div class="parent__calendar-dots" id="calendar-dots"></div>
|
|
</div>
|
|
<div class="parent__errors" id="parent-errors">
|
|
<h3>Haeufigste Fehlertasten</h3>
|
|
<div id="error-keys-list"></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="parent__section">
|
|
<h2>Einstellungen</h2>
|
|
<div class="parent__settings">
|
|
<div class="parent__setting">
|
|
<label>Tastatur-Layout</label>
|
|
<select id="setting-layout">
|
|
<option value="de">DE (Deutschland)</option>
|
|
<option value="ch">CH (Schweiz)</option>
|
|
</select>
|
|
</div>
|
|
<div class="parent__setting">
|
|
<label>Audio</label>
|
|
<label class="parent__toggle">
|
|
<input type="checkbox" id="setting-audio">
|
|
<span>An/Aus</span>
|
|
</label>
|
|
</div>
|
|
<div class="parent__setting">
|
|
<label>Begleitfigur</label>
|
|
<select id="setting-companion"></select>
|
|
</div>
|
|
<div class="parent__setting">
|
|
<label>API-Key</label>
|
|
<input type="password" id="setting-apikey" class="parent__input" placeholder="Gemini API Key">
|
|
<button class="parent__save-btn" id="save-apikey-btn">Speichern</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
**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: `<span class="parent__error-key">E (12x)</span>`
|
|
- 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
|
|
</action>
|
|
<verify>
|
|
<automated>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</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire parent area into app.ts</name>
|
|
<files>src/app.ts</files>
|
|
<read_first>src/app.ts, src/ui/parent.ts</read_first>
|
|
<action>
|
|
**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.
|
|
</action>
|
|
<verify>
|
|
<automated>npx tsc --noEmit && grep -c "showParentCodePrompt" src/app.ts && grep -c "initParentScreen" src/app.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-komplettes-spielerlebnis/03-05-SUMMARY.md`
|
|
</output>
|