error: Failed to parse Markdown content!
# PRD: Glidr — SPL Exam Preparation App

**Version:** 1.0
**Date:** 2026-03-15
**Status:** Ready for Implementation
**Author:** Atlas (Principal Software Architect)

---

## 1. Executive Summary

### 1.1 Product Overview

Glidr is a mobile flashcard and spaced repetition app designed to help glider pilots prepare for the EASA Sailplane Pilot License (SPL) theoretical knowledge exam. The app covers all 9 exam subjects, uses the SuperMemo SM-2 spaced repetition algorithm to optimize long-term retention, and includes a cram mode for last-minute exam preparation.

The app is sold as a one-time purchase (~$49.99) on the App Store and Google Play. Content (questions, answers, explanations) is served from a remote API at tekmidian.com and cached locally, enabling question corrections and updates without app re-releases.

### 1.2 Target Users

- Primary: German/Swiss/European glider pilot students preparing for SPL theoretical exam
- Secondary: Existing pilots refreshing knowledge or preparing for recurrency checks
- Language: English and French (bilingual, selectable in-app)

### 1.3 Success Metrics

- App Store rating >= 4.5 stars
- >= 80% of purchased users complete at least one full subject review cycle
- Content update delivery: corrections visible to users within 24 hours of server update
- Exam pass rate correlation: users who complete >= 3 full cycles pass exam at >= 90%

### 1.4 Timeline Estimate

| Phase | Duration | Deliverable |
|-------|----------|-------------|
| Phase 1: Foundation | 3 weeks | App shell, DB schema, content download, question display |
| Phase 2: Learning Engine | 2 weeks | SM-2 implementation, review queue, self-assessment |
| Phase 3: Cram Mode | 1 week | Cram mode, session stats |
| Phase 4: Purchase + Polish | 2 weeks | IAP, onboarding, UI polish, accessibility |
| Phase 5: Backend | 1 week | Server setup, API, manifest system |
| Phase 6: Testing + Release | 2 weeks | QA, beta, App Store submission |
| **Total** | **~11 weeks** | |

---

## 2. Product Requirements

### 2.1 Functional Requirements

#### FR-01: Content Display
- Display multiple choice questions with 4 answer options (A, B, C, D)
- Show question text in selected language (English or French)
- Support embedded figures (PNG and SVG) within questions
- Figures must be zoomable via pinch gesture and double-tap
- Display correct answer and explanation after user selects an answer
- Explanation collapses/expands on tap

#### FR-02: Subject Navigation
- List all 9 subjects on home screen with progress indicators
- Each subject shows: total cards, due today, learned cards, new cards
- Tap subject to begin study session

#### FR-03: Spaced Repetition Mode
- Implement SuperMemo SM-2 algorithm exactly as specified
- Present cards due today in review sessions
- After revealing answer, user rates recall using 4-button self-assessment: Again / Hard / Good / Easy
- SM-2 state (n, EF, interval) updated immediately after each rating
- Session ends when all due cards have been reviewed
- Show session summary: cards reviewed, correct count, next due date

#### FR-04: Cram Mode
- Accessible from each subject screen
- Shows ALL cards in a subject (ignoring SM-2 schedule)
- User answers, then sees correct answer + explanation
- No self-assessment rating in cram mode
- Cram progress does not modify SM-2 state
- Session score shown at end (X/Y correct)

#### FR-05: Content Updates
- On each app launch: check remote manifest for content version changes
- If new version available: download updated subject JSON in background
- Update local SQLite database with new/changed questions
- Show user notification when update is available and downloaded
- Full offline operation after initial download

#### FR-06: Purchase / Unlock
- Free tier: first 10 questions of each subject are accessible without purchase
- Paid tier: one-time purchase unlocks all 9 subjects fully
- Restore Purchases button in Settings
- Purchase state persisted in flutter_secure_storage

#### FR-07: Language Selection
- Language toggle (EN / FR) accessible from Settings and from subject screen
- Switching language immediately updates all displayed content
- Language preference persisted across app sessions

#### FR-08: Progress and Statistics
- Per-subject statistics: total, new, learning, review, mature (interval > 21 days)
- Overall retention rate (correct / total reviews)
- Study streak counter (consecutive days with at least 1 review)
- Estimated exam readiness per subject (percentage of cards with EF >= 2.0 and interval >= 7)

#### FR-09: Settings
- Language selection (EN / FR)
- New cards per day per subject (1-20, default 5)
- Review reminder notification (optional, time picker)
- Restore purchases
- App version + build number

### 2.2 Non-Functional Requirements

#### NFR-01: Performance
- App launch to home screen: < 1.5 seconds (cold start)
- Question display including image: < 300ms
- SM-2 state save after rating: < 50ms
- Content manifest check: non-blocking (background thread)

#### NFR-02: Offline Support
- Full functionality after initial content download (no network required)
- Initial download required only once; graceful offline handling after

#### NFR-03: Platform Support
- iOS 16.0+ (primary)
- Android 10+ (API level 29+) (secondary, same codebase)
- Tested on iPhone 13/14/15 (primary); Pixel 6/7 and Samsung Galaxy S21/S22 (Android)

