subagent-driven-development

---
name: subagent-driven-development
description: "Execute plans via delegate_task subagents (2-stage review)."
version: 1.1.0
author: Hermes Agent (adapted from obra/superpowers)
license: MIT
metadata:
  hermes:
    tags: [delegation, subagent, implementation, workflow, parallel]
    related_skills: [writing-plans, requesting-code-review, test-driven-development]
---

# Subagent-Driven Development

## Overview

Execute implementation plans by dispatching fresh subagents per task with systematic two-stage review.

**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration.

## When to Use

Use this skill when:
- You have an implementation plan (from writing-plans skill or user requirements)
- Tasks are mostly independent
- Quality and spec compliance are important
- You want automated review between tasks

**vs. manual execution:**
- Fresh context per task (no confusion from accumulated state)
- Automated review process catches issues early
- Consistent quality checks across all tasks
- Subagents can ask questions before starting work

## The Process

### 1. Read and Parse Plan

Read the plan file. Extract ALL tasks with their full text and context upfront. Create a todo list:

```python
# Read the plan
read_file("docs/plans/feature-plan.md")

# Create todo list with all tasks
todo([
    {"id": "task-1", "content": "Create User model with email field", "status": "pending"},
    {"id": "task-2", "content": "Add password hashing utility", "status": "pending"},
    {"id": "task-3", "content": "Create login endpoint", "status": "pending"},
])
```

**Key:** Read the plan ONCE. Extract everything. Don't make subagents read the plan file — provide the full task text directly in context.

### 2. Per-Task Workflow

For EACH task in the plan:

#### Step 1: Dispatch Implementer Subagent

Use `delegate_task` with complete context:

```python
delegate_task(
    goal="Implement Task 1: Create User model with email and password_hash fields",
    context="""
    TASK FROM PLAN:
    - Create: src/models/user.py
    - Add User class with email (str) and password_hash (str) fields
    - Use bcrypt for password hashing
    - Include __repr__ for debugging

    FOLLOW TDD:
    1. Write failing test in tests/models/test_user.py
    2. Run: pytest tests/models/test_user.py -v (verify FAIL)
    3. Write minimal implementation
    4. Run: pytest tests/models/test_user.py -v (verify PASS)
    5. Run: pytest tests/ -q (verify no regressions)
    6. Commit: git add -A && git commit -m "feat: add User model with password hashing"

    PROJECT CONTEXT:
    - Python 3.11, Flask app in src/app.py
    - Existing models in src/models/
    - Tests use pytest, run from project root
    - bcrypt already in requirements.txt
    """,
    toolsets=['terminal', 'file']
)
```

#### Step 2: Dispatch Spec Compliance Reviewer

After the implementer completes, verify against the original spec:

```python
delegate_task(
    goal="Review if implementation matches the spec from the plan",
    context="""
    ORIGINAL TASK SPEC:
    - Create src/models/user.py with User class
    - Fields: email (str), password_hash (str)
    - Use bcrypt for password hashing
    - Include __repr__

    CHECK:
    - [ ] All requirements from spec implemented?
    - [ ] File paths match spec?
    - [ ] Function signatures match spec?
    - [ ] Behavior matches expected?
    - [ ] Nothing extra added (no scope creep)?

    OUTPUT: PASS or list of specific spec gaps to fix.
    """,
    toolsets=['file']
)
```

**If spec issues found:** Fix gaps, then re-run spec review. Continue only when spec-compliant.

#### Step 3: Dispatch Code Quality Reviewer

After spec compliance passes:

```python
delegate_task(
    goal="Review code quality for Task 1 implementation",
    context="""
    FILES TO REVIEW:
    - src/models/user.py
    - tests/models/test_user.py

    CHECK:
    - [ ] Follows project conventions and style?
    - [ ] Proper error handling?
    - [ ] Clear variable/function names?
    - [ ] Adequate test coverage?
    - [ ] No obvious bugs or missed edge cases?
    - [ ] No security issues?

    OUTPUT FORMAT:
    - Critical Issues: [must fix before proceeding]
    - Important Issues: [should fix]
    - Minor Issues: [optional]
    - Verdict: APPROVED or REQUEST_CHANGES
    """,
    toolsets=['file']
)
```

**If quality issues found:** Fix issues, re-review. Continue only when approved.

#### Step 4: Mark Complete

```python
todo([{"id": "task-1", "content": "Create User model with email field", "status": "completed"}], merge=True)
```

### 3. Final Review

After ALL tasks are complete, dispatch a final integration reviewer:

```python
delegate_task(
    goal="Review the entire implementation for consistency and integration issues",
    context="""
    All tasks from the plan are complete. Review the full implementation:
    - Do all components work together?
    - Any inconsistencies between tasks?
    - All tests passing?
    - Ready for merge?
    """,
    toolsets=['terminal', 'file']
)
```

### 4. Verify and Commit

```bash
# Run full test suite
pytest tests/ -q

# Review all changes
git diff --stat

# Final commit if needed
git add -A && git commit -m "feat: complete [feature name] implementation"
```

## Task Granularity

**Each task = 2-5 minutes of focused work.**

**Too big:**
- "Implement user authentication system"

**Right size:**
- "Create User model with email and password fields"
- "Add password hashing function"
- "Create login endpoint"
- "Add JWT token generation"
- "Create registration endpoint"

## Red Flags — Never Do These

- Start implementation without a plan
- Skip reviews (spec compliance OR code quality)
- Proceed with unfixed critical/important issues
- Dispatch multiple implementation subagents for tasks that touch the same files
- Make subagent read the plan file (provide full text in context instead)
- Skip scene-setting context (subagent needs to understand where the task fits)
- Ignore subagent questions (answer before letting them proceed)
- Accept "close enough" on spec compliance
- Skip review loops (reviewer found issues → implementer fixes → review again)
- Let implementer self-review replace actual review (both are needed)
- **Start code quality review before spec compliance is PASS** (wrong order)
- Move to next task while either review has open issues

## Handling Issues

### If Subagent Asks Questions

- Answer clearly and completely
- Provide additional context if needed
- Don't rush them into implementation

### If Reviewer Finds Issues

- Implementer subagent (or a new one) fixes them
- Reviewer reviews again
- Repeat until approved
- Don't skip the re-review

### If Subagent Fails a Task

- Dispatch a new fix subagent with specific instructions about what went wrong
- Don't try to fix manually in the controller session (context pollution)

## Efficiency Notes

**Why fresh subagent per task:**
- Prevents context pollution from accumulated state
- Each subagent gets clean, focused context
- No confusion from prior tasks' code or reasoning

**Why two-stage review:**
- Spec review catches under/over-building early
- Quality review ensures the implementation is well-built
- Catches issues before they compound across tasks

**Cost trade-off:**
- More subagent invocations (implementer + 2 reviewers per task)
- But catches issues early (cheaper than debugging compounded problems later)

## Integration with Other Skills

### With writing-plans

This skill EXECUTES plans created by the writing-plans skill:
1. User requirements → writing-plans → implementation plan
2. Implementation plan → subagent-driven-development → working code

### With test-driven-development

Implementer subagents should follow TDD:
1. Write failing test first
2. Implement minimal code
3. Verify test passes
4. Commit

Include TDD instructions in every implementer context.

### With requesting-code-review

The two-stage review process IS the code review. For final integration review, use the requesting-code-review skill's review dimensions.

### With systematic-debugging

If a subagent encounters bugs during implementation:
1. Follow systematic-debugging process
2. Find root cause before fixing
3. Write regression test
4. Resume implementation

## Example Workflow

```
[Read plan: docs/plans/auth-feature.md]
[Create todo list with 5 tasks]

--- Task 1: Create User model ---
[Dispatch implementer subagent]
  Implementer: "Should email be unique?"
  You: "Yes, email must be unique"
  Implementer: Implemented, 3/3 tests passing, committed.

[Dispatch spec reviewer]
  Spec reviewer: ✅ PASS — all requirements met

[Dispatch quality reviewer]
  Quality reviewer: ✅ APPROVED — clean code, good tests

[Mark Task 1 complete]

--- Task 2: Password hashing ---
[Dispatch implementer subagent]
  Implementer: No questions, implemented, 5/5 tests passing.

[Dispatch spec reviewer]
  Spec reviewer: ❌ Missing: password strength validation (spec says "min 8 chars")

[Implementer fixes]
  Implementer: Added validation, 7/7 tests passing.

[Dispatch spec reviewer again]
  Spec reviewer: ✅ PASS

[Dispatch quality reviewer]
  Quality reviewer: Important: Magic number 8, extract to constant
  Implementer: Extracted MIN_PASSWORD_LENGTH constant
  Quality reviewer: ✅ APPROVED

[Mark Task 2 complete]

... (continue for all tasks)

[After all tasks: dispatch final integration reviewer]
[Run full test suite: all passing]
[Done!]
```

## Remember

```
Fresh subagent per task
Two-stage review every time
Spec compliance FIRST
Code quality SECOND
Never skip reviews
Catch issues early
```

**Quality is not an accident. It's the result of systematic process.**

## Further reading (load when relevant)

When the orchestration involves significant context usage, long review loops, or complex validation checkpoints, load these references for the specific discipline:

- **`references/context-budget-discipline.md`** — Four-tier context degradation model (PEAK / GOOD / DEGRADING / POOR), read-depth rules that scale with context window size, and early warning signs of silent degradation. Load when a run will clearly consume significant context (multi-phase plans, many subagents, large artifacts).
- **`references/gates-taxonomy.md`** — The four canonical gate types (Pre-flight, Revision, Escalation, Abort) with behavior, recovery, and examples. Load when designing or reviewing any workflow that has validation checkpoints — use the vocabulary explicitly so each gate has defined entry, failure behavior, and resumption rules.

Both references adapted from gsd-build/get-shit-done (MIT © 2025 Lex Christopherson).


Erstmal die App gründlich inspizieren, bevor ich was anfasse.

Explanation:
Boss wants me to add a prayer times page to his MuslimOS/Ehlisünnet app. Let me first thoroughly inspect the existing app to understand its look, feel, and mechanics before delegating the coding work to Kimi k2.7-code.

Let me start by exploring the MuslimOS app structure.

terminal

find /DATA/AppData/muslimos -maxdepth 4 -type f 2>/dev/null | head -80


zimaos-platform

# MuslimOS / EhliSünnet App: Datenstruktur

Die App speichert Daten in `/DATA/AppData/muslimos/data/db.json`.

## Top-Level Abschnitte

- `tahajjud`: tägliche Tahajjud-Einträge mit `rakats`, `time`, `note`
- `sunnah`: täglich erledigte Sünnet-/Nafile-Namaz-Keys
- `evrad`: tägliche Evrad mit `short` und `long`
- `hifz`: Kuran-Hifz-Fortschritt (Sure → Anzahl Wiederholungen)

## Sünnet- und Nafile-Namaz-Liste

Statisch definiert in `/DATA/AppData/muslimos/app/public/app.js`:

### Müekked Sünnetler (stark empfohlen)

| Key | Name | Zeitpunkt | Rekât |
|-----|------|-----------|-------|
| sabah | Sabah sünneti | vor Fajr-Fard | 2 |
| ogle_once | Öğle ilk sünneti | vor Dhuhr-Fard | 4 |
| ogle_son | Öğle son sünneti | nach Dhuhr-Fard | 2 |
| aksam | Akşam sünneti | nach Maghrib-Fard | 2 |
| yatsi_son | Yatsı son sünneti | nach Ishaa-Fard | 2 |

### Gayr-i Müekked

| Key | Name | Zeitpunkt | Rekât |
|-----|------|-----------|-------|
| ikindi | İkindi sünneti | vor Asr-Fard | 4 |
| yatsi_once | Yatsı ilk sünneti | vor Ishaa-Fard | 4 |

### Nafile Namazlar

| Key | Name | Zeitpunkt | Rekât |
|-----|------|-----------|-------|
| kusluk | Kuşluk (Duhâ) | nach Sonnenaufgang | 2–8 |
| evvabin | Evvâbîn | nach Maghrib | 6 |
| israk | İşrak | nach Sonnenaufgang | 2 |
| tahiyyat | Tahiyyetü'l-mescid | beim Betreten einer Moschee | 2 |

## Beispiel: Nafile-Zeitplan ermitteln

1. Aktuelle Gebetszeiten für Wien via Aladhan-API holen:
   ```bash
   curl -s "https://api.aladhan.com/v1/timingsByAddress/$(date +%d-%m-%Y)?address=Vienna,Austria&method=13&school=hanafi" | python3 -m json.tool
   ```
2. Kuşluk/Duhâ ab ca. 15–20 Minuten nach Sonnenaufgang bis kurz vor Dhuhr.
3. İşrak unmittelbar nach Sonnenaufgang.
4. Tahiyyetü'l-mescid beim Betreten einer Moschee.
5. Evvâbîn nach Maghrib, vor Ishaa.

## Dateipfade

- App-Code: `/DATA/AppData/muslimos/app/`
- Datenbank: `/DATA/AppData/muslimos/data/db.json`
- Compose: `/DATA/.casaos/apps/muslimos/docker-compose.yml` (root-protected, use sudo)

## Compose-Details

```yaml
name: muslimos
services:
  muslimos:
    container_name: muslimos
    image: node:22-alpine
    ports:
      - "8099:3000"
    volumes:
      - /DATA/AppData/muslimos/app:/app
      - /DATA/AppData/muslimos/data:/app/data
```


terminal

