docs(03): create phase plan — komplettes spielerlebnis
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>
This commit is contained in:
@@ -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"
|
||||
---
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user