#### NFR-04: Accessibility
- Dynamic Type support (text scales with system font size)
- VoiceOver / TalkBack support for all interactive elements
- Minimum tap target size: 44x44pt

#### NFR-05: Data Privacy
- No user accounts, no personal data collected
- No analytics in v1 (add opt-in analytics in v2)
- Purchase state stored locally only
- GDPR-compliant: no EU data transfer

---

## 3. System Architecture

### 3.1 High-Level Architecture

```
┌─────────────────────────────────────────────────┐
│ GLIDR iOS/Android App │
│ │
│ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Flutter UI │ │ Business Logic │ │
│ │ (Widgets) │◄──►│ (Riverpod Providers) │ │
│ └──────────────┘ └───────────┬───────────┘ │
│ │ │
│ ┌───────────────────────────────▼─────────────┐ │
│ │ Repository Layer (Drift ORM) │ │
│ └───────────────────────────────┬─────────────┘ │
│ │ │
│ ┌───────────────┐ ┌────────────▼──────────────┐ │
│ │ Secure Storage│ │ SQLite Database (Drift) │ │
│ │ (flutter_ │ │ questions, progress, │ │
│ │ secure_ │ │ sessions, settings │ │
│ │ storage) │ └───────────────────────────┘ │
│ └───────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Content Sync Service (Dio HTTP) │ │
│ └──────────────────┬───────────────────────────┘ │
└─────────────────────│───────────────────────────┘
│ HTTPS + X-Glidr-Key header

┌─────────────────────────────────────────────────┐
│ tekmidian.com/glidr/api/v1/ │
│ │
│ manifest.json Apache/PHP auth layer │
│ subjects/air_law.json Rate limiting │
│ subjects/meteo.json Static file serving │
│ figures/*.png HTTPS (Let's Encrypt) │
│ figures/*.svg │
└─────────────────────────────────────────────────┘
```

### 3.2 Flutter App Layer Architecture

```
lib/
├── main.dart
├── app.dart # MaterialApp, routing, theme
├── core/
│ ├── constants.dart # API base URL, API key (from --dart-define)
│ ├── router.dart # go_router route definitions
│ └── theme.dart # Color scheme, typography
├── data/
│ ├── database/
│ │ ├── database.dart # Drift database definition
│ │ ├── tables/ # Drift table definitions
│ │ └── daos/ # Data access objects per domain
│ ├── models/ # Freezed immutable data classes
│ │ ├── question.dart
│ │ ├── card_progress.dart
│ │ ├── study_session.dart
│ │ └── subject.dart
│ ├── repositories/ # Repository interfaces + implementations
│ │ ├── question_repository.dart
│ │ ├── progress_repository.dart
│ │ └── content_sync_repository.dart
│ └── api/
│ ├── glidr_api_client.dart # Dio client with auth interceptor
│ └── models/ # API response models
├── domain/
│ └── sm2/
│ ├── sm2_algorithm.dart # Pure SM-2 calculation functions
│ └── review_scheduler.dart # Queue building logic
├── presentation/
│ ├── home/
│ │ ├── home_screen.dart
│ │ └── home_provider.dart
│ ├── study/
│ │ ├── study_screen.dart # SM-2 review session
│ │ ├── study_provider.dart
│ │ └── widgets/
│ │ ├── question_card.dart
│ │ ├── answer_options.dart
│ │ ├── rating_buttons.dart
│ │ └── figure_viewer.dart # photo_view zoomable image
│ ├── cram/
│ │ ├── cram_screen.dart
│ │ └── cram_provider.dart
│ ├── subject_detail/
│ │ ├── subject_detail_screen.dart
│ │ └── subject_detail_provider.dart
│ ├── stats/
│ │ ├── stats_screen.dart
│ │ └── stats_provider.dart
│ └── settings/
│ ├── settings_screen.dart
│ └── settings_provider.dart
└── l10n/
├── app_en.arb
└── app_fr.arb
```

---

## 4. Data Models

### 4.1 Question JSON Format (Remote API)

Each subject is served as a single JSON file. Example structure:

```json
{
"subject_id": "air_law",
"subject_code": "01",
"name": {
"en": "Air Law",
"fr": "Droit aérien"
},
"version": "1.0.0",
"updated_at": "2026-03-15T00:00:00Z",
"questions": [
{
"id": "air_law_q1",
"number": 1,
"text": {
"en": "The holder of an SPL license or LAPL(S) license completed a total of 9 winch launches, 4 launches in aero-tow and 2 bungee launches during the last 24 months. What launch methods may the pilot conduct as PIC today?",
"fr": "Le titulaire d'une licence SPL ou LAPL(S) a effectué au total 9 lancements au treuil, 4 lancements en remorqué et 2 lancements au sandow au cours des 24 derniers mois. Quelles méthodes de lancement le pilote peut-il effectuer en tant que commandant de bord aujourd'hui?"
},
"options": {
"en": {
"A": "Winch and bungee.",
"B": "Winch, bungee and aero-tow.",
"C": "Winch and aero-tow.",
"D": "Aero-tow and bungee."
},
"fr": {
"A": "Treuil et sandow.",
"B": "Treuil, sandow et remorqué.",
"C": "Treuil et remorqué.",
"D": "Remorqué et sandow."
}
},
"correct": "A",
"explanation": {
"en": "Under Part-SFCL (SFCL.010 and SFCL.160), a pilot must have completed at least 5 launches using a specific launch method within the preceding 24 months...",
"fr": "Selon la Part-SFCL (SFCL.010 et SFCL.160), un pilote doit avoir effectué au moins 5 lancements en utilisant une méthode de lancement spécifique au cours des 24 mois précédents..."
},
"figures": []
},
{
"id": "pfp_q14",
"number": 14,
"text": {
"en": "What is the maximum payload according to the loading table?",
"fr": "Quelle est la charge utile maximale selon le tableau de chargement?"
},
"options": {
"en": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" },
"fr": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" }
},
"correct": "B",
"explanation": {
"en": "According to the Discus loading table shown...",
"fr": "Selon le tableau de chargement du Discus affiché..."
},
"figures": [
{
"filename": "bazl_30_q14_discus_loading_table.png",
"type": "png",
"alt_en": "Discus aircraft loading table",
"alt_fr": "Tableau de chargement de l'aéronef Discus",
"position": "question"
}
]
}
]
}
```

### 4.2 Manifest JSON Format

```json
{
"api_version": "1",
"manifest_version": "1.0.0",
"updated_at": "2026-03-15T00:00:00Z",
"subjects": {
"air_law": { "version": "1.0.0", "sha256": "abc...", "question_count": 110, "size_bytes": 85000 },
"aircraft_general_knowledge": { "version": "1.0.0", "sha256": "def...", "question_count": 110, "size_bytes": 92000 },
"communications": { "version": "1.0.0", "sha256": "ghi...", "question_count": 90, "size_bytes": 71000 },
"flight_performance": { "version": "1.0.0", "sha256": "jkl...", "question_count": 90, "size_bytes": 68000 },
"human_performance": { "version": "1.0.0", "sha256": "mno...", "question_count": 110, "size_bytes": 83000 },
"meteorology": { "version": "1.0.0", "sha256": "pqr...", "question_count": 110, "size_bytes": 88000 },
"navigation": { "version": "1.0.0", "sha256": "stu...", "question_count": 141, "size_bytes": 115000 },
"operational_procedures": { "version": "1.0.0", "sha256": "vwx...", "question_count": 110, "size_bytes": 84000 },
"principles_of_flight": { "version": "1.0.0", "sha256": "yza...", "question_count": 110, "size_bytes": 86000 }
}
}
```

### 4.3 SQLite Database Schema (Drift)

```sql
-- Questions table (populated from downloaded JSON)
CREATE TABLE questions (
id TEXT NOT NULL PRIMARY KEY,
subject_id TEXT NOT NULL,
number INTEGER NOT NULL,
text_en TEXT NOT NULL,
text_fr TEXT NOT NULL,
options_en TEXT NOT NULL, -- JSON string: {"A":"...","B":"...","C":"...","D":"..."}
options_fr TEXT NOT NULL,
correct TEXT NOT NULL, -- "A" | "B" | "C" | "D"
explanation_en TEXT NOT NULL,
explanation_fr TEXT NOT NULL,
figures TEXT NOT NULL -- JSON array string (empty "[]" if no figures)
);

-- SM-2 progress per card
CREATE TABLE card_progress (
question_id TEXT NOT NULL PRIMARY KEY,
repetition_number INTEGER NOT NULL DEFAULT 0,
easiness_factor REAL NOT NULL DEFAULT 2.5,
interval_days INTEGER NOT NULL DEFAULT 1,
next_review_date TEXT, -- "YYYY-MM-DD", NULL = new (never reviewed)
last_review_date TEXT, -- "YYYY-MM-DD"
total_reviews INTEGER NOT NULL DEFAULT 0,
correct_reviews INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (question_id) REFERENCES questions(id)
);

-- Study sessions
CREATE TABLE study_sessions (
id TEXT NOT NULL PRIMARY KEY, -- UUID
started_at TEXT NOT NULL, -- ISO 8601
ended_at TEXT,
mode TEXT NOT NULL, -- "spaced_repetition" | "cram"
subject_id TEXT, -- NULL = mixed
cards_reviewed INTEGER NOT NULL DEFAULT 0,
cards_correct INTEGER NOT NULL DEFAULT 0
);

-- Individual review events
CREATE TABLE review_log (
id TEXT NOT NULL PRIMARY KEY, -- UUID
session_id TEXT NOT NULL,
question_id TEXT NOT NULL,
reviewed_at TEXT NOT NULL, -- ISO 8601
quality INTEGER NOT NULL, -- 0-5 (SM-2 grade) or -1 for cram
time_to_answer_ms INTEGER,
was_cram INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (session_id) REFERENCES study_sessions(id),
FOREIGN KEY (question_id) REFERENCES questions(id)
);

-- Content version tracking
CREATE TABLE content_manifest (
subject_id TEXT NOT NULL PRIMARY KEY,
version TEXT NOT NULL,
sha256 TEXT NOT NULL,
downloaded_at TEXT NOT NULL,
question_count INTEGER NOT NULL
);

-- App settings (key-value)
CREATE TABLE settings (
key TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
-- Default settings:
-- language: "en"
-- new_cards_per_day: "5"
-- reminder_enabled: "false"
-- reminder_time: "19:00"
-- is_unlocked: "false"
-- onboarding_complete: "false"
```