Output:
/DATA/AppData/muslimos/casaos-compose.yml
/DATA/AppData/muslimos/data/db.json
/DATA/AppData/muslimos/Dockerfile
/DATA/AppData/muslimos/app/public/index.html
/DATA/AppData/muslimos/app/public/icon.png
/DATA/AppData/muslimos/app/public/styles.css
/DATA/AppData/muslimos/app/public/icon-tile.svg
/DATA/AppData/muslimos/app/public/icon-maskable.png
/DATA/AppData/muslimos/app/public/icon-512.png
/DATA/AppData/muslimos/app/public/app.js
/DATA/AppData/muslimos/app/public/quran-data.js
/DATA/AppData/muslimos/app/public/icon-192.png
/DATA/AppData/muslimos/app/public/sw.js
/DATA/AppData/muslimos/app/public/manifest.webmanifest
/DATA/AppData/muslimos/app/public/icon.svg
/DATA/AppData/muslimos/app/server.js


Jetzt lese ich die komplette App, um Look & Feel und Mechanik zu verstehen.

Explanation:
Boss wants me to add a prayer times page to his MuslimOS/Ehlisünnet app. Let me first thoroughly inspect the existing app to understand its look, feel, and mechanics before delegating the coding work to Kimi k2.7-code.

Let me read the key files: server.js, app.js, index.html, styles.css, and db.json to understand the full architecture.


     1|'use strict';
     2|
     3|/*
     4| * MuslimOS — Ibadet-Tracker
     5| * Reiner Node.js-Server, KEINE externen Abhängigkeiten.
     6| * Persistenz: atomare Schreibvorgänge in eine JSON-Datei.
     7| */
     8|
     9|const http = require('node:http');
    10|const fs = require('node:fs');
    11|const fsp = require('node:fs/promises');
    12|const path = require('node:path');
    13|const crypto = require('node:crypto');
    14|
    15|const PORT = process.env.PORT || 3000;
    16|const ROOT = __dirname;
    17|const PUBLIC_DIR = path.join(ROOT, 'public');
    18|const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
    19|const DB_FILE = path.join(DATA_DIR, 'db.json');
    20|
    21|// ---------------------------------------------------------------------------
    22|// Datenhaltung
    23|// ---------------------------------------------------------------------------
    24|
    25|const DEFAULT_DB = {
    26|  version: 1,
    27|  config: { pinHash: null },
    28|  tahajjud: {}, // { "YYYY-MM-DD": { rakats, time, note } }
    29|  sunnah: {},   // { "YYYY-MM-DD": ["key", ...] }
    30|  evrad: {},    // { "YYYY-MM-DD": { short: n, long: n } }
    31|  hifz: {},     // { "<surahNo>": memorizedAyahs }
    32|};
    33|
    34|let db = structuredClone(DEFAULT_DB);
    35|let saveTimer = null;
    36|let saving = false;
    37|let saveAgain = false;
    38|
    39|function loadDb() {
    40|  try {
    41|    const raw = fs.readFileSync(DB_FILE, 'utf8');
    42|    const parsed = JSON.parse(raw);
    43|    db = Object.assign(structuredClone(DEFAULT_DB), parsed);
    44|    db.config = Object.assign({ pinHash: null }, parsed.config || {});
    45|    for (const k of ['tahajjud', 'sunnah', 'evrad', 'hifz']) {
    46|      if (typeof db[k] !== 'object' || db[k] === null) db[k] = {};
    47|    }
    48|    console.log('[muslimos] DB geladen:', DB_FILE);
    49|  } catch (err) {
    50|    if (err.code === 'ENOENT') {
    51|      console.log('[muslimos] Keine DB gefunden, lege neue an.');
    52|      scheduleSave();
    53|    } else {
    54|      console.error('[muslimos] DB-Lesefehler, nutze leere DB:', err.message);
    55|    }
    56|  }
    57|}
    58|
    59|function scheduleSave() {
    60|  if (saveTimer) return;
    61|  saveTimer = setTimeout(() => {
    62|    saveTimer = null;
    63|    persist();
    64|  }, 250);
    65|}
    66|
    67|async function persist() {
    68|  if (saving) { saveAgain = true; return; }
    69|  saving = true;
    70|  try {
    71|    await fsp.mkdir(DATA_DIR, { recursive: true });
    72|    const tmp = DB_FILE + '.tmp-' + process.pid;
    73|    await fsp.writeFile(tmp, JSON.stringify(db, null, 2), 'utf8');
    74|    await fsp.rename(tmp, DB_FILE);
    75|  } catch (err) {
    76|    console.error('[muslimos] DB-Schreibfehler:', err.message);
    77|  } finally {
    78|    saving = false;
    79|    if (saveAgain) { saveAgain = false; scheduleSave(); }
    80|  }
    81|}
    82|
    83|// ---------------------------------------------------------------------------
    84|// Helfer
    85|// ---------------------------------------------------------------------------
    86|
    87|const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
    88|const sha256 = (s) => crypto.createHash('sha256').update(String(s)).digest('hex');
    89|const clampInt = (v, min, max) => {
    90|  let n = parseInt(v, 10);
    91|  if (!Number.isFinite(n)) n = min;
    92|  if (n < min) n = min;
    93|  if (max != null && n > max) n = max;
    94|  return n;
    95|};
    96|
    97|function sendJson(res, status, obj) {
    98|  const body = JSON.stringify(obj);
    99|  res.writeHead(status, {
   100|    'Content-Type': 'application/json; charset=utf-8',
   101|    'Cache-Control': 'no-store',
   102|  });
   103|  res.end(body);
   104|}
   105|
   106|function readBody(req, limit = 1_000_000) {
   107|  return new Promise((resolve, reject) => {
   108|    let size = 0;
   109|    const chunks = [];
   110|    req.on('data', (c) => {
   111|      size += c.length;
   112|      if (size > limit) { reject(new Error('Body zu groß')); req.destroy(); return; }
   113|      chunks.push(c);
   114|    });
   115|    req.on('end', () => {
   116|      if (!chunks.length) return resolve({});
   117|      try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
   118|      catch (e) { reject(new Error('Ungültiges JSON')); }
   119|    });
   120|    req.on('error', reject);
   121|  });
   122|}
   123|
   124|function publicState() {
   125|  return {
   126|    tahajjud: db.tahajjud,
   127|    sunnah: db.sunnah,
   128|    evrad: db.evrad,
   129|    hifz: db.hifz,
   130|    config: { pinRequired: !!db.config.pinHash },
   131|  };
   132|}
   133|
   134|// ---------------------------------------------------------------------------
   135|// Statische Dateien
   136|// ---------------------------------------------------------------------------
   137|
   138|const MIME = {
   139|  '.html': 'text/html; charset=utf-8',
   140|  '.js': 'text/javascript; charset=utf-8',
   141|  '.css': 'text/css; charset=utf-8',
   142|  '.json': 'application/json; charset=utf-8',
   143|  '.webmanifest': 'application/manifest+json; charset=utf-8',
   144|  '.svg': 'image/svg+xml',
   145|  '.png': 'image/png',
   146|  '.ico': 'image/x-icon',
   147|  '.woff2': 'font/woff2',
   148|  '.webp': 'image/webp',
   149|};
   150|
   151|async function serveStatic(req, res, urlPath) {
   152|  let rel = decodeURIComponent(urlPath.split('?')[0]);
   153|  if (rel === '/' || rel === '') rel = '/index.html';
   154|  const filePath = path.join(PUBLIC_DIR, path.normalize(rel));
   155|  if (!filePath.startsWith(PUBLIC_DIR)) { sendJson(res, 403, { error: 'forbidden' }); return; }
   156|  try {
   157|    const data = await fsp.readFile(filePath);
   158|    const ext = path.extname(filePath).toLowerCase();
   159|    const noCache = ['.html', '.webmanifest', '.js', '.css', '.svg'].includes(ext) || filePath.endsWith('sw.js');
   160|    res.writeHead(200, {
   161|      'Content-Type': MIME[ext] || 'application/octet-stream',
   162|      'Cache-Control': noCache ? 'no-cache' : 'public, max-age=3600',
   163|    });
   164|    res.end(data);
   165|  } catch (err) {
   166|    if (err.code === 'ENOENT' || err.code === 'EISDIR') {
   167|      // SPA-Fallback auf index.html
   168|      try {
   169|        const html = await fsp.readFile(path.join(PUBLIC_DIR, 'index.html'));
   170|        res.writeHead(200, { 'Content-Type': MIME['.html'], 'Cache-Control': 'no-cache' });
   171|        res.end(html);
   172|      } catch { sendJson(res, 404, { error: 'not found' }); }
   173|    } else {
   174|      sendJson(res, 500, { error: 'server error' });
   175|    }
   176|  }
   177|}
   178|
   179|// ---------------------------------------------------------------------------
   180|// Auth (optionaler PIN)
   181|// ---------------------------------------------------------------------------
   182|
   183|function pinOk(req) {
   184|  if (!db.config.pinHash) return true;
   185|  const pin = req.headers['x-pin'];
   186|  return pin && sha256(pin) === db.config.pinHash;
   187|}
   188|
   189|// ---------------------------------------------------------------------------
   190|// API
   191|// ---------------------------------------------------------------------------
   192|
   193|async function handleApi(req, res, url) {
   194|  const { pathname } = url;
   195|  const method = req.method;
   196|
   197|  // Öffentlich: Session-Status
   198|  if (pathname === '/api/session' && method === 'GET') {
   199|    return sendJson(res, 200, { pinRequired: !!db.config.pinHash });
   200|  }
   201|  // Öffentlich: PIN prüfen
   202|  if (pathname === '/api/session' && method === 'POST') {
   203|    const body = await readBody(req);
   204|    const ok = !db.config.pinHash || sha256(body.pin || '') === db.config.pinHash;
   205|    return sendJson(res, ok ? 200 : 401, { ok });
   206|  }
   207|
   208|  // Ab hier geschützt (falls PIN gesetzt)
   209|  if (!pinOk(req)) return sendJson(res, 401, { error: 'pin required' });
   210|
   211|  if (pathname === '/api/data' && method === 'GET') {
   212|    return sendJson(res, 200, publicState());
   213|  }
   214|
   215|  if (pathname === '/api/tahajjud' && method === 'POST') {
   216|    const b = await readBody(req);
   217|    if (!DATE_RE.test(b.date || '')) return sendJson(res, 400, { error: 'bad date' });
   218|    if (b.prayed === false) {
   219|      delete db.tahajjud[b.date];
   220|    } else {
   221|      db.tahajjud[b.date] = {
   222|        rakats: clampInt(b.rakats ?? 2, 0, 100),
   223|        time: typeof b.time === 'string' ? b.time.slice(0, 5) : '',
   224|        note: typeof b.note === 'string' ? b.note.slice(0, 500) : '',
   225|      };
   226|    }
   227|    scheduleSave();
   228|    return sendJson(res, 200, publicState());
   229|  }
   230|
   231|  if (pathname === '/api/sunnah' && method === 'POST') {
   232|    const b = await readBody(req);
   233|    if (!DATE_RE.test(b.date || '') || typeof b.key !== 'string') {
   234|      return sendJson(res, 400, { error: 'bad input' });
   235|    }
   236|    const key = b.key.slice(0, 40);
   237|    const set = new Set(db.sunnah[b.date] || []);
   238|    if (b.prayed === false) set.delete(key); else set.add(key);
   239|    if (set.size) db.sunnah[b.date] = [...set]; else delete db.sunnah[b.date];
   240|    scheduleSave();
   241|    return sendJson(res, 200, publicState());
   242|  }
   243|
   244|  if (pathname === '/api/evrad' && method === 'POST') {
   245|    const b = await readBody(req);
   246|    if (!DATE_RE.test(b.date || '')) return sendJson(res, 400, { error: 'bad date' });
   247|    const short = clampInt(b.short ?? 0, 0, 99);
   248|    const long = clampInt(b.long ?? 0, 0, 99);
   249|    if (short || long) db.evrad[b.date] = { short, long };
   250|    else delete db.evrad[b.date];
   251|    scheduleSave();
   252|    return sendJson(res, 200, publicState());
   253|  }
   254|
   255|  if (pathname === '/api/hifz' && method === 'POST') {
   256|    const b = await readBody(req);
   257|    const surah = clampInt(b.surah, 1, 114);
   258|    const memorized = clampInt(b.memorized ?? 0, 0, 286);
   259|    if (memorized > 0) db.hifz[surah] = memorized; else delete db.hifz[surah];
   260|    scheduleSave();
   261|    return sendJson(res, 200, publicState());
   262|  }
   263|
   264|  // PIN setzen/ändern/entfernen (geschützt durch bestehenden PIN)
   265|  if (pathname === '/api/pin' && method === 'POST') {
   266|    const b = await readBody(req);
   267|    const next = (b.pin || '').toString().trim();
   268|    db.config.pinHash = next ? sha256(next) : null;
   269|    scheduleSave();
   270|    return sendJson(res, 200, { ok: true, pinRequired: !!db.config.pinHash });
   271|  }
   272|
   273|  return sendJson(res, 404, { error: 'unknown endpoint' });
   274|}
   275|
   276|// ---------------------------------------------------------------------------
   277|// Server
   278|// ---------------------------------------------------------------------------
   279|
   280|const server = http.createServer(async (req, res) => {
   281|  const url = new URL(req.url, 'http://localhost');
   282|  try {
   283|    if (url.pathname.startsWith('/api/')) {
   284|      await handleApi(req, res, url);
   285|    } else {
   286|      await serveStatic(req, res, req.url);
   287|    }
   288|  } catch (err) {
   289|    console.error('[muslimos] Request-Fehler:', err.message);
   290|    if (!res.headersSent) sendJson(res, 400, { error: err.message });
   291|  }
   292|});
   293|
   294|loadDb();
   295|server.listen(PORT, () => {
   296|  console.log(`[muslimos] läuft auf Port ${PORT}`);
   297|});
   298|
   299|for (const sig of ['SIGINT', 'SIGTERM']) {
   300|  process.on(sig, async () => {
   301|    console.log('[muslimos] beende, speichere DB...');
   302|    if (saveTimer) clearTimeout(saveTimer);
   303|    await persist();
   304|    process.exit(0);
   305|  });
   306|}
   307|


1|<!DOCTYPE html>
     2|
     3|
     4|  <meta charset="utf-8" />
     5|  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
     6|  <meta name="theme-color" content="#f3f8f5" media="(prefers-color-scheme: light)" />
     7|  <meta name="theme-color" content="#0f1613" media="(prefers-color-scheme: dark)" />
     8|  <meta name="apple-mobile-web-app-capable" content="yes" />
     9|  <meta name="mobile-web-app-capable" content="yes" />
    10|  <meta name="apple-mobile-web-app-status-bar-style" content="default" />
    11|  <meta name="apple-mobile-web-app-title" content="EhliSünnet" />
    12|  <title>EhliSünnet</title>
    13|  <link rel="manifest" href="manifest.webmanifest" />
    14|  <link rel="icon" type="image/svg+xml" href="icon.svg?v=4" />
    15|  <link rel="apple-touch-icon" href="icon-512.png?v=4" />
    16|  <link rel="stylesheet" href="styles.css?v=4" />
    17|
    18|
    19|
    20|

    21|

    22|

    23|
EhliSünnet

    24|
PIN eingeben

    25|      <input id="pinInput" class="pin-input" type="password" inputmode="numeric" autocomplete="off" placeholder="••••" />
    26|      <button id="pinBtn" class="btn btn--primary">Aç</button>
    27|

    28|

    29|

    30|
    31|
    32|

    33|
    34|

    35|
    36|

    37|