---

## 5. Learning Algorithm: SM-2 Implementation

### 5.1 Core Algorithm

```dart
// lib/domain/sm2/sm2_algorithm.dart

class SM2Result {
final int repetitionNumber;
final double easinessFactor;
final int intervalDays;
final DateTime nextReviewDate;

const SM2Result({
required this.repetitionNumber,
required this.easinessFactor,
required this.intervalDays,
required this.nextReviewDate,
});
}

/// Grade 0: complete blackout
/// Grade 1: incorrect, remembered on seeing answer
/// Grade 2: incorrect, correct seemed easy after
/// Grade 3: correct with serious difficulty
/// Grade 4: correct after hesitation
/// Grade 5: perfect response
SM2Result computeNextInterval({
required int currentRepetitionNumber, // n
required double currentEasinessFactor, // EF
required int currentIntervalDays, // I
required int quality, // 0-5
}) {
assert(quality >= 0 && quality <= 5);

int n = currentRepetitionNumber;
double ef = currentEasinessFactor;
int interval = currentIntervalDays;

if (quality < 3) {
// Failed review: restart interval sequence, keep EF
n = 0;
interval = 1;
} else {
// Successful review: advance interval
switch (n) {
case 0:
interval = 1;
case 1:
interval = 6;
default:
interval = (interval * ef).ceil();
}
n += 1;
}

// Update EF (clamp to minimum 1.3)
ef = ef + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
ef = ef.clamp(1.3, double.infinity);

final nextDate = DateTime.now().add(Duration(days: interval));

return SM2Result(
repetitionNumber: n,
easinessFactor: ef,
intervalDays: interval,
nextReviewDate: DateTime(nextDate.year, nextDate.month, nextDate.day),
);
}
```

### 5.2 Quality Grade Mapping (4-Button UI)

The app presents 4 buttons (Anki-style simplification of SM-2's 0-5 scale):

| Button | Color | SM-2 Grade | Effect |
|--------|-------|-----------|--------|
| Again | Red | 1 | Reset interval to 1 day, EF -0.54 |
| Hard | Orange | 3 | EF -0.14, interval advances slowly |
| Good | Green | 4 | EF unchanged, interval advances normally |
| Easy | Blue | 5 | EF +0.10, interval advances faster |

### 5.3 Daily Review Queue

```dart
// lib/domain/sm2/review_scheduler.dart

Future> buildDailyQueue({
required String? subjectId, // null = all subjects
required int maxNewCards,
}) async {
final today = DateTime.now();
final todayStr = '${today.year}-${today.month.toString().padLeft(2,'0')}-${today.day.toString().padLeft(2,'0')}';

// 1. Due cards (have been reviewed before, scheduled for today or earlier)
final dueCards = await _progressDao.getDueCards(
subjectId: subjectId,
asOf: todayStr,
);

// 2. New cards (never reviewed, next_review_date IS NULL)
final newCards = await _progressDao.getNewCards(
subjectId: subjectId,
limit: maxNewCards,
);

// Shuffle each group independently, then concatenate: due first, then new
dueCards.shuffle();
newCards.shuffle();

return [...dueCards, ...newCards];
}
```

### 5.4 Cram Mode

```dart
Future> buildCramQueue({
required String subjectId,
required bool shuffled,
}) async {
final questions = await _questionDao.getAllForSubject(subjectId);
if (shuffled) questions.shuffle();
return questions;
}

// Cram review recording — does NOT update card_progress SM-2 state
Future recordCramReview({
required String sessionId,
required String questionId,
required bool wasCorrect,
}) async {
await _reviewLogDao.insert(ReviewLogEntry(
id: const Uuid().v4(),
sessionId: sessionId,
questionId: questionId,
reviewedAt: DateTime.now().toIso8601String(),
quality: wasCorrect ? 4 : 1, // approximate mapping for stats
wasCram: true,
));
// card_progress is intentionally NOT updated
}
```

---

## 6. API Design

### 6.1 Base Configuration

```
Base URL: https://tekmidian.com/glidr/api/v1
Auth Header: X-Glidr-Key: {compile-time-injected-secret}
Content-Type: application/json
```

### 6.2 Endpoints

#### GET /manifest.json
Returns the current version hash for all subjects. Used to detect if local content needs updating.

Response:
```json
{
"api_version": "1",
"manifest_version": "1.2.0",
"updated_at": "2026-03-15T00:00:00Z",
"subjects": {
"air_law": { "version": "1.2.0", "sha256": "abc123...", "question_count": 110, "size_bytes": 85000 },
...
}
}
```

#### GET /subjects/{subject_id}.json
Returns full question set for a subject. Only called when local hash differs from manifest.

Path parameters:
- `subject_id`: one of `air_law`, `aircraft_general_knowledge`, `communications`, `flight_performance`, `human_performance`, `meteorology`, `navigation`, `operational_procedures`, `principles_of_flight`

Response: Full subject JSON as described in Section 4.1.

#### GET /figures/{filename}
Serves a figure file (PNG or SVG). Called only if figures are NOT bundled with the app.

Note: For v1, all figures are bundled as Flutter assets. This endpoint is a fallback for future use or figure-only updates.

### 6.3 Error Handling

| HTTP Status | Meaning | App Behavior |
|------------|---------|-------------|
| 200 | Success | Process response |
| 304 | Not Modified (future: ETag support) | Use cached content |
| 403 | Invalid API key | Log error silently, use cached content |
| 404 | Subject not found | Log error, skip that subject update |
| 429 | Rate limited | Retry after 60 seconds |
| 500 | Server error | Use cached content, retry next launch |

All errors are non-fatal: the app always falls back to cached local content.

### 6.4 Content Sync Flow

```
App Launch

├── Is local content available?
│ NO → Show "Downloading content..." screen
│ Download all 9 subjects + figures sequentially
│ Show progress bar
│ On complete → proceed to Home

│ YES → Proceed to Home immediately
│ Background: check manifest
│ │
│ ├── Hash differs for any subject?
│ │ YES → Download updated subject JSON
│ │ Update SQLite questions table
│ │ Show subtle "Content updated" banner
│ │
│ └── No changes → do nothing

└── Home Screen
```

---

## 7. UI Flows and Screens

### 7.1 Screen Inventory

| Screen | Route | Purpose |
|--------|-------|---------|
| Splash | `/` | App init, content check, auth check |
| Onboarding | `/onboarding` | First-run: explain app, prompt purchase or free trial |
| Home | `/home` | Subject list with progress indicators |
| Subject Detail | `/subject/:id` | Subject overview, start study/cram |
| Study Session | `/study/:id` | SM-2 review session |
| Cram Session | `/cram/:id` | Cram mode session |
| Session Complete | `/session-complete` | Post-session summary |
| Statistics | `/stats` | Overall and per-subject stats |
| Settings | `/settings` | Language, IAP, preferences |
| Paywall | `/paywall` | Purchase screen for locked content |

### 7.2 Home Screen Layout

```
┌─────────────────────────────┐
│ Glidr [Stats] [⚙]│
├─────────────────────────────┤
│ Good morning! │
│ Review streak: 🔥 7 days │
│ │
│ Due today: 23 cards │
│ [Start All Reviews] │
├─────────────────────────────┤
│ Subjects │
│ │
│ 01 Air Law ■■■□□ │
│ Due: 3 · New: 5 │
│ │
│ 02 Aircraft General ■■□□□ │
│ Due: 7 · New: 5 │
│ │
│ 03 Communications ■□□□□ │
│ Due: 0 · New: 5 │
│ │
│ ... (scrollable list) │
└─────────────────────────────┘
```

### 7.3 Study Session Flow

```
1. Question Screen
┌─────────────────────────────┐
│ Air Law Q23 of 34 │
│ Progress bar ████░░░░░░ │
├─────────────────────────────┤
│ │
│ Question text here... │
│ │
│ [Figure if present, │
│ pinch to zoom] │
│ │
├─────────────────────────────┤
│ ○ A) Option text │
│ ○ B) Option text │
│ ○ C) Option text │
│ ○ D) Option text │
└─────────────────────────────┘

2. Answer Revealed Screen (after tapping an option)
┌─────────────────────────────┐
│ Air Law Q23 of 34 │
├─────────────────────────────┤
│ │
│ Question text... │
│ │
├─────────────────────────────┤
│ ○ A) Option ← Wrong │
│ ✓ B) Option ← CORRECT │
│ ○ C) Option │
│ ○ D) Option │
├─────────────────────────────┤
│ > Explanation (tap to │
│ expand) │
│ Under Part-SFCL... │
├─────────────────────────────┤
│ How well did you know it? │
│ [Again] [Hard] [Good][Easy]│
└─────────────────────────────┘

3. Session Complete Screen
┌─────────────────────────────┐
│ Session Complete! │
│ │
│ Reviewed: 23 cards │
│ Correct: 18 (78%) │
│ Next review: Tomorrow │
│ │
│ [Back to Home] │
└─────────────────────────────┘
```

### 7.4 Cram Mode Flow

```
1. Subject screen → [Cram Mode] button
2. Cram session: question → tap to reveal → correct/incorrect tap
3. No self-rating buttons (just "Next" arrow)
4. Session end: score summary (X/Y)
```

### 7.5 Figure Viewer

When a question includes a figure:
- Figure displayed inline in the question card
- `photo_view` widget wraps the image
- Pinch-to-zoom with min/max scale (0.5x - 5.0x)
- Double-tap resets to fit
- Single-tap on figure expands to full-screen overlay
- Full-screen overlay: same zoom behavior + close button (X)

---

## 8. Technology Stack

### 8.1 Dependencies (pubspec.yaml)

```yaml
dependencies:
flutter:
sdk: flutter

# State management
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0

# Database
drift: ^2.18.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.1.0
path: ^1.9.0

# HTTP
dio: ^5.4.0

# Immutable data models
freezed_annotation: ^2.4.0
json_annotation: ^4.9.0

# Image zoom
photo_view: ^0.15.0

# In-app purchase
in_app_purchase: ^3.1.0

# Secure storage (API key, purchase state)
flutter_secure_storage: ^9.0.0

# Navigation
go_router: ^13.0.0

# UUID generation
uuid: ^4.4.0

# Crypto (SHA-256 for manifest verification)
crypto: ^3.0.0

# SVG rendering
flutter_svg: ^2.0.0

# Internationalization
flutter_localizations:
sdk: flutter
intl: ^0.19.0

dev_dependencies:
flutter_test:
sdk: flutter
drift_dev: ^2.18.0
riverpod_generator: ^2.3.0
build_runner: ^2.4.0
freezed: ^2.4.0
json_serializable: ^6.7.0
flutter_lints: ^4.0.0
```

### 8.2 Flutter Configuration

```
flutter:
assets:
- assets/figures/ # All 58 figure files (PNG + SVG)
- assets/fonts/ # Custom fonts if used

generate: true # Enable l10n generation
```

Build-time injection of API key (never in source):
```bash
flutter build ios --dart-define=GLIDR_API_KEY=your_secret_here
flutter build appbundle --dart-define=GLIDR_API_KEY=your_secret_here
```

---

## 9. Backend Setup (tekmidian.com)

### 9.1 Directory Structure

```
/var/www/tekmidian.com/public_html/glidr/
├── api/
│ └── v1/
│ ├── .htaccess # Auth + CORS headers
│ ├── manifest.json # Regenerated on each content update
│ ├── subjects/
│ │ ├── air_law.json
│ │ ├── aircraft_general_knowledge.json
│ │ ├── communications.json
│ │ ├── flight_performance.json
│ │ ├── human_performance.json
│ │ ├── meteorology.json
│ │ ├── navigation.json
│ │ ├── operational_procedures.json
│ │ └── principles_of_flight.json
│ └── figures/
│ ├── *.png (50 files)
│ └── *.svg (8 files)
└── tools/
└── generate_manifest.php # CLI tool to regenerate manifest.json
```

### 9.2 .htaccess Auth Configuration

```apache
# /glidr/api/v1/.htaccess
Options -Indexes

# CORS for potential web use
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Headers "X-Glidr-Key, Content-Type"

# Auth check via environment variable
SetEnvIf HTTP_X_GLIDR_KEY "^your_secret_here$" GLIDR_AUTH=1
Order Deny,Allow
Deny from all
Allow from env=GLIDR_AUTH

# Better: use a PHP wrapper for auth to avoid key in .htaccess
```

Better approach — PHP auth wrapper:

```php
// auth.php — included by all JSON-serving endpoints
$provided_key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';
$expected_key = getenv('GLIDR_API_KEY'); // Set in Apache/server environment

if (empty($expected_key) || $provided_key !== $expected_key) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => 'Forbidden']);
exit;
}
```

### 9.3 Content Update Workflow

When questions need to be corrected or updated:

1. Edit the source Markdown files (already in Obsidian vault)
2. Run the conversion tool (see Section 10.4) to regenerate JSON files
3. Upload new JSON files to server via SFTP/rsync
4. Run `generate_manifest.php` to update version hashes
5. App users receive update on next launch

---

## 10. Implementation Checklists

### 10.1 Phase 1: Foundation (3 weeks)

#### Database & Models
- [ ] Set up Flutter project with all dependencies listed in Section 8.1
- [ ] Create Drift database with all tables from Section 4.3
- [ ] Generate Drift DAOs for questions, card_progress, review_log, settings
- [ ] Create Freezed data models for Question, CardProgress, StudySession
- [ ] Write unit tests for all DAOs

#### Content Download
- [ ] Implement Dio HTTP client with X-Glidr-Key auth interceptor
- [ ] Implement manifest.json fetch and local version comparison
- [ ] Implement subject JSON download with SHA-256 verification
- [ ] Implement SQLite import from downloaded JSON
- [ ] Implement first-run download screen with progress indicator
- [ ] Implement background update check on subsequent launches

#### Question Display
- [ ] Implement question card widget (text + options)
- [ ] Implement figure viewer widget using photo_view
- [ ] Implement SVG rendering via flutter_svg
- [ ] Implement answer selection with color feedback (correct/incorrect)
- [ ] Implement explanation collapsible section

#### App Shell
- [ ] Set up go_router with all routes from Section 7.1
- [ ] Implement Riverpod provider structure
- [ ] Implement home screen with subject list
- [ ] Implement subject detail screen
- [ ] Implement bilingual content switching (EN/FR)

#### Testing Checklist — Phase 1
- [ ] Unit tests: JSON parsing for all 9 subjects
- [ ] Unit tests: SHA-256 manifest verification
- [ ] Unit tests: SQLite import idempotency (re-importing same data is safe)
- [ ] Widget tests: question card displays correctly for question with figure
- [ ] Widget tests: question card displays correctly for question without figure
- [ ] Integration test: full download flow on fresh install
- [ ] Manual test: figure zoom works on real device

### 10.2 Phase 2: Learning Engine (2 weeks)

#### SM-2 Algorithm
- [ ] Implement `computeNextInterval()` function (Section 5.1)
- [ ] Unit test all quality grades 0-5 with known expected outputs
- [ ] Unit test EF clamping at 1.3 minimum
- [ ] Unit test interval reset on quality < 3 (keeps EF, resets n and I)
- [ ] Unit test progression: n=0→1day, n=1→6days, n=2→6*EF days

#### Review Queue
- [ ] Implement `buildDailyQueue()` — due cards + new cards (Section 5.3)
- [ ] Implement "new cards per day" limit from settings
- [ ] Implement queue shuffle
- [ ] Unit test queue: due cards before new cards
- [ ] Unit test queue: respects new card limit

#### Study Session UI
- [ ] Implement study session screen with question display
- [ ] Implement progress bar (X of Y reviewed)
- [ ] Implement answer tap → reveal flow
- [ ] Implement 4-button rating row (Again / Hard / Good / Easy)
- [ ] On rating: call `computeNextInterval()`, persist to card_progress
- [ ] Log review to review_log table
- [ ] Implement within-session re-show for quality < 3 cards
- [ ] Implement session completion detection
- [ ] Implement session summary screen

#### Testing Checklist — Phase 2
- [ ] Unit tests: all SM-2 branches (quality 0-5, all n values, EF edge cases)
- [ ] Unit tests: daily queue respects new card limit
- [ ] Integration test: complete a study session of 10 cards, verify DB state
- [ ] Integration test: rating "Again" re-shows card in same session
- [ ] Integration test: session completion triggers summary screen
- [ ] Manual test: study 20 cards in one session, check review_log entries

### 10.3 Phase 3: Cram Mode (1 week)

- [ ] Implement cram mode queue builder (all cards in subject, shuffled)
- [ ] Implement cram session screen (question → reveal → next, no rating buttons)
- [ ] Implement cram correct/incorrect tap (for session stats only)
- [ ] Verify cram reviews do NOT modify card_progress SM-2 state
- [ ] Log cram reviews to review_log with was_cram=1
- [ ] Implement cram session summary screen

#### Testing Checklist — Phase 3
- [ ] Unit test: cram review does not modify card_progress
- [ ] Integration test: cram session records in review_log with was_cram=1
- [ ] Integration test: SM-2 state unchanged after cram session

### 10.4 Phase 4: Purchase + Polish (2 weeks)

#### In-App Purchase
- [ ] Create non-consumable IAP product in App Store Connect
- [ ] Create one-time product in Google Play Console
- [ ] Implement `in_app_purchase` purchase flow
- [ ] Implement "Restore Purchases" button
- [ ] Persist unlock state in flutter_secure_storage
- [ ] Implement paywall screen with subject preview (first 10 questions free)
- [ ] Gate subject access: > Q10 requires purchase

#### Statistics Screen
- [ ] Implement per-subject stats: new / learning / review / mature counts
- [ ] Implement overall retention rate calculation
- [ ] Implement study streak counter
- [ ] Implement "exam readiness" indicator per subject

#### Onboarding
- [ ] Implement first-run onboarding (3-4 screens: welcome, how it works, language, purchase/trial)
- [ ] Implement first-run content download trigger

#### UI Polish
- [ ] Implement review notification (local notification at configured time)
- [ ] Implement settings screen (language, new cards/day, reminder, restore, version)
- [ ] Accessibility: VoiceOver labels on all interactive elements
- [ ] Dynamic Type: test at all font sizes
- [ ] Dark mode support

#### Testing Checklist — Phase 4
- [ ] Manual test: purchase flow on Sandbox environment (iOS)
- [ ] Manual test: restore purchases after app delete and reinstall
- [ ] Manual test: free tier correctly limits to 10 questions per subject
- [ ] Accessibility audit: VoiceOver navigate entire study session
- [ ] Manual test: dark mode on iOS and Android

### 10.5 Phase 5: Backend (1 week)

- [ ] Provision `tekmidian.com/glidr/api/v1/` directory
- [ ] Set GLIDR_API_KEY server environment variable
- [ ] Write and deploy `.htaccess` auth rules
- [ ] Write Markdown-to-JSON conversion script (Python or PHP)
- [ ] Convert all 9 subject Markdown files to JSON format
- [ ] Convert French Markdown files and merge into bilingual JSON
- [ ] Copy all figures to server
- [ ] Write and run `generate_manifest.php` to create initial manifest.json
- [ ] Test all endpoints with curl (with and without API key)
- [ ] Test rate limiting (verify 429 on abuse)
- [ ] Document update workflow in a README

#### Security Checklist — Phase 5
- [ ] HTTPS with valid SSL certificate on tekmidian.com
- [ ] API key not present in any git repository (use environment variable)
- [ ] Directory listing disabled (Options -Indexes)
- [ ] Server error pages don't expose stack traces
- [ ] Test: direct URL access without API key returns 403

### 10.6 Phase 6: Testing + Release (2 weeks)

- [ ] Full regression test on iPhone 13/14/15 (real devices)
- [ ] Full regression test on Android Pixel 6/7 and Samsung Galaxy S21
- [ ] TestFlight beta (iOS): 5-10 beta testers
- [ ] Beta feedback incorporated
- [ ] App Store listing: screenshots, description (EN + FR), keywords
- [ ] App Store Review Guidelines compliance check
- [ ] Privacy Policy published (required for App Store)
- [ ] Submit for App Store review
- [ ] Google Play closed testing track
- [ ] Submit for Play Store review

---

## 11. Content Conversion Tool

A command-line Python script to convert the existing Obsidian Markdown files to the JSON format expected by the API.

### 11.1 Script Purpose

Input: `/SPL Exam Questions/01 - Air Law.md` + `/SPL Exam Questions FR/01 - Droit aérien.md`
Output: `air_law.json` matching the schema in Section 4.1

### 11.2 Parsing Logic

The script must handle:
1. Parse `### Q{N}: {text} ^q{N}` as question text and number
2. Parse `- A) ... B) ... C) ... D) ...` as answer options
3. Parse `**Correct: {letter})**` as correct answer
4. Parse `> **Explanation:** {text}` as explanation
5. Parse `![[figures/{filename}]]` and `![alt](figures/{filename})` as figure references
6. Match each English question to its French counterpart by question number
7. Combine into bilingual JSON structure

### 11.3 Script Location

`/Users/i052341/dev/apps/glidr/tools/convert_questions.py`

(To be created as part of Phase 5)

---

## 12. Risk Assessment

| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|-----------|
| App Store rejection (IAP implementation) | Low | High | Follow IAP guidelines strictly; add required Restore Purchases button |
| Question count discrepancy (Markdown vs spec) | Medium | Low | Conversion script validates counts and flags mismatches |
| tekmidian.com SSL certificate expiry breaking content sync | Low | Medium | Set calendar reminder for cert renewal; app falls back to cached content |
| API key extracted from binary | Low | Low | App contains no user data; question bank has limited commercial value; rotate key if abused |
| Flutter major version breaking dependencies | Low | Medium | Pin to Flutter stable channel; update dependencies deliberately, not on latest |
| French translation gaps | Medium | Medium | Conversion script flags any English questions without a matching French question |

---

## 13. Future Enhancements (v2 Backlog)

- User accounts: sync progress across devices via iCloud/server
- More content: additional national question banks (Germany, Austria, UK)
- Mock exam mode: simulate real SPL exam (random selection, timed, per EASA format)
- Detailed analytics: per-topic weak areas, study time tracking
- Opt-in analytics: understand which questions users find hardest
- Push notifications: study reminders
- Certificate pinning: harden API against MITM attacks
- Apple App Attest: prevent automated bulk downloading
- Widget: iOS home screen widget showing "cards due today"
- Apple Watch: quick review on watch
- Web version: study on desktop browser (Flutter Web)

---

## Appendix A: Subject IDs Reference

| Code | Subject Name (EN) | Subject Name (FR) | subject_id |
|------|-------------------|-------------------|------------|
| 01 | Air Law | Droit aérien | air_law |
| 02 | Aircraft General Knowledge | Connaissances générales de l'aéronef | aircraft_general_knowledge |
| 03 | Communications | Communications | communications |
| 04 | Flight Performance and Planning | Performances et planification du vol | flight_performance |
| 05 | Human Performance | Performance humaine | human_performance |
| 06 | Meteorology | Météorologie | meteorology |
| 07 | Navigation | Navigation | navigation |
| 08 | Operational Procedures | Procédures opérationnelles | operational_procedures |
| 09 | Principles of Flight | Principes du vol | principles_of_flight |

## Appendix B: Question Count Reference

| Subject | Questions in Current Files | Target (Full Bank) |
|---------|---------------------------|-------------------|
| Air Law | 50 (QuizVDS) | 110 |
| Aircraft General Knowledge | 50 (QuizVDS) | 110 |
| Communications | 50 (QuizVDS) | 90 |
| Flight Performance and Planning | 30 (QuizVDS) | 90 |
| Human Performance | 50 (QuizVDS) | 110 |
| Meteorology | 50 (QuizVDS) | 110 |
| Navigation | 80 (QuizVDS + SFVS) | 141 |
| Operational Procedures | 50 (QuizVDS) | 110 |
| Principles of Flight | 50 (QuizVDS) | 110 |
| **Total** | **510** | **981** |

The app works with whatever is in the JSON files. The full bank (981 questions) can be built out over time by adding BAZL mock exam questions, which are already partially present in the Markdown files as supplementary sections.

## Appendix C: SM-2 EF Delta at Each Quality Grade

| Quality | EF Delta | Example: EF=2.5 → |
|---------|----------|-------------------|
| 5 | +0.10 | 2.60 |
| 4 | 0.00 | 2.50 |
| 3 | -0.14 | 2.36 |
| 2 | -0.32 | 2.18 |
| 1 | -0.54 | 1.96 |
| 0 | -0.80 | 1.70 |

Formula: `delta = 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)`