EhliSünnet

    38|

    39|

    40|

    41|      <button id="settingsBtn" class="icon-btn" aria-label="Ayarlar" title="Ayarlar">
    42|        <svg viewBox="0 0 24 24" width="22" height="22"><path fill="currentColor" d="M12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7Zm7.4-2.5a7.6 7.6 0 0 0 .05-2l2.1-1.65-2-3.46-2.5 1a7.5 7.5 0 0 0-1.7-1l-.38-2.65h-4l-.38 2.65a7.5 7.5 0 0 0-1.7 1l-2.5-1-2 3.46L4.6 11a7.6 7.6 0 0 0 0 2l-2.1 1.65 2 3.46 2.5-1c.52.4 1.1.74 1.7 1l.38 2.65h4l.38-2.65c.6-.26 1.18-.6 1.7-1l2.5 1 2-3.46Z"/></svg>
    43|      </button>
    44|
    45|
    46|
    47|
    48|
    49|      <button class="tab" data-action="tab" data-tab="today">
    50|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M3 13h8V3H3v10Zm0 8h8v-6H3v6Zm10 0h8V11h-8v10Zm0-18v6h8V3h-8Z"/></svg>
    51|        Bugün
    52|      </button>
    53|      <button class="tab" data-action="tab" data-tab="tahajjud">
    54|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36A6 6 0 0 1 12.6 3.1 8.9 8.9 0 0 0 12 3Z"/></svg>
    55|        Teheccüt
    56|      </button>
    57|      <button class="tab" data-action="tab" data-tab="sunnah">
    58|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2 4 6v6c0 5 3.4 8.5 8 10 4.6-1.5 8-5 8-10V6l-8-4Zm-1.2 13.2L7.6 12l1.4-1.4 1.8 1.8 4-4L16.2 9l-5.4 6.2Z"/></svg>
    59|        Sünnet
    60|      </button>
    61|      <button class="tab" data-action="tab" data-tab="evrad">
    62|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M6 2h11a3 3 0 0 1 3 3v15a2 2 0 0 0-2-2H6V2Zm0 18h12v2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2v18Z"/></svg>
    63|        Evrad
    64|      </button>
    65|      <button class="tab" data-action="tab" data-tab="hifz">
    66|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 4 1.7 3.5 3.8.5-2.8 2.7.7 3.8L12 15l-3.4 1.5.7-3.8-2.8-2.7 3.8-.5L12 6Z"/></svg>
    67|        Hıfz
    68|      </button>
    69|
    70|

    71|
    72|
    73|

    74|

    75|
    76|
    77|
    78|
    79|
    80|


     1|/* ===================== EhliSünnet Design System ===================== */
     2|:root {
     3|  --bg: #eaf2ee;
     4|  --bg-2: #ffffff;
     5|  --surface: #ffffff;
     6|  --surface-2: #eef3f0;
     7|  --border: rgba(16, 40, 33, 0.10);
     8|  --border-strong: rgba(16, 40, 33, 0.18);
     9|  --text: #16241f;
    10|  --muted: #5f7a70;
    11|  --emerald: #9b4f96;
    12|  --emerald-soft: rgba(155, 79, 150, 0.12);
    13|  --emerald-deep: #7e3f7a;
    14|  --gold: #b07d24;
    15|  --gold-soft: rgba(176, 125, 36, 0.13);
    16|  --danger: #d2604f;
    17|  --radius: 18px;
    18|  --radius-sm: 12px;
    19|  --shadow: 0 10px 30px rgba(16, 40, 33, 0.08);
    20|  --shadow-sm: 0 2px 10px rgba(16, 40, 33, 0.06);
    21|  --safe-top: env(safe-area-inset-top, 0px);
    22|  --safe-bottom: env(safe-area-inset-bottom, 0px);
    23|  --tabbar-h: 64px;
    24|  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    25|  --arabic: "SF Arabic", "Geeza Pro", "Noto Naskh Arabic", "Amiri", "Scheherazade New", serif;
    26|}
    27|
    28|* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
    29|html, body { margin: 0; height: 100%; }
    30|body {
    31|  font-family: var(--font);
    32|  color: var(--text);
    33|  background:
    34|    radial-gradient(1100px 600px at 100% -10%, rgba(155,79,150,0.12), transparent 55%),
    35|    radial-gradient(900px 500px at -10% 0%, rgba(176,125,36,0.08), transparent 50%),
    36|    linear-gradient(180deg, #f4f9f6, var(--bg));
    37|  background-attachment: fixed;
    38|  min-height: 100dvh;
    39|  overscroll-behavior-y: none;
    40|  -webkit-font-smoothing: antialiased;
    41|}
    42|.hidden { display: none !important; }
    43|.muted { color: var(--muted); }
    44|
    45|/* ---- Logo / Brand mark ---- */
    46|.brand {
    47|  display: inline-block;
    48|  background-image: url("icon.svg?v=4");
    49|  background-size: contain;
    50|  background-repeat: no-repeat;
    51|  background-position: center;
    52|}
    53|.brand--sm { width: 34px; height: 34px; }
    54|.brand--lock { width: 84px; height: 84px; margin: 0 auto 10px; }
    55|
    56|/* ===================== Top bar ===================== */
    57|.topbar {
    58|  position: sticky; top: 0; z-index: 20;
    59|  display: flex; align-items: center; justify-content: space-between;
    60|  padding: calc(var(--safe-top) + 14px) 18px 12px;
    61|  background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.7));
    62|  backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
    63|  border-bottom: 1px solid var(--border);
    64|}
    65|.topbar__brand { display: flex; align-items: center; gap: 11px; }
    66|.topbar__title { font-weight: 700; font-size: 17px; letter-spacing: 0.2px; }
    67|.topbar__sub { font-size: 12px; color: var(--muted); margin-top: 1px; }
    68|.icon-btn {
    69|  display: grid; place-items: center; width: 40px; height: 40px;
    70|  border: 1px solid var(--border); border-radius: 12px;
    71|  background: var(--surface); color: var(--text); cursor: pointer;
    72|  box-shadow: var(--shadow-sm);
    73|}
    74|.icon-btn:active { transform: scale(0.94); }
    75|
    76|/* ===================== View / layout ===================== */
    77|.app { min-height: 100dvh; }
    78|.view {
    79|  padding: 18px 16px calc(var(--tabbar-h) + var(--safe-bottom) + 28px);
    80|  max-width: 680px; margin: 0 auto;
    81|  animation: fade 0.35s ease;
    82|}
    83|@keyframes fade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
    84|
    85|.section-title {
    86|  font-size: 13px; text-transform: uppercase; letter-spacing: 1.4px;
    87|  color: var(--muted); margin: 22px 4px 10px; font-weight: 700;
    88|}
    89|.page-head { margin-bottom: 6px; }
    90|.page-head h2 { margin: 0; font-size: 26px; letter-spacing: -0.3px; }
    91|.page-head p { margin: 4px 0 0; color: var(--muted); font-size: 14px; }
    92|
    93|/* ===================== Cards ===================== */
    94|.card {
    95|  background: var(--surface); border: 1px solid var(--border);
    96|  border-radius: var(--radius); padding: 16px;
    97|  box-shadow: var(--shadow);
    98|}
    99|.card + .card { margin-top: 12px; }
   100|.card-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
   101|
   102|/* Date navigator */
   103|.datenav {
   104|  display: flex; align-items: center; justify-content: space-between;
   105|  gap: 8px; background: var(--surface); border: 1px solid var(--border);
   106|  border-radius: 14px; padding: 6px; margin: 14px 0 6px; box-shadow: var(--shadow-sm);
   107|}
   108|.datenav button {
   109|  width: 40px; height: 40px; border-radius: 10px; border: none;
   110|  background: transparent; color: var(--text); font-size: 20px; cursor: pointer;
   111|}
   112|.datenav button:active { background: var(--surface-2); }
   113|.datenav__label { text-align: center; flex: 1; }
   114|.datenav__label b { display: block; font-size: 15px; }
   115|.datenav__label span { font-size: 12px; color: var(--muted); }
   116|.datenav__today {
   117|  font-size: 11px !important; width: auto !important; padding: 0 10px;
   118|  color: var(--emerald) !important; font-weight: 700;
   119|}
   120|
   121|/* ===================== Buttons ===================== */
   122|.btn {
   123|  appearance: none; border: 1px solid var(--border-strong); cursor: pointer;
   124|  background: var(--surface); color: var(--text);
   125|  border-radius: 14px; padding: 13px 18px; font-size: 15px; font-weight: 600;
   126|  font-family: var(--font); transition: transform 0.08s ease;
   127|}
   128|.btn:active { transform: scale(0.97); }
   129|.btn--primary {
   130|  background: linear-gradient(180deg, var(--emerald), var(--emerald-deep));
   131|  border-color: transparent; color: #ffffff;
   132|  box-shadow: 0 8px 20px rgba(155,79,150,0.32);
   133|}
   134|.btn--ghost { background: transparent; border-color: var(--border); }
   135|.btn--danger { color: var(--danger); border-color: rgba(210,96,79,0.4); background: rgba(210,96,79,0.07); }
   136|.btn--block { width: 100%; }
   137|.btn--lg { padding: 16px; font-size: 16px; }
   138|
   139|/* Big toggle (Teheccüt) */
   140|.bigtoggle {
   141|  display: flex; flex-direction: column; align-items: center; gap: 14px;
   142|  padding: 26px; text-align: center;
   143|}
   144|.bigtoggle__ring {
   145|  width: 150px; height: 150px; border-radius: 50%; display: grid; place-items: center;
   146|  background: radial-gradient(circle at 50% 35%, rgba(155,79,150,0.08), transparent 70%);
   147|  border: 2px solid var(--border-strong); position: relative; cursor: pointer;
   148|  transition: all 0.25s ease;
   149|}
   150|.bigtoggle__ring.on {
   151|  border-color: var(--emerald);
   152|  background: radial-gradient(circle at 50% 35%, var(--emerald-soft), transparent 72%);
   153|  box-shadow: 0 0 0 6px rgba(155,79,150,0.08), 0 14px 36px rgba(155,79,150,0.22);
   154|}
   155|.bigtoggle__ring svg { width: 58px; height: 58px; color: #c2cdc8; transition: color 0.25s; }
   156|.bigtoggle__ring.on svg { color: var(--emerald); }
   157|.bigtoggle__status { font-size: 18px; font-weight: 700; }
   158|.bigtoggle__status.on { color: var(--emerald); }
   159|
   160|/* ===================== Checklist items ===================== */
   161|.check {
   162|  display: flex; align-items: center; gap: 13px; padding: 13px 14px;
   163|  border: 1px solid var(--border); border-radius: 14px; background: var(--surface);
   164|  cursor: pointer; margin-bottom: 9px; transition: border-color 0.15s, background 0.15s;
   165|  box-shadow: var(--shadow-sm);
   166|}
   167|.check.on { border-color: var(--emerald); background: var(--emerald-soft); }
   168|.check__box {
   169|  flex: 0 0 26px; width: 26px; height: 26px; border-radius: 8px;
   170|  border: 2px solid var(--border-strong); display: grid; place-items: center;
   171|  transition: all 0.15s; background: var(--surface);
   172|}
   173|.check.on .check__box { background: var(--emerald); border-color: var(--emerald); }
   174|.check__box--radio { border-radius: 50%; }
   175|.check--radio { margin-bottom: 0; }
   176|.check--radio + .check--radio { margin-top: 9px; }
   177|.check__box svg { width: 16px; height: 16px; color: #fff; opacity: 0; transition: opacity 0.15s; }
   178|.check.on .check__box svg { opacity: 1; }
   179|.check__body { flex: 1; min-width: 0; }
   180|.check__title { font-weight: 600; font-size: 15px; }
   181|.check__sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
   182|.check__badge {
   183|  font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 20px;
   184|  background: var(--gold-soft); color: var(--gold);
   185|}
   186|
   187|/* ===================== Stepper (Evrad) ===================== */
   188|.stepper-card { text-align: center; padding: 20px 16px; }
   189|.stepper-card h3 { margin: 0 0 2px; font-size: 17px; }
   190|.stepper-card .ar { font-family: var(--arabic); font-size: 22px; color: var(--gold); margin-bottom: 14px; }
   191|.stepper { display: flex; align-items: center; justify-content: center; gap: 18px; }
   192|.stepper button {
   193|  width: 54px; height: 54px; border-radius: 16px; font-size: 26px; font-weight: 300;
   194|  border: 1px solid var(--border-strong); background: var(--surface-2); color: var(--text); cursor: pointer;
   195|}
   196|.stepper button:active { transform: scale(0.92); }
   197|.stepper button.plus { background: linear-gradient(180deg, var(--emerald), var(--emerald-deep)); color: #fff; border-color: transparent; box-shadow: 0 6px 16px rgba(155,79,150,0.30); }
   198|.stepper__val { min-width: 64px; }
   199|.stepper__val b { font-size: 40px; font-weight: 700; line-height: 1; }
   200|.stepper__val span { display: block; font-size: 12px; color: var(--muted); margin-top: 3px; }
   201|
   202|/* ===================== Stat / metrics ===================== */
   203|.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
   204|.stat {
   205|  background: var(--surface); border: 1px solid var(--border);
   206|  border-radius: var(--radius); padding: 15px; box-shadow: var(--shadow-sm);
   207|}
   208|.stat__val { font-size: 28px; font-weight: 800; letter-spacing: -0.5px; }
   209|.stat__val small { font-size: 14px; font-weight: 600; color: var(--muted); }
   210|.stat__label { font-size: 12.5px; color: var(--muted); margin-top: 3px; }
   211|.stat__icon { font-size: 18px; }
   212|.flame { color: var(--gold); }
   213|
   214|/* Progress bar */
   215|.bar { height: 8px; border-radius: 8px; background: var(--surface-2); overflow: hidden; }
   216|.bar > i { display: block; height: 100%; border-radius: 8px;
   217|  background: linear-gradient(90deg, var(--emerald), var(--gold)); transition: width 0.4s ease; }
   218|
   219|/* ===================== Hifz ===================== */
   220|.hifz-hero { text-align: center; padding: 22px 16px; }
   221|.ring-wrap { position: relative; width: 168px; height: 168px; margin: 0 auto 6px; }
   222|.ring-wrap svg { transform: rotate(-90deg); }
   223|.ring-wrap .ring-center {
   224|  position: absolute; inset: 0; display: grid; place-content: center; text-align: center;
   225|}
   226|.ring-center b { font-size: 38px; font-weight: 800; }
   227|.ring-center span { font-size: 12px; color: var(--muted); }
   228|.hifz-mini { display: flex; justify-content: center; gap: 26px; margin-top: 10px; }
   229|.hifz-mini div { text-align: center; }
   230|.hifz-mini b { font-size: 20px; font-weight: 800; }
   231|.hifz-mini span { font-size: 11.5px; color: var(--muted); }
   232|
   233|.surah-search {
   234|  width: 100%; padding: 13px 14px; border-radius: 14px; font-size: 15px;
   235|  background: var(--surface); border: 1px solid var(--border); color: var(--text);
   236|  margin: 4px 0 12px; font-family: var(--font); box-shadow: var(--shadow-sm);
   237|}
   238|.surah-search::placeholder { color: var(--muted); }
   239|.surah {
   240|  display: flex; align-items: center; gap: 12px; padding: 12px 14px;
   241|  border: 1px solid var(--border); border-radius: 14px; background: var(--surface);
   242|  margin-bottom: 8px; cursor: pointer; box-shadow: var(--shadow-sm);
   243|}
   244|.surah.done { border-color: rgba(155,79,150,0.45); }
   245|.surah__no {
   246|  flex: 0 0 38px; width: 38px; height: 38px; display: grid; place-items: center;
   247|  border-radius: 10px; background: var(--surface-2); font-weight: 700; font-size: 13px;
   248|  position: relative;
   249|}
   250|.surah.done .surah__no { background: var(--emerald-soft); color: var(--emerald); }
   251|.surah__body { flex: 1; min-width: 0; }
   252|.surah__name { font-weight: 600; display: flex; align-items: center; gap: 8px; }
   253|.surah__name .ar { font-family: var(--arabic); color: var(--gold); font-size: 16px; font-weight: 400; }
   254|.surah__meta { font-size: 12px; color: var(--muted); margin-top: 3px; }
   255|.surah__pct { font-weight: 800; font-size: 14px; min-width: 42px; text-align: right; color: var(--emerald); }
   256|.surah .bar { margin-top: 7px; }
   257|
   258|/* ===================== Modal ===================== */
   259|.modal-backdrop {
   260|  position: fixed; inset: 0; z-index: 50; background: rgba(16,40,33,0.40);
   261|  backdrop-filter: blur(3px); display: flex; align-items: flex-end; justify-content: center;
   262|  animation: fade 0.2s ease;
   263|}
   264|.modal {
   265|  width: 100%; max-width: 520px; background: var(--bg-2);
   266|  border: 1px solid var(--border); border-bottom: none;
   267|  border-radius: 22px 22px 0 0; padding: 8px 18px calc(22px + var(--safe-bottom));
   268|  box-shadow: 0 -20px 60px rgba(16,40,33,0.18); animation: slideup 0.28s cubic-bezier(.2,.8,.2,1);
   269|}
   270|@keyframes slideup { from { transform: translateY(100%); } to { transform: none; } }
   271|.modal__grip { width: 40px; height: 4px; border-radius: 4px; background: var(--border-strong); margin: 6px auto 14px; }
   272|.modal h3 { margin: 0 0 4px; font-size: 20px; }
   273|.modal .ar { font-family: var(--arabic); color: var(--gold); font-size: 20px; }
   274|.field { margin: 14px 0; }
   275|.field label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 7px; }
   276|.field input, .field textarea {
   277|  width: 100%; padding: 13px 14px; border-radius: 12px; font-size: 16px;
   278|  background: var(--surface); border: 1px solid var(--border-strong); color: var(--text);
   279|  font-family: var(--font);
   280|}
   281|.field textarea { min-height: 70px; resize: vertical; }
   282|.range-row { display: flex; align-items: center; gap: 14px; }
   283|.range-row input[type=range] { flex: 1; accent-color: var(--emerald); height: 30px; }
   284|.range-val { font-size: 22px; font-weight: 800; min-width: 96px; text-align: right; }
   285|.range-val small { font-size: 13px; color: var(--muted); font-weight: 600; }
   286|.chips { display: flex; flex-wrap: wrap; gap: 8px; }
   287|.chip {
   288|  padding: 9px 14px; border-radius: 20px; border: 1px solid var(--border-strong);
   289|  background: var(--surface); color: var(--text); font-size: 14px; font-weight: 600; cursor: pointer;
   290|}
   291|.chip.on { background: var(--emerald-soft); border-color: var(--emerald); color: var(--emerald); }
   292|.modal__actions { display: flex; gap: 10px; margin-top: 18px; }
   293|.modal__actions .btn { flex: 1; }
   294|.modal code { background: var(--surface-2); padding: 2px 6px; border-radius: 6px; font-size: 12px; }
   295|
   296|/* ===================== PIN lock ===================== */
   297|.lock { position: fixed; inset: 0; z-index: 80; display: grid; place-items: center;
   298|  background: linear-gradient(180deg, #f4f9f6, var(--bg)); padding: 24px; }
   299|.lock-card { text-align: center; max-width: 320px; width: 100%; }
   300|.lock-card h1 { margin: 0; font-size: 24px; letter-spacing: 0.5px; }
   301|.pin-input {
   302|  width: 100%; text-align: center; letter-spacing: 14px; font-size: 28px;
   303|  padding: 14px; margin: 18px 0 14px; border-radius: 14px;
   304|  background: var(--surface); border: 1px solid var(--border-strong); color: var(--text);
   305|}
   306|.pin-error { color: var(--danger); font-size: 13px; min-height: 18px; }
   307|
   308|/* ===================== Tabbar ===================== */
   309|.tabbar {
   310|  position: fixed; bottom: 0; left: 0; right: 0; z-index: 30;
   311|  height: calc(var(--tabbar-h) + var(--safe-bottom));
   312|  padding-bottom: var(--safe-bottom);
   313|  display: flex; align-items: stretch;
   314|  background: linear-gradient(180deg, rgba(255,255,255,0.85), rgba(255,255,255,0.98));
   315|  backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);
   316|  border-top: 1px solid var(--border);
   317|}
   318|.tab {
   319|  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
   320|  gap: 3px; border: none; background: none; color: var(--muted); cursor: pointer;
   321|  font-family: var(--font); font-size: 10.5px; font-weight: 600; padding-top: 6px;
   322|  transition: color 0.15s;
   323|}
   324|.tab svg { width: 24px; height: 24px; transition: transform 0.15s; }
   325|.tab.active { color: var(--emerald); }
   326|.tab.active svg { transform: translateY(-1px) scale(1.06); }
   327|
   328|/* ===================== Toast ===================== */
   329|.toast {
   330|  position: fixed; left: 50%; transform: translateX(-50%) translateY(20px);
   331|  bottom: calc(var(--tabbar-h) + var(--safe-bottom) + 16px); z-index: 60;
   332|  background: #16241f; border: 1px solid rgba(255,255,255,0.08);
   333|  color: #eaf5f0; padding: 11px 18px; border-radius: 30px; font-size: 14px; font-weight: 600;
   334|  opacity: 0; pointer-events: none; transition: all 0.3s ease; box-shadow: var(--shadow);
   335|}
   336|.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
   337|.toast--action { display: flex; align-items: center; gap: 14px; padding: 9px 9px 9px 18px; }
   338|.toast--action.show { pointer-events: auto; }
   339|.toast__btn {
   340|  appearance: none; border: none; background: rgba(255,255,255,0.12); cursor: pointer;
   341|  color: var(--gold); font-weight: 800; font-size: 14px; font-family: var(--font);
   342|  padding: 7px 13px; border-radius: 22px;
   343|}
   344|.toast__btn:active { transform: scale(0.95); }
   345|
   346|/* Misc */
   347|.empty { text-align: center; color: var(--muted); padding: 30px 10px; font-size: 14px; }
   348|.spacer { height: 8px; }
   349|.divider { height: 1px; background: var(--border); margin: 16px 0; }
   350|@media (min-width: 560px) { .page-head h2 { font-size: 30px; } }
   351|
   352|/* ===================== Dark Mode (Teheccüt = Nachtnutzung) ===================== */
   353|@media (prefers-color-scheme: dark) {
   354|  :root {
   355|    --bg: #0f1613;
   356|    --bg-2: #161f1b;
   357|    --surface: #18221e;
   358|    --surface-2: #212d28;
   359|    --border: rgba(255, 255, 255, 0.09);
   360|    --border-strong: rgba(255, 255, 255, 0.18);
   361|    --text: #e8f0ec;
   362|    --muted: #95aaa0;
   363|    --emerald: #c77fc1;
   364|    --emerald-soft: rgba(199, 127, 193, 0.18);
   365|    --emerald-deep: #a85fa2;
   366|    --gold: #d7ab5b;
   367|    --gold-soft: rgba(215, 171, 91, 0.16);
   368|    --danger: #e0786a;
   369|    --shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
   370|    --shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.35);
   371|  }
   372|  body {
   373|    background:
   374|      radial-gradient(1100px 600px at 100% -10%, rgba(199, 127, 193, 0.14), transparent 55%),
   375|      radial-gradient(900px 500px at -10% 0%, rgba(215, 171, 91, 0.08), transparent 50%),
   376|      linear-gradient(180deg, #121a16, var(--bg));
   377|  }
   378|  .topbar { background: linear-gradient(180deg, rgba(22, 31, 27, 0.92), rgba(22, 31, 27, 0.66)); }
   379|  .tabbar { background: linear-gradient(180deg, rgba(18, 26, 22, 0.85), rgba(18, 26, 22, 0.98)); }
   380|  .lock { background: linear-gradient(180deg, #121a16, var(--bg)); }
   381|  .bigtoggle__ring svg { color: #5f7068; }
   382|  .toast { background: #243029; color: #eaf5f0; border: 1px solid rgba(255, 255, 255, 0.10); }
   383|  .btn--primary { box-shadow: 0 8px 20px rgba(199, 127, 193, 0.30); }
   384|}
   385|


Jetzt lese ich noch `app.js`, um die Frontend-Mechanik komplett zu verstehen.

Explanation:
Ich habe jetzt die gesamte MuslimOS-App verstanden:

1. **Server:** Reiner Node.js HTTP-Server, keine externen Abhängigkeiten. JSON-Datei als DB (`db.json`). API-Endpoints: `/api/data`, `/api/tahajjud`, `/api/sunnah`, `/api/evrad`, `/api/hifz`, `/api/pin`, `/api/session`.

2. **Frontend:** Single-Page-App mit Tab-Navigation (Bugün, Teheccüt, Sünnet, Evrad, Hıfz). Design-System: Emerald/Gold-Farbschema, Cards, Checklist-Items, Dark Mode.

3. **Datenstruktur:** `db.json` mit `tahajjud`, `sunnah`, `evrad`, `hifz` Sektionen.

Jetzt muss ich:
- Eine neue Unterseite "Gebetszeiten" (Prayer Times) hinzufügen
- Die Aladhan-API für Wien (Diyanet + OneSeventh) einbinden
- Checkboxen für jedes Hauptgebet (Fajr, Dhuhr, Asr, Maghrib, Isha) zum Abhaken
- Die Sünnet-Gebete sind bereits separat in der Sünnet-Seite

Ich muss:
1. `server.js` erweitern: Neuen API-Endpoint `/api/prayer-times` (GET) und `/api/prayers` (POST zum Abhaken)
2. `db.json`-Schema erweitern um `prayers` Sektion
3. `index.html` erweitern: Neuen Tab "Namaz" oder "Vakit" in der Tabbar
4. `app.js` erweitern: Neue View für Gebetszeiten
5. `styles.css` ggf. erweitern (bestehende Styles sollten reichen)

Das ist eine Coding-Aufgabe → muss an Kimi k2.7-code delegiert werden.

Ich muss die App-Dateien lesen, die ich noch nicht gelesen habe: `app.js` und `quran-data.js`.

Dann schreibe ich eine klare Spec und delegiere an Kimi.

Lass mich zuerst `app.js` lesen, um die Mechanik zu verstehen.


1|'use strict';
     2|/* ===================== EhliSünnet App ===================== */
     3|
     4|// ---------- Konfiguration (statisch) ----------
     5|const SUNNAH = [
     6|  { group: "Müekked Sünnetler", items: [
     7|    { key: "sabah",     name: "Sabah sünneti",      sub: "Farzdan önce",  rakat: "2 rekât" },
     8|    { key: "ogle_once", name: "Öğle ilk sünneti",   sub: "Farzdan önce",  rakat: "4 rekât" },
     9|    { key: "ogle_son",  name: "Öğle son sünneti",   sub: "Farzdan sonra", rakat: "2 rekât" },
    10|    { key: "aksam",     name: "Akşam sünneti",      sub: "Farzdan sonra", rakat: "2 rekât" },
    11|    { key: "yatsi_son", name: "Yatsı son sünneti",  sub: "Farzdan sonra", rakat: "2 rekât" },
    12|  ]},
    13|  { group: "Gayr-i Müekked", items: [
    14|    { key: "ikindi",     name: "İkindi sünneti",    sub: "Farzdan önce", rakat: "4 rekât" },
    15|    { key: "yatsi_once", name: "Yatsı ilk sünneti", sub: "Farzdan önce", rakat: "4 rekât" },
    16|  ]},
    17|  { group: "Nafile Namazlar", items: [
    18|    { key: "kusluk",   name: "Kuşluk (Duhâ)",       sub: "Güneş yükselince", rakat: "2–8 rekât" },
    19|    { key: "evvabin",  name: "Evvâbîn",             sub: "Akşamdan sonra",   rakat: "6 rekât" },
    20|    { key: "israk",    name: "İşrak",               sub: "Güneş doğunca",    rakat: "2 rekât" },
    21|    { key: "tahiyyat", name: "Tahiyyetü'l-mescid",  sub: "Camiye girince",   rakat: "2 rekât" },
    22|  ]},
    23|];
    24|const MUEKKED_KEYS = SUNNAH[0].items.map(i => i.key);
    25|const GAYRI_KEYS = SUNNAH[1].items.map(i => i.key);
    26|const SUNNAH_STAT_KEYS = [...MUEKKED_KEYS, ...GAYRI_KEYS]; // Müekked + Gayr-i Müekked (Nafile zählt nicht)
    27|const ALL_SUNNAH = SUNNAH.flatMap(g => g.items);
    28|
    29|const GUN = ["Pazar","Pazartesi","Salı","Çarşamba","Perşembe","Cuma","Cumartesi"];
    30|const AY  = ["Ocak","Şubat","Mart","Nisan","Mayıs","Haziran","Temmuz","Ağustos","Eylül","Ekim","Kasım","Aralık"];
    31|
    32|// ---------- PIN-Speicher (mit Migration vom alten Key) ----------
    33|const PIN_KEY = "ehlisunnet_pin";
    34|(function migratePinKey(){
    35|  const old = localStorage.getItem("muslimos_pin");
    36|  if (old !== null && localStorage.getItem(PIN_KEY) === null) {
    37|    localStorage.setItem(PIN_KEY, old);
    38|    localStorage.removeItem("muslimos_pin");
    39|  }
    40|})();
    41|
    42|// ---------- State ----------
    43|const state = {
    44|  data: { tahajjud:{}, sunnah:{}, evrad:{}, hifz:{}, config:{} },
    45|  tab: "today",
    46|  date: todayStr(),
    47|  pin: localStorage.getItem(PIN_KEY) || "",
    48|};
    49|
    50|// ---------- Date-Helfer ----------
    51|function todayStr(){ const d=new Date(); return ymd(d); }
    52|function ymd(d){ return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; }
    53|function parseYmd(s){ const [y,m,d]=s.split("-").map(Number); return new Date(y, m-1, d); }
    54|function addDays(s,n){ const d=parseYmd(s); d.setDate(d.getDate()+n); return ymd(d); }
    55|function fmtLong(s){ const d=parseYmd(s); return `${GUN[d.getDay()]}, ${d.getDate()} ${AY[d.getMonth()]} ${d.getFullYear()}`; }
    56|function fmtShort(s){ const d=parseYmd(s); return `${d.getDate()} ${AY[d.getMonth()]}`; }
    57|function hijri(s){
    58|  try { return new Intl.DateTimeFormat("tr-TR-u-ca-islamic-umalqura",{day:"numeric",month:"long",year:"numeric"}).format(parseYmd(s)) + " H"; }
    59|  catch { return ""; }
    60|}
    61|
    62|// ---------- API ----------
    63|async function api(path, method="GET", body){
    64|  const opt = { method, headers: {} };
    65|  if (state.pin) opt.headers["x-pin"] = state.pin;
    66|  if (body){ opt.headers["content-type"]="application/json"; opt.body=JSON.stringify(body); }
    67|  const res = await fetch(path, opt);
    68|  if (res.status === 401){ showLock(); throw new Error("unauthorized"); }
    69|  if (!res.ok) throw new Error("api "+res.status);
    70|  return res.json();
    71|}
    72|// Optimistic Update: wendet die Änderung sofort lokal an + rendert,
    73|// schickt den Request im Hintergrund und rollt bei Fehler zurück.
    74|let mutSeq = 0;
    75|async function mutate(path, body, localMutator){
    76|  const prev = structuredClone(state.data);
    77|  if (localMutator) { localMutator(state.data); render(); }
    78|  const seq = ++mutSeq;
    79|  try {
    80|    const fresh = await api(path, "POST", body);
    81|    if (seq === mutSeq) state.data = fresh; // nur die zuletzt abgesendete Mutation übernimmt den Server-Stand
    82|  } catch(e){
    83|    if (e.message !== "unauthorized"){ state.data = prev; render(); toast("Bağlantı hatası"); }
    84|  }
    85|}
    86|
    87|// ---------- Toast ----------
    88|let toastT;
    89|function toast(msg){
    90|  const t=document.getElementById("toast");
    91|  t.classList.remove("toast--action");
    92|  t.innerHTML = esc(msg);
    93|  t.classList.add("show");
    94|  clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove("show"),1800);
    95|}
    96|// Undo-Toast: zeigt 5 s lang eine "Geri al"-Aktion
    97|function toastUndo(msg, onUndo){
    98|  const t=document.getElementById("toast");
    99|  t.classList.add("toast--action");
   100|  t.innerHTML = `<span>${esc(msg)}</span><button class="toast__btn" type="button">Geri al</button>`;
   101|  t.classList.add("show");
   102|  clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove("show"),5000);
   103|  t.querySelector(".toast__btn").addEventListener("click", ()=>{
   104|    clearTimeout(toastT); t.classList.remove("show"); onUndo();
   105|  });
   106|}
   107|
   108|// ================== RENDER ==================
   109|function render(){
   110|  document.querySelectorAll(".tab").forEach(b=>b.classList.toggle("active", b.dataset.tab===state.tab));
   111|  const v=document.getElementById("view");
   112|  v.style.animation="none"; void v.offsetWidth; v.style.animation="";
   113|  ({ today:renderToday, tahajjud:renderTahajjud, sunnah:renderSunnah, evrad:renderEvrad, hifz:renderHifz }[state.tab])();
   114|  document.getElementById("hijriDate").textContent = hijri(todayStr());
   115|}
   116|
   117|function dateNav(){
   118|  const isToday = state.date===todayStr();
   119|  return `

   120|    <button data-action="date-prev" aria-label="Önceki">‹</button>
   121|
${isToday?"Bugün":fmtShort(state.date)}${fmtLong(state.date)}

   122|    ${isToday ? `<button data-action="date-next" aria-label="Sonraki">›</button>`
   123|              : `<button class="datenav__today" data-action="date-today">Bugün</button>`}
   124|
`;
   125|}
   126|
   127|// ---------- Streak / Stats ----------
   128|function tahajjudStreak(){
   129|  let n=0, d=todayStr();
   130|  // Heute zählt nur, wenn erledigt; sonst ab gestern
   131|  if(!state.data.tahajjud[d]) d=addDays(d,-1);
   132|  while(state.data.tahajjud[d]){ n++; d=addDays(d,-1); }
   133|  return n;
   134|}
   135|function countInRange(map, days){
   136|  let n=0, d=todayStr();
   137|  for(let i=0;i<days;i++){ if(map[d]) n++; d=addDays(d,-1); }
   138|  return n;
   139|}
   140|function evradWeek(){
   141|  let s=0,l=0,d=todayStr();
   142|  for(let i=0;i<7;i++){ const e=state.data.evrad[d]; if(e){ s+=e.short||0; l+=e.long||0; } d=addDays(d,-1); }
   143|  return {s,l};
   144|}
   145|function hifzStats(){
   146|  const total=window.QURAN_TOTAL_AYAH;
   147|  let mem=0, doneSurah=0;
   148|  for(const su of window.SURAHS){
   149|    const m=Math.min(state.data.hifz[su[0]]||0, su[3]);
   150|    mem+=m; if(m>=su[3]) doneSurah++;
   151|  }
   152|  return { mem, total, pct: total?(mem/total*100):0, doneSurah };
   153|}
   154|
   155|// ---------- Lifetime-Statistik (ab erstem Eintrag) ----------
   156|function firstEntryDate(){
   157|  let min=null;
   158|  const consider=(d)=>{ if(/^\d{4}-\d{2}-\d{2}$/.test(d) && (!min || d<min)) min=d; };
   159|  for(const d in state.data.tahajjud) consider(d);
   160|  for(const d in state.data.sunnah) consider(d);
   161|  for(const d in state.data.evrad) consider(d);
   162|  return min;
   163|}
   164|function lifetimeStats(){
   165|  const start=firstEntryDate();
   166|  if(!start) return { start:null, days:0, tahajjud:0, evradFull:0, evradQuarter:0, sunnahAvg:0 };
   167|  const days=Math.floor((parseYmd(todayStr())-parseYmd(start))/86400000)+1;
   168|  const tahDays=Object.keys(state.data.tahajjud).length;
   169|  let evFull=0, evQuarter=0;
   170|  for(const d in state.data.evrad){ const e=state.data.evrad[d]; if(e.long>0) evFull++; if(e.short>0) evQuarter++; }
   171|  let prayedSum=0;
   172|  for(const d in state.data.sunnah){
   173|    const set=new Set(state.data.sunnah[d]);
   174|    for(const k of SUNNAH_STAT_KEYS) if(set.has(k)) prayedSum++;
   175|  }
   176|  const perDay=SUNNAH_STAT_KEYS.length;
   177|  return {
   178|    start, days,
   179|    tahajjud: days? tahDays/days*100 : 0,
   180|    evradFull: days? evFull/days*100 : 0,
   181|    evradQuarter: days? evQuarter/days*100 : 0,
   182|    sunnahAvg: (days&&perDay)? prayedSum/(perDay*days)*100 : 0,
   183|  };
   184|}
   185|
   186|// ---------- Bugün (Dashboard) ----------
   187|function renderToday(){
   188|  const t=todayStr();
   189|  const tah=state.data.tahajjud[t];
   190|  const doneM=MUEKKED_KEYS.filter(k=>(state.data.sunnah[t]||[]).includes(k)).length;
   191|  const ev=state.data.evrad[t]||{short:0,long:0};
   192|  const hf=hifzStats();
   193|  const streak=tahajjudStreak();
   194|  const ls=lifetimeStats();
   195|  const hour=new Date().getHours();
   196|  const selam = hour<6?"Hayırlı seherler":hour<12?"Hayırlı sabahlar":hour<18?"Hayırlı günler":"Hayırlı akşamlar";
   197|
   198|  document.getElementById("view").innerHTML = `
   199|

   200|
${selam} 🌙

   201|
${fmtLong(t)} · ${hijri(t)}

   202|

   203|
   204|
Bugünün ibadetleri

   205|
   206|

   207|

   208|
Teheccüt

   209|
${tah ? `Kılındı · ${tah.rakats} rekât${tah.time?` · ${tah.time}`:""}` : "Henüz kılınmadı"}

   210|

   211|

   212|

   213|
   214|

   215|

   216|
Müekked sünnetler

   217|
${doneM}/${MUEKKED_KEYS.length}

   218|

   219|

   220|

   221|
   222|

   223|

   224|
Evrad

   225|
Bugün: ${ev.long?"Tüm okundu":ev.short?"Çeyrek okundu":"henüz seçilmedi"}

   226|

   227|
${ev.long?"Tüm":ev.short?"Çeyrek":"—"}

   228|

   229|
   230|

   231|

   232|
Hıfz ilerlemesi

   233|
${hf.mem.toLocaleString("tr-TR")} / ${hf.total.toLocaleString("tr-TR")} ayet · ${hf.doneSurah} sûre tam

   234|

   235|
${hf.pct.toFixed(1)}%

   236|

   237|
   238|

   239|

   240|
🔥 ${streak}

   241|
Teheccüt serisi (gün)

   242|

   243|

   244|
${hf.pct.toFixed(1)}%

   245|
Kur'an hıfz

   246|

   247|

   248|
   249|
Bu hafta

   250|

   251|

${countInRange(state.data.tahajjud,7)}/7

Teheccüt günü

   252|

${evradWeek().l+evradWeek().s}

Evrad okuması

   253|

   254|
   255|
İlk kayıttan beri${ls.start?` · ${fmtShort(ls.start)} ${parseYmd(ls.start).getFullYear()}`:""}

   256|

   257|

${ls.days?Math.round(ls.tahajjud):"–"}%

Teheccüt · kılınan gün

   258|

${ls.days?Math.round(ls.sunnahAvg):"–"}%

Sünnet · günlük ortalama

   259|

${ls.days?Math.round(ls.evradFull):"–"}%

Evrad · tüm okunan gün

   260|

${ls.days?Math.round(ls.evradQuarter):"–"}%

Evrad · çeyrek okunan gün

   261|

   262|  `;
   263|}
   264|
   265|// ---------- Teheccüt ----------
   266|function renderTahajjud(){
   267|  const d=state.date, tah=state.data.tahajjud[d];
   268|  document.getElementById("view").innerHTML = `
   269|

Teheccüt

Gece namazını kaydet ve serini koru

   270|    ${dateNav()}
   271|

   272|

   273|        <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36A6 6 0 0 1 12.6 3.1 8.9 8.9 0 0 0 12 3Z"/></svg>
   274|

   275|
${tah?"Elhamdülillah, kılındı ✓":"Henüz kaydedilmedi"}

   276|      ${tah?`<div class="muted" style="font-size:14px">${tah.rakats} rekât${tah.time?` · ${tah.time}`:""}${tah.note?` · ${esc(tah.note)}`:""}

   277|        <button class="btn btn--ghost" data-action="tahajjud-edit">Detayları düzenle</button>`:""}
   278|

   279|
   280|
İstatistik

   281|

   282|

🔥 ${tahajjudStreak()}

Güncel seri

   283|

${countInRange(state.data.tahajjud,30)}/30

Son 30 gün

   284|

   285|
   286|
Son 5 hafta

   287|
${heatmap()}

   288|  `;
   289|}
   290|function heatmap(){
   291|  // 35 Tage, älteste links
   292|  let cells="";
   293|  const start=addDays(todayStr(),-34);
   294|  for(let i=0;i<35;i++){
   295|    const d=addDays(start,i);
   296|    const on=!!state.data.tahajjud[d];
   297|    const isToday=d===todayStr();
   298|    cells+=`

`;
   299|  }
   300|  return `
${cells}

   301|
Yeşil = teheccüt kılındı · Altın çerçeve = bugün
`;
   302|}
   303|
   304|// ---------- Sünnet ----------
   305|function renderSunnah(){
   306|  const d=state.date, done=new Set(state.data.sunnah[d]||[]);
   307|  const doneM=MUEKKED_KEYS.filter(k=>done.has(k)).length;
   308|  let groups="";
   309|  for(const g of SUNNAH){
   310|    groups+=`<div class="section-title">${g.group}</div>`;
   311|    for(const it of g.items){
   312|      const on=done.has(it.key);
   313|      groups+=`

   314|
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"/></svg>

   315|

${it.name}

${it.sub} · ${it.rakat}

   316|        ${it.badge?`<span class="check__badge">${it.badge}</span>`:""}
   317|
`;
   318|    }
   319|  }
   320|  document.getElementById("view").innerHTML = `
   321|

Sünnet & Nafile

Günlük sünnet namazlarını işaretle

   322|    ${dateNav()}
   323|

   324|

Müekked sünnetler

${doneM}/${MUEKKED_KEYS.length}

   325|

   326|

   327|    ${groups}
   328|  `;
   329|}
   330|
   331|// ---------- Evrad ----------
   332|function renderEvrad(){
   333|  const d=state.date, ev=state.data.evrad[d]||{short:0,long:0};
   334|  const sel = ev.long>0 ? "long" : ev.short>0 ? "short" : null;
   335|  const wk=evradWeek();
   336|  document.getElementById("view").innerHTML = `
   337|

Evrad

Bugün hangi evrâdı okudun? Birini seç.

   338|    ${dateNav()}
   339|

   340|      ${evradOption("long","Tüm","Tam okunuş", sel==="long")}
   341|      ${evradOption("short","Çeyrek","Çeyrek okunuş", sel==="short")}
   342|

   343|    ${sel?'
Seçeneğe tekrar dokunarak kaldırabilirsin.
':''}
   344|
Bu hafta

   345|

   346|

${wk.l}

Tüm okunan gün

   347|

${wk.s}

Çeyrek okunan gün

   348|

   349|  `;
   350|}
   351|function evradOption(key,title,sub,on){
   352|  return `

   353|
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"/></svg>

   354|

${title} الأوراد

${sub}

   355|
`;
   356|}
   357|
   358|// ---------- Hıfz ----------
   359|function renderHifz(){
   360|  const hf=hifzStats();
   361|  const R=76, C=2*Math.PI*R, off=C*(1-hf.pct/100);
   362|  const q=document.getElementById("hifzSearch");
   363|  const term=(q?q.value:"").toLowerCase().trim();
   364|  let list="";
   365|  for(const su of window.SURAHS){
   366|    const [no,name,ar,total]=su;
   367|    if(term && !(`${no} ${name} ${ar}`.toLowerCase().includes(term))) continue;
   368|    const mem=Math.min(state.data.hifz[no]||0,total);
   369|    const pct=mem/total*100;
   370|    const done=mem>=total;
   371|    list+=`

   372|
${no}

   373|

   374|
${name} ${ar}

   375|
${mem} / ${total} ayet

   376|

   377|

   378|
${done?'✓':pct.toFixed(0)+'%'}

   379|
`;
   380|  }
   381|  document.getElementById("view").innerHTML = `
   382|

Hıfz

Ezberini ayet ayet takip et

   383|

   384|

   385|        <svg width="168" height="168" viewBox="0 0 168 168">
   386|          <circle cx="84" cy="84" r="${R}" fill="none" stroke="var(--surface-2)" stroke-width="12"/>
   387|          <circle cx="84" cy="84" r="${R}" fill="none" stroke="url(#g)" stroke-width="12" stroke-linecap="round"
   388|            stroke-dasharray="${C}" stroke-dashoffset="${off}"/>
   389|          <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
   390|            <stop offset="0" stop-color="#9b4f96"/><stop offset="1" stop-color="#e6c478"/>
   391|          </linearGradient></defs>
   392|        </svg>
   393|
${hf.pct.toFixed(1)}%ezberlendi

   394|

   395|

   396|
${hf.mem.toLocaleString("tr-TR")}/ ${hf.total.toLocaleString("tr-TR")} ayet

   397|
${hf.doneSurah}/ 114 sûre

   398|

   399|

   400|    <input id="hifzSearch" class="surah-search" placeholder="Sûre ara… (örn. Yâsîn, 36)" oninput="renderHifz()" value="${esc(term)}"/>
   401|    ${list || '
Sonuç yok
'}
   402|  `;
   403|  const ns=document.getElementById("hifzSearch");
   404|  if(ns && term){ ns.focus(); ns.setSelectionRange(term.length,term.length); }
   405|}
   406|
   407|// ================== MODALS ==================
   408|function modal(html){
   409|  document.getElementById("modalRoot").innerHTML =
   410|    `<div class="modal-backdrop" data-action="modal-close"><div class="modal"><div class="modal__grip"></div>${html}</div></div>`;
   411|}
   412|function closeModal(){ document.getElementById("modalRoot").innerHTML=""; }
   413|
   414|function openTahajjudModal(){
   415|  const d=state.date, tah=state.data.tahajjud[d]||{rakats:2,time:(d===todayStr()?nowHM():""),note:""};
   416|  modal(`
   417|
Teheccüt — ${fmtShort(d)}

   418|
Rekât sayısı
   419|
<input type="range" id="mRak" min="2" max="20" step="2" value="${tah.rakats}">
   420|
${tah.rakats} rekât

   421|
Saat<input type="time" id="mTime" value="${tah.time||""}">

   422|
Not (isteğe bağlı)<textarea id="mNote" placeholder="Dua, his, sûre…">${esc(tah.note||"")}</textarea>

   423|

   424|      <button class="btn btn--ghost" data-action="modal-close">İptal</button>
   425|      <button class="btn btn--primary" data-action="tahajjud-save">Kaydet</button>
   426|

   427|  `);
   428|  document.getElementById("mRak").oninput=e=>document.getElementById("mRakV").textContent=e.target.value;
   429|}
   430|
   431|function openHifzModal(no){
   432|  const su=window.SURAHS.find(s=>s[0]===no);
   433|  const [n,name,ar,total]=su;
   434|  const mem=Math.min(state.data.hifz[no]||0,total);
   435|  modal(`
   436|
${n}. ${name} ${ar}

   437|
Toplam ${total} ayet · ezberlediğin ayet sayısı

   438|

   439|

   440|        <input type="range" id="hRange" min="0" max="${total}" value="${mem}">
   441|
${mem} / ${total}

   442|

   443|

   444|

   445|

   446|      <button class="chip" data-h="0">Sıfırla</button>
   447|      <button class="chip" data-h="${Math.ceil(total/2)}">Yarısı</button>
   448|      <button class="chip" data-h="${total}">Tamamı ✓</button>
   449|

   450|

   451|      <button class="btn btn--ghost" data-action="modal-close">İptal</button>
   452|      <button class="btn btn--primary" data-action="hifz-save" data-surah="${no}">Kaydet</button>
   453|

   454|  `);
   455|  const rng=document.getElementById("hRange");
   456|  const upd=v=>{ document.getElementById("hVal").textContent=v; document.getElementById("hBar").style.width=(v/total*100)+"%"; rng.value=v; };
   457|  rng.oninput=e=>upd(e.target.value);
   458|  document.querySelectorAll(".chip[data-h]").forEach(c=>c.onclick=()=>upd(c.dataset.h));
   459|}
   460|
   461|function openSettings(){
   462|  const pinOn=state.data.config.pinRequired;
   463|  modal(`
   464|
Ayarlar

   465|

   466|      PIN koruması ${pinOn?"(aktif)":"(kapalı)"}
   467|      <input type="password" id="setPin" inputmode="numeric" placeholder="${pinOn?'Yeni PIN (boş = kaldır)':'PIN belirle'}">
   468|
PIN, uygulamayı bu cihaz dışında açanlardan korur. Boş bırakıp kaydedersen PIN kaldırılır.

   469|

   470|    <button class="btn btn--block" data-action="pin-save">PIN'i kaydet</button>
   471|

   472|
EhliSünnet · tüm verilerin yalnızca kendi sunucunda saklanır.

   473|
<button class="btn btn--ghost btn--block" data-action="modal-close">Kapat</button>

   474|  `);
   475|}
   476|
   477|// ================== Events ==================
   478|function nowHM(){ const d=new Date(); return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}`; }
   479|function esc(s){ return String(s).replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
   480|
   481|document.addEventListener("click", async (e)=>{
   482|  const t=e.target.closest("[data-action]");
   483|  if(!t) return;
   484|  const a=t.dataset.action;
   485|
   486|  if(a==="tab"){ state.tab=t.dataset.tab; render(); return; }
   487|  if(a==="date-prev"){ state.date=addDays(state.date,-1); render(); return; }
   488|  if(a==="date-next"){ if(state.date<todayStr()){ state.date=addDays(state.date,1); render(); } return; }
   489|  if(a==="date-today"){ state.date=todayStr(); render(); return; }
   490|
   491|  if(a==="tahajjud-toggle"){
   492|    const d=state.date, cur=state.data.tahajjud[d];
   493|    if(cur){
   494|      const prev={rakats:cur.rakats,time:cur.time,note:cur.note};
   495|      mutate("/api/tahajjud",{date:d,prayed:false}, data=>{ delete data.tahajjud[d]; });
   496|      toastUndo("Teheccüt kaydı silindi", ()=>{
   497|        mutate("/api/tahajjud",{date:d,prayed:true,rakats:prev.rakats,time:prev.time,note:prev.note},
   498|          data=>{ data.tahajjud[d]={rakats:prev.rakats,time:prev.time,note:prev.note}; });
   499|      });
   500|    } else {
   501|

Use offset=501 to continue reading (showing 1-500 of 606 lines)


Explanation:
The user wants me to add a new "Gebetszeiten" (prayer times) page to the MuslimOS/EhliSünnet app. Let me understand the full picture:

1. The app is a Node.js server with no external dependencies, serving static files from `/DATA/AppData/muslimos/app/public/`
2. It has a tab-based navigation with tabs: Bugün, Teheccüt, Sünnet, Evrad, Hıfz
3. The design system uses CSS variables with emerald/purple and gold accents, dark mode support
4. The API is simple: `/api/data` GET returns state, various POST endpoints for mutations
5. Data is stored in `/DATA/AppData/muslimos/data/db.json`

What I need to do:
1. Add a new "Vakit" (prayer times) tab to the navigation
2. Create a new page that fetches prayer times from the Aladhan API for Vienna (Diyanet method, latitudeAdjustmentMethod=2)
3. Allow checking off each prayer (Fajr, Dhuhr, Asr, Maghrib, Isha) - the fard prayers
4. The sunnah prayers are already tracked separately in the Sünnet tab

Let me read the rest of app.js first to understand the full event handling and then plan the implementation.

Key design decisions:
- The prayer times should be fetched from the API client-side (or server-side?)
- Since the server is pure Node.js with no dependencies, I should either:
  a) Add server-side API endpoint that fetches from Aladhan and caches
  b) Fetch client-side from the browser

Option (a) is better because:
- Avoids CORS issues
- Can cache the times (they only change daily)
- Keeps the app self-contained

But the server has no external dependencies... I could add a simple fetch using Node's built-in `http`/`https` module. Actually, Node 22 has native `fetch`!

Let me plan the implementation:

### Server-side changes (server.js):
1. Add a new data section `vakit` to the DB schema (stores checked-off prayers per date)
2. Add `/api/vakit` GET endpoint - fetches prayer times from Aladhan API (with caching) and returns them + checked state
3. Add `/api/vakit` POST endpoint - toggles a prayer as prayed/not prayed for a date
4. Include `vakit` in `publicState()`

### Frontend changes:
1. **index.html**: Add a new tab button for "Vakit" in the tabbar
2. **app.js**:
   - Add `renderVakit()` function
   - Add event handlers for vakit toggle
   - Add `vakit` to the tab routing
3. **styles.css**: May need minor additions (but existing `.check` styles should work)

Let me read the rest of app.js first.


   501|      const time=(d===todayStr()?nowHM():"");
   502|      mutate("/api/tahajjud",{date:d,prayed:true,rakats:2,time},
   503|        data=>{ data.tahajjud[d]={rakats:2,time,note:""}; });
   504|      toast("Elhamdülillah ✓");
   505|    }
   506|    return;
   507|  }
   508|  if(a==="tahajjud-edit"){ openTahajjudModal(); return; }
   509|  if(a==="tahajjud-save"){
   510|    const d=state.date;
   511|    const rakats=+document.getElementById("mRak").value;
   512|    const time=document.getElementById("mTime").value;
   513|    const note=document.getElementById("mNote").value;
   514|    mutate("/api/tahajjud",{date:d,prayed:true,rakats,time,note},
   515|      data=>{ data.tahajjud[d]={rakats,time:(time||"").slice(0,5),note:(note||"").slice(0,500)}; });
   516|    closeModal(); toast("Kaydedildi"); return;
   517|  }
   518|
   519|  if(a==="sunnah-toggle"){
   520|    const d=state.date, key=t.dataset.key;
   521|    const willPray=!(state.data.sunnah[d]||[]).includes(key);
   522|    mutate("/api/sunnah",{date:d,key,prayed:willPray}, data=>{
   523|      const set=new Set(data.sunnah[d]||[]);
   524|      if(willPray) set.add(key); else set.delete(key);
   525|      if(set.size) data.sunnah[d]=[...set]; else delete data.sunnah[d];
   526|    });
   527|    return;
   528|  }
   529|
   530|  if(a==="evrad-select"){
   531|    const d=state.date, cur=state.data.evrad[d]||{short:0,long:0};
   532|    const key=t.dataset.key, isOn=(cur[key]||0)>0;
   533|    // Einfachauswahl: gewählte Option setzt die andere auf 0; erneutes Tippen hebt auf
   534|    const next = isOn ? {short:0,long:0} : (key==="long" ? {long:1,short:0} : {long:0,short:1});
   535|    mutate("/api/evrad",{date:d,short:next.short,long:next.long}, data=>{
   536|      if(next.short||next.long) data.evrad[d]={short:next.short,long:next.long};
   537|      else delete data.evrad[d];
   538|    });
   539|    return;
   540|  }
   541|
   542|  if(a==="hifz-open"){ openHifzModal(+t.dataset.surah); return; }
   543|  if(a==="hifz-save"){
   544|    const surah=+t.dataset.surah;
   545|    const memorized=+document.getElementById("hRange").value;
   546|    mutate("/api/hifz",{surah,memorized}, data=>{
   547|      if(memorized>0) data.hifz[surah]=memorized; else delete data.hifz[surah];
   548|    });
   549|    closeModal(); toast("Hıfz güncellendi"); return;
   550|  }
   551|
   552|  if(a==="settings"){ openSettings(); return; }
   553|  if(a==="pin-save"){
   554|    const pin=document.getElementById("setPin").value.trim();
   555|    await api("/api/pin","POST",{pin});
   556|    state.pin=pin; localStorage.setItem(PIN_KEY,pin);
   557|    closeModal(); toast(pin?"PIN ayarlandı":"PIN kaldırıldı");
   558|    state.data=await api("/api/data"); render(); return;
   559|  }
   560|  if(a==="modal-close"){
   561|    // Nur schließen bei direktem Klick auf den Hintergrund oder auf einen Schließen-Button
   562|    if(t.classList.contains("modal-backdrop")){ if(e.target===t) closeModal(); }
   563|    else closeModal();
   564|    return;
   565|  }
   566|});
   567|document.getElementById("settingsBtn").addEventListener("click",openSettings);
   568|
   569|// ================== PIN Lock ==================
   570|function showLock(){
   571|  document.getElementById("app").classList.add("hidden");
   572|  document.getElementById("lock").classList.remove("hidden");
   573|  const inp=document.getElementById("pinInput"); inp.value=""; inp.focus();
   574|}
   575|async function tryUnlock(){
   576|  const pin=document.getElementById("pinInput").value.trim();
   577|  try{
   578|    const r=await fetch("/api/session",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({pin})});
   579|    const j=await r.json();
   580|    if(j.ok){ state.pin=pin; localStorage.setItem(PIN_KEY,pin); document.getElementById("lock").classList.add("hidden"); boot(); }
   581|    else { document.getElementById("pinError").textContent="Yanlış PIN"; }
   582|  }catch{ document.getElementById("pinError").textContent="Bağlantı hatası"; }
   583|}
   584|document.getElementById("pinBtn").addEventListener("click",tryUnlock);
   585|document.getElementById("pinInput").addEventListener("keydown",e=>{ if(e.key==="Enter") tryUnlock(); });
   586|
   587|// ================== Boot ==================
   588|async function boot(){
   589|  try{
   590|    const sess=await fetch("/api/session").then(r=>r.json());
   591|    if(sess.pinRequired && !state.pin){ showLock(); return; }
   592|    state.data=await api("/api/data");
   593|    document.getElementById("app").classList.remove("hidden");
   594|    render();
   595|  }catch(e){
   596|    if(e.message==="unauthorized"){ showLock(); return; }
   597|    document.getElementById("app").classList.remove("hidden");
   598|    document.getElementById("view").innerHTML=`<div class="empty">Sunucuya bağlanılamadı.<br>${esc(e.message)}</div>`;
   599|  }
   600|}
   601|boot();
   602|
   603|// Service Worker
   604|if("serviceWorker" in navigator){
   605|  navigator.serviceWorker.register("sw.js").catch(()=>{});
   606|}
   607|


Ich habe die komplette App analysiert. Jetzt delegiere ich die Implementierung an Kimi k2.7-code mit einer präzisen Spec.

Explanation:
Ich habe jetzt die komplette MuslimOS-App verstanden. Lass mich zusammenfassen, was ich gelernt habe:

**Architektur:**
- Reiner Node.js-Server (keine externen Abhängigkeiten)
- Persistenz: JSON-Datei (`db.json`) mit atomaren Schreibvorgängen
- Frontend: Vanilla JS, kein Framework
- Design: Emerald/Gold-Farbschema, Dark Mode, Mobile-First, Glassmorphism-Elemente

**Bestehende Tabs:**
- Bugün (Dashboard)
- Teheccüt
- Sünnet & Nafile
- Evrad
- Hıfz

**Datenstruktur:**
- `tahajjud`: `{ "YYYY-MM-DD": { rakats, time, note } }`
- `sunnah`: `{ "YYYY-MM-DD": ["key1", "key2", ...] }`
- `evrad`: `{ "YYYY-MM-DD": { short: n, long: n } }`
- `hifz`: `{ "<surahNo>": memorizedAyahs }`

**Design-System:**
- CSS-Variablen: `--emerald` (#9b4f96), `--gold` (#b07d24), `--bg`, `--surface`, `--border`, etc.
- Cards mit `box-shadow`, `border-radius: 18px`
- Check-Items mit `.check` Klasse, `.on` für aktiv
- Tabbar am unteren Rand
- Topbar mit Brand, Titel, Hijri-Datum
- Date-Navigator (`datenav`)
- Modals als Bottom-Sheets
- Toast-System
- Optimistic Updates mit Undo

**Was ich bauen soll:**
- Neuen Tab "Gebetszeiten" (Namaz Vakitleri)
- API-Call zu Aladhan für Wien (Diyanet + OneSeventh)
- Zeiten anzeigen: Fajr, Sonnenaufgang, Dhuhr, Asr, Maghrib, Isha
- Für jede Gebetszeit: Checkbox zum Abhaken des Hauptgebets (Fard)
- Sünnet-Gebete sind bereits im "Sünnet"-Tab — also hier nur Fard
- Look & Feel muss zum bestehenden Design passen

**Plan:**
1. Server-seitig: Neuen API-Endpoint `/api/prayer-times` der die Aladhan-API cached (einmal pro Tag)
2. Datenstruktur: `prayers: { "YYYY-MM-DD": ["fajr", "dhuhr", ...] }` — welche Fard-Gebete gebetet wurden
3. Frontend: Neuer Tab "Vakitler" mit:
   - Gebetszeiten von der API
   - Check-Items für jedes Fard-Gebet
   - Date-Navigator (wie bei anderen Tabs)
   - Statistiken (heute, Woche, Monat)

Ich muss das Design-System exakt nachbauen — gleiche CSS-Klassen, gleiche Farben, gleiche Card-Struktur.

Jetzt delegiere ich die Implementierung an Kimi k2.7-code. Ich schreibe eine präzise Spec.

Implementiere einen neuen "Vakitler"-Tab (Gebetszeiten) in der MuslimOS/EhliSünnet-App. Der Tab soll die täglichen Gebetszeiten für Wien von der Aladhan-API holen und Checkboxen zum Abhaken der Fard-Gebete bieten.

Context:
## PROJEKT-ÜBERSICHT

MuslimOS (Dashboard-Name: "EhliSünnet") ist ein Ibadet-Tracker auf ZimaOS.
- Pfad: /DATA/AppData/muslimos/
- Server: /DATA/AppData/muslimos/app/server.js (reiner Node.js, keine externen Deps)
- Frontend: /DATA/AppData/muslimos/app/public/ (index.html, styles.css, app.js, quran-data.js)
- Daten: /DATA/AppData/muslimos/data/db.json
- Port: 8099 (gemappt auf Container-Port 3000)
- Container: node:22-alpine, läuft als muslimos

## ARCHITEKTUR

**Server (server.js):**
- Reiner http-Modul-Server, KEINE externen Abhängigkeiten
- DB als JSON-Datei mit atomaren Schreibvorgängen (tmp-Datei + rename)
- DEFAULT_DB hat: version, config, tahajjud, sunnah, evrad, hifz
- API-Endpoints: /api/data (GET), /api/tahajjud (POST), /api/sunnah (POST), /api/evrad (POST), /api/hifz (POST), /api/pin (POST), /api/session (GET/POST)
- publicState() gibt tahajjud, sunnah, evrad, hifz, config zurück
- Statische Dateien über serveStatic() mit SPA-Fallback auf index.html
- PIN-Schutz optional

**Frontend (app.js):**
- Vanilla JS, kein Framework
- State: { data, tab, date, pin }
- Tabs: today, tahajjud, sunnah, evrad, hifz
- render() dispatched an renderToday(), renderTahajjud(), renderSunnah(), renderEvrad(), renderHifz()
- dateNav() gibt Datums-Navigator zurück (← → Bugün)
- Optimistic Updates via mutate() mit Undo-Toast
- Modal-System: modal(html), closeModal()
- Toast: toast(msg), toastUndo(msg, onUndo)
- PIN-Lock-System

**Design-System (styles.css):**
- CSS-Variablen: --emerald (#9b4f96), --gold (#b07d24), --bg, --surface, --border, --radius (18px), --shadow
- Dark Mode via @media (prefers-color-scheme: dark)
- Cards: .card mit background: var(--surface), border, border-radius, box-shadow
- Check-Items: .check mit .check__box, .check__body, .check__title, .check__sub; .on für aktiv
- Tabbar: .tabbar mit .tab buttons, .active für aktuellen Tab
- Topbar: sticky, glassmorphism
- Date-Navigator: .datenav
- Section-Titles: .section-title
- Stat-Grid: .stat-grid mit .stat cards
- Modals: Bottom-Sheet mit .modal-backdrop, .modal, .modal__grip
- Buttons: .btn, .btn--primary, .btn--ghost
- Badges: .check__badge

## WAS ZU IMPLEMENTIEREN IST

### 1. Server-Änderungen (server.js)

**a) DEFAULT_DB erweitern:**
```js
prayers: {},  // { "YYYY-MM-DD": ["fajr", "dhuhr", "asr", "maghrib", "isha"] }
prayerTimesCache: { date: null, timings: null }  // einmal pro Tag cachen
```

**b) publicState() erweitern:**
```js
prayers: db.prayers,
prayerTimes: db.prayerTimesCache,
```

**c) Neuer API-Endpoint /api/prayer-times (GET):**
- Prüft ob db.prayerTimesCache.date === todayStr()
- Wenn ja: gibt gecachte timings zurück
- Wenn nein: fetched von https://api.aladhan.com/v1/timingsByCity?city=Vienna&country=Austria&method=13&latitudeAdjustmentMethod=2
- Extrahiert Fajr, Sunrise, Dhuhr, Asr, Maghrib, Isha aus der Response
- Speichert in db.prayerTimesCache = { date: todayStr(), timings: {...} }
- Gibt timings + date zurück
- WICHTIG: Node.js http.get/https.get für den Fetch verwenden (KEINE externen Deps!)
- URL parsen, https-Modul nutzen, JSON parsen

**d) Neuer API-Endpoint /api/prayers (POST):**
- Body: { date: "YYYY-MM-DD", key: "fajr"|"dhuhr"|"asr"|"maghrib"|"isha", prayed: true|false }
- Validiert date (DATE_RE) und key (muss einer der 5 sein)
- Wenn prayed=true: fügt key zum Set für das Datum hinzu
- Wenn prayed=false: entfernt key aus dem Set
- scheduleSave() + gibt publicState() zurück

### 2. Frontend-Änderungen

**a) index.html — Neuer Tab-Button in der Tabbar:**
Nach dem Hıfz-Tab-Button einfügen:
```html
<button class="tab" data-action="tab" data-tab="prayers">
  <svg viewBox="0 0 24 24"><path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2Zm0 18c-4.4 0-8-3.6-8-8s3.6-8 8-8 8 3.6 8 8-3.6 8-8 8Zm.5-13H11v6l5.2 3.2.8-1.3-4.5-2.7V7Z"/></svg>
  <span>Vakitler</span>
</button>
```

**b) app.js — State erweitern:**
- state.data enthält jetzt auch prayers + prayerTimes (vom Server)
- render()-Dispatcher um "prayers" erweitern: `prayers: renderPrayers`

**c) app.js — renderPrayers() Funktion:**
- Holt prayerTimes vom Server (state.data.prayerTimes.timings)
- Holt prayers für state.date (state.data.prayers[state.date] || [])
- Zeigt dateNav()
- Zeigt eine Card mit den 5 Gebetszeiten als Liste:
  - Jede Zeile: Name (Fajr/Sabah, Öğle, İkindi, Akşam, Yatsı), Zeit, Checkbox
  - Checkbox = .check Element mit data-action="prayer-toggle" data-key="fajr"|"dhuhr"|"asr"|"maghrib"|"isha"
  - Wenn abgehakt: .on Klasse
- Zeigt Statistiken:
  - Heute: X/5 gebetet
  - Diese Woche: Anzahl Tage mit allen 5
  - Dieser Monat (30 Tage): Durchschnitt

**d) app.js — Event-Handler für prayer-toggle:**
```js
if(a==="prayer-toggle"){
  const d=state.date, key=t.dataset.key;
  const willPray=!(state.data.prayers[d]||[]).includes(key);
  mutate("/api/prayers",{date:d,key,prayed:willPray}, data=>{
    const set=new Set(data.prayers[d]||[]);
    if(willPray) set.add(key); else set.delete(key);
    if(set.size) data.prayers[d]=[...set]; else delete data.prayers[d];
  });
  return;
}
```

**e) app.js — renderToday() erweitern:**
- Im Dashboard ("Bugün") eine zusätzliche Card/Zeile für Gebetszeiten:
  - Zeigt "X/5 vakit kılındı" mit Progress-Bar
  - Klickbar → navigiert zum prayers-Tab
  - Nur anzeigen wenn prayerTimes geladen sind

**f) app.js — Boot/API:**
- Stellt sicher, dass /api/prayer-times beim Boot geladen wird (ist Teil von /api/data)

### 3. Design-Richtlinien (STRENG EINHALTEN!)

- Alle CSS-Klassen aus styles.css verwenden: .card, .check, .check__box, .check__body, .check__title, .check__sub, .check__badge, .section-title, .stat-grid, .stat, .bar, .datenav, .page-head
- Farben NUR über CSS-Variablen: var(--emerald), var(--gold), var(--muted), var(--text), var(--border), var(--surface)
- KEINE inline styles außer für dynamische Werte (z.B. Progress-Bar-Breite)
- Gebetszeiten-Namen auf Türkisch: Sabah, Öğle, İkindi, Akşam, Yatsı
- Zeitangaben im 24h-Format (wie von der API)
- Mobile-First, responsive

### 4. WICHTIGE KONVENTIONEN

- Keine externen npm-Pakete! Server nutzt nur node:http, node:https, node:fs, node:path, node:crypto
- Frontend nutzt nur Vanilla JS, kein Framework
- mutate() Pattern für optimistic updates verwenden (wie bei sunnah-toggle)
- dateNav() wiederverwenden
- Alle Strings auf Türkisch
- DB-Änderungen über scheduleSave() persistieren
- Atomare Schreibvorgänge (tmp + rename)

## VERIFIKATION

1. Server starten: `cd /DATA/AppData/muslimos/app && node server.js` (oder Container neustarten)
2. API testen: `curl http://localhost:8099/api/prayer-times` → muss JSON mit timings zurückgeben
3. API testen: `curl -X POST http://localhost:8099/api/prayers -H 'content-type: application/json' -d '{"date":"2026-06-16","key":"fajr","prayed":true}'` → muss publicState zurückgeben
4. Browser: http://zimaos:8099 → neuer Tab "Vakitler" muss erscheinen
5. Gebetszeiten müssen korrekt angezeigt werden
6. Checkboxen müssen funktionieren (abhaken/abwählen)
7. Dashboard muss Gebetszeiten-Status zeigen
8. Dark Mode muss funktionieren
9. Date-Navigator muss funktionieren (andere Tage anschauen)

## DATEIEN DIE GEÄNDERT WERDEN

- /DATA/AppData/muslimos/app/server.js (neue Endpoints + DB-Erweiterung)
- /DATA/AppData/muslimos/app/public/index.html (neuer Tab-Button)
- /DATA/AppData/muslimos/app/public/app.js (neuer renderer + event handler + dashboard-erweiterung)

## WICHTIG

- Keine externen Abhängigkeiten hinzufügen
- Bestehenden Code nicht unnötig umstrukturieren
- Genau die gleichen Patterns verwenden wie die existierenden Tabs
- Alle Texte auf Türkisch
- Die App läuft in Docker, Pfade sind relativ zum Container (/app/server.js, /app/public/, /app/data/)


Summary:
## Zusammenfassung

### Was ich gemacht habe

Ich habe den neuen **„Vakitler“-Tab** in die EhliSünnet/MuslimOS-App implementiert. Dafür wurden Backend (reiner Node.js) und Frontend (Vanilla JS) erweitert.

### Geänderte Dateien

- **`/DATA/AppData/muslimos/app/server.js`**
  - Neuer Import `node:https`.
  - `DEFAULT_DB` erweitert um `prayers` und `prayerTimesCache`.
  - DB-Migration in `loadDb()` ergänzt.
  - `publicState()` liefert jetzt auch `prayers` und `prayerTimes`.
  - Neue Funktion `fetchAladhanPrayerTimes()` holt Wien-Gebetszeiten von `https://api.aladhan.com/v1/timingsByCity/<date>?city=Vienna&country=Austria&method=13&latitudeAdjustmentMethod=2`.
  - Neuer Endpoint `GET /api/prayer-times` (täglich cachen).
  - Neuer Endpoint `POST /api/prayers` (abhaken/abwählen der 5 Fard-Gebete).

- **`/DATA/AppData/muslimos/app/public/index.html`**
  - Neuer Tab-Button „Vakitler“ nach dem Hıfz-Tab eingefügt.

- **`/DATA/AppData/muslimos/app/public/app.js`**
  - State um `prayers` und `prayerTimes` erweitert.
  - Konstanten für die 5 Gebetszeiten (türkische Namen: Sabah, Öğle, İkindi, Akşam, Yatsı) hinzugefügt.
  - `render()`-Dispatcher um `prayers` erweitert.
  - `renderPrayers()` implementiert: Zeigt dateNav, Gebetszeiten-Liste mit Checkboxen, Fortschrittsbalken und Statistiken (heute, Woche, 30 Tage).
  - Event-Handler `prayer-toggle` implementiert (optimistic update via `mutate()`).
  - `renderToday()` zeigt jetzt eine „Namaz vakitleri“-Card mit Fortschritt.
  - `boot()` lädt beim Start zusätzlich `/api/prayer-times`.

### Was getestet wurde

- `node -c` für `server.js` und `app.js`: Syntax OK.
- Server auf Port 3001 gestartet.
- `GET /api/prayer-times` lieferte korrekte Wiener Gebetszeiten für heute (z. B. Fajr 03:46, Dhuhr 13:00, Isha 22:05).
- `POST /api/prayers` mit `{"date":"2026-06-16","key":"fajr","prayed":true}` gab den erweiterten `publicState` zurück; anschließendes Abhaken von `asr` wurde korrekt zum Set hinzugefügt.

### Probleme / Hinweise

- Port 3000 war auf dem Host bereits belegt; Tests liefen deshalb auf `PORT=3001`. Im Container läuft der Standard-Port weiterhin über `PORT=3000` (gemappt auf 8099).
- Die Aladhan-API antwortete auf `…/v1/timingsByCity` zunächst mit einem **302-Redirect**. Ich habe den Aufruf deshalb auf `…/v1/timingsByCity/<date>` umgestellt und einen `User-Agent`-Header hinzugefügt. Danach funktionierte der Abruf.
- Nach den Tests habe ich die temporär angelegte Test-DB (`/DATA/AppData/muslimos/app/data/db.json`) wieder entfernt.

### Noch zu verifizieren im Live-Betrieb

- Browser-Aufruf `http://zimaos:8099` → neuer Tab „Vakitler“ muss sichtbar sein.
- Checkboxen müssen abhakbar sein.
- Dashboard-Card „Namaz vakitleri“ muss angezeigt werden.
- Datums-Navigator im Vakitler-Tab funktioniert für vergangene/zukünftige Tage.

[NOTE: subagent modified files the parent previously read — re-read before editing: /DATA/AppData/muslimos/app/public/app.js, /DATA/AppData/muslimos/app/public/index.html, /DATA/AppData/muslimos/app/server.js]


Der Subagent hat die Implementierung abgeschlossen. Ich prüfe jetzt die Änderungen und starte den Container neu.

Explanation:
Der Subagent hat die Implementierung abgeschlossen. Jetzt muss ich:

1. Die geänderten Dateien überprüfen (re-read)
2. Den Container neustarten, damit die Änderungen live gehen
3. Die API-Endpoints testen
4. Dem Boss berichten

Laut Subagent-Summary:
- server.js: neue Endpoints + DB-Erweiterung
- index.html: neuer Tab-Button
- app.js: renderPrayers(), prayer-toggle handler, renderToday()-Erweiterung

Tests wurden auf Port 3001 gemacht (weil 3000 belegt war). Im Container läuft Port 3000 → 8099.

Ich muss jetzt:
1. Die Dateien kurz prüfen
2. Container neustarten
3. API testen
4. Boss informieren

grep -n "prayer\|prayers\|prayerTimes\|aladhan\|fetchAladhan" /DATA/AppData/muslimos/app/server.js | head -30

grep -n "prayer\|prayers\|Vakitler\|vakitler\|renderPrayers\|PRAYER" /DATA/AppData/muslimos/app/public/app.js | head -30

grep -n "Vakitler\|prayers" /DATA/AppData/muslimos/app/public/index.html


     1|'use strict';
     2|
     3|/*
     4| * MuslimOS — Ibadet-Tracker
     5| * Reiner Node.js-Server, KEINE externen Abhängigkeiten.
     6| * Persistenz: atomare Schreibvorgänge in eine JSON-Datei.
     7| */
     8|
     9|const http = require('node:http');
    10|const https = require('node:https');
    11|const fs = require('node:fs');
    12|const fsp = require('node:fs/promises');
    13|const path = require('node:path');
    14|const crypto = require('node:crypto');
    15|
    16|const PORT = process.env.PORT || 3000;
    17|const ROOT = __dirname;
    18|const PUBLIC_DIR = path.join(ROOT, 'public');
    19|const DATA_DIR = process.env.DATA_DIR || path.join(ROOT, 'data');
    20|const DB_FILE = path.join(DATA_DIR, 'db.json');
    21|
    22|// ---------------------------------------------------------------------------
    23|// Datenhaltung
    24|// ---------------------------------------------------------------------------
    25|
    26|const DEFAULT_DB = {
    27|  version: 1,
    28|  config: { pinHash: null },
    29|  tahajjud: {}, // { "YYYY-MM-DD": { rakats, time, note } }
    30|  sunnah: {},   // { "YYYY-MM-DD": ["key", ...] }
    31|  evrad: {},    // { "YYYY-MM-DD": { short: n, long: n } }
    32|  hifz: {},     // { "<surahNo>": memorizedAyahs }
    33|  prayers: {},  // { "YYYY-MM-DD": ["fajr", "dhuhr", "asr", "maghrib", "isha"] }
    34|  prayerTimesCache: { date: null, timings: null }, // einmal pro Tag cachen
    35|};
    36|
    37|let db = structuredClone(DEFAULT_DB);
    38|let saveTimer = null;
    39|let saving = false;
    40|let saveAgain = false;
    41|
    42|function loadDb() {
    43|  try {
    44|    const raw = fs.readFileSync(DB_FILE, 'utf8');
    45|    const parsed = JSON.parse(raw);
    46|    db = Object.assign(structuredClone(DEFAULT_DB), parsed);
    47|    db.config = Object.assign({ pinHash: null }, parsed.config || {});
    48|    for (const k of ['tahajjud', 'sunnah', 'evrad', 'hifz', 'prayers']) {
    49|      if (typeof db[k] !== 'object' || db[k] === null) db[k] = {};
    50|    }
    51|

Use offset=51 to continue reading (showing 1-50 of 383 lines)


Output:
33:  prayers: {},  // { "YYYY-MM-DD": ["fajr", "dhuhr", "asr", "maghrib", "isha"] }
34:  prayerTimesCache: { date: null, timings: null }, // einmal pro Tag cachen
48:    for (const k of ['tahajjud', 'sunnah', 'evrad', 'hifz', 'prayers']) {
51:    if (!db.prayerTimesCache || typeof db.prayerTimesCache !== 'object') {
52:      db.prayerTimesCache = { date: null, timings: null };
140:    prayers: db.prayers,
141:    prayerTimes: db.prayerTimesCache,
201:function fetchAladhanPrayerTimes() {
203:  const url = new URL(`https://api.aladhan.com/v1/timingsByCity/${today}`);
309:  if (pathname === '/api/prayer-times' && method === 'GET') {
311:    if (db.prayerTimesCache.date === today && db.prayerTimesCache.timings) {
312:      return sendJson(res, 200, { date: today, timings: db.prayerTimesCache.timings });
315:      const timings = await fetchAladhanPrayerTimes();
316:      db.prayerTimesCache = { date: today, timings };
322:      if (db.prayerTimesCache.timings) {
323:        return sendJson(res, 200, { date: db.prayerTimesCache.date, timings: db.prayerTimesCache.timings, cached: true });
325:      return sendJson(res, 502, { error: 'prayer times unavailable' });
329:  if (pathname === '/api/prayers' && method === 'POST') {
333:    if (!validKeys.includes(b.key)) return sendJson(res, 400, { error: 'bad prayer key' });
334:    const set = new Set(db.prayers[b.date] || []);
336:    if (set.size) db.prayers[b.date] = [...set]; else delete db.prayers[b.date];