Matthias Nott
2026-03-18 79f87c25496271939c9e7792515e9dfde837f368
chore: add gitignore, remove FR figure duplicates
24 files added
91 files deleted
changed files
.gitignore patch | view | blame | history
Glidr.md patch | view | blame | history
SPL Exam Questions FR/figures/t10_q114 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t10_q70 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t10_q77 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t10_q88 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t10_q94 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t20_q103 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t20_q87 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t20_q90 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t20_q96 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q19 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q20 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q21 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q27 2.svg patch | view | blame | history
SPL Exam Questions FR/figures/t30_q28 2.svg patch | view | blame | history
SPL Exam Questions FR/figures/t30_q29 2.svg patch | view | blame | history
SPL Exam Questions FR/figures/t30_q36 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q40 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q41 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q44 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q46 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q47 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q57 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q58 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q59 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q61 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q62 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q63 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q68 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q69 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q72 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q75 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q77 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q80 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q81 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q82 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q83 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q88 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q91 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q92 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q93 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q94 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q95 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t30_q96 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t40_q111 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t40_q114 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t40_q48 2.svg patch | view | blame | history
SPL Exam Questions FR/figures/t40_q49 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q101 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q103 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q107 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q129 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q131 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q145 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q162 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q179 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q182 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q200 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q57 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q61 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q65 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q66 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q71 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q73 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q76 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q77 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q82 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q90 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q92 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t50_q93 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q153 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q161 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q164 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q167 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q171 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q46 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t60_q6 2.svg patch | view | blame | history
SPL Exam Questions FR/figures/t80_q102 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q111 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q112 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q113 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q123 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q129 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q132 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q150 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q151 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q152 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q66 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q75 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q87 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q90 2.png patch | view | blame | history
SPL Exam Questions FR/figures/t80_q95 2.png patch | view | blame | history
app/glidr patch | view | blame | history
articles/how-it-works.md patch | view | blame | history
articles/supermemo-wired.md patch | view | blame | history
business.md patch | view | blame | history
fix_explanation_formatting.py patch | view | blame | history
privacy.md patch | view | blame | history
screenshots/IMG_0737.PNG patch | view | blame | history
screenshots/IMG_0738.PNG patch | view | blame | history
screenshots/IMG_0739.PNG patch | view | blame | history
screenshots/IMG_0740.PNG patch | view | blame | history
screenshots/IMG_0741.PNG patch | view | blame | history
screenshots/IMG_0742.PNG patch | view | blame | history
screenshots/IMG_0743.PNG patch | view | blame | history
screenshots/ipad/ipad_home.png patch | view | blame | history
tasks/PRD-Glidr.md patch | view | blame | history
tasks/research-api-security.md patch | view | blame | history
tasks/research-content-analysis.md patch | view | blame | history
tasks/research-supermemo-sm2.md patch | view | blame | history
tasks/research-tech-stack.md patch | view | blame | history
tasks/restructure_explanations.py patch | view | blame | history
tasks/todo.md patch | view | blame | history
xcode_initial.png patch | view | blame | history
.gitignore
....@@ -0,0 +1,5 @@
1
+.DS_Store
2
+.obsidian
3
+__pycache__
4
+*.pyc
5
+
Glidr.md
....@@ -0,0 +1,6 @@
1
+---
2
+icloud-sync: true
3
+icloud-sync-exclude:
4
+ - "app/"
5
+---
6
+
SPL Exam Questions FR/figures/t10_q114 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q70 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q77 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q88 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q94 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q103 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q87 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q90 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q96 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q19 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q20 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q21 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q27 2.svg
deleted file mode 100644
....@@ -1,73 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8"?>
2
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
3
- <rect width="600" height="250" fill="white"/>
4
-
5
- <!-- Title -->
6
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Obstacles</text>
7
-
8
- <!-- ===== A) Single lighted obstacle ===== -->
9
- <g transform="translate(75, 125)">
10
- <!-- Filled circle (base) -->
11
- <circle cx="0" cy="20" r="8" fill="black"/>
12
- <!-- Light rays (star) -->
13
- <line x1="0" y1="-5" x2="0" y2="-18" stroke="black" stroke-width="1.5"/>
14
- <line x1="9" y1="0" x2="18" y2="-6" stroke="black" stroke-width="1.5"/>
15
- <line x1="-9" y1="0" x2="-18" y2="-6" stroke="black" stroke-width="1.5"/>
16
- <line x1="6" y1="-9" x2="13" y2="-18" stroke="black" stroke-width="1.5"/>
17
- <line x1="-6" y1="-9" x2="-13" y2="-18" stroke="black" stroke-width="1.5"/>
18
- <line x1="9" y1="-5" x2="18" y2="-10" stroke="black" stroke-width="1.5"/>
19
- <line x1="-9" y1="-5" x2="-18" y2="-10" stroke="black" stroke-width="1.5"/>
20
- <!-- Label -->
21
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
22
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Single lighted</text>
23
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacle</text>
24
- </g>
25
-
26
- <!-- ===== B) Single unlighted obstacle ===== -->
27
- <g transform="translate(225, 125)">
28
- <!-- Filled circle (base) -->
29
- <circle cx="0" cy="20" r="8" fill="black"/>
30
- <!-- Label -->
31
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
32
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Single unlighted</text>
33
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacle</text>
34
- </g>
35
-
36
- <!-- ===== C) Group of lighted obstacles ===== -->
37
- <g transform="translate(375, 125)">
38
- <!-- Two filled circles side by side -->
39
- <circle cx="-12" cy="20" r="7" fill="black"/>
40
- <circle cx="12" cy="20" r="7" fill="black"/>
41
- <!-- Light rays above center -->
42
- <line x1="0" y1="-2" x2="0" y2="-16" stroke="black" stroke-width="1.5"/>
43
- <line x1="9" y1="2" x2="18" y2="-4" stroke="black" stroke-width="1.5"/>
44
- <line x1="-9" y1="2" x2="-18" y2="-4" stroke="black" stroke-width="1.5"/>
45
- <line x1="6" y1="-7" x2="13" y2="-16" stroke="black" stroke-width="1.5"/>
46
- <line x1="-6" y1="-7" x2="-13" y2="-16" stroke="black" stroke-width="1.5"/>
47
- <line x1="9" y1="-3" x2="18" y2="-8" stroke="black" stroke-width="1.5"/>
48
- <line x1="-9" y1="-3" x2="-18" y2="-8" stroke="black" stroke-width="1.5"/>
49
- <!-- Label -->
50
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
51
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Group of lighted</text>
52
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacles</text>
53
- </g>
54
-
55
- <!-- ===== D) Group of unlighted obstacles ===== -->
56
- <g transform="translate(525, 125)">
57
- <!-- Two filled circles side by side -->
58
- <circle cx="-12" cy="20" r="7" fill="black"/>
59
- <circle cx="12" cy="20" r="7" fill="black"/>
60
- <!-- Label -->
61
- <text x="0" y="48" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
62
- <text x="0" y="63" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Group of unlighted</text>
63
- <text x="0" y="76" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">obstacles</text>
64
- </g>
65
-
66
- <!-- Dividers -->
67
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
68
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
69
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
70
-
71
- <!-- Border -->
72
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
73
-</svg>
SPL Exam Questions FR/figures/t30_q28 2.svg
deleted file mode 100644
....@@ -1,64 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8"?>
2
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
3
- <rect width="600" height="250" fill="white"/>
4
-
5
- <!-- Title -->
6
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Airports</text>
7
-
8
- <!-- ===== A) Civil airport with paved runway ===== -->
9
- <g transform="translate(75, 120)">
10
- <!-- Circle -->
11
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
12
- <!-- Runway line through center (horizontal) -->
13
- <rect x="-5" y="-22" width="10" height="44" fill="black" rx="2"/>
14
- <!-- Label -->
15
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
16
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Civil airport</text>
17
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">paved runway</text>
18
- </g>
19
-
20
- <!-- ===== B) Military airport ===== -->
21
- <g transform="translate(225, 120)">
22
- <!-- Circle with flag/military cross -->
23
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
24
- <!-- Runway line -->
25
- <rect x="-5" y="-22" width="10" height="44" fill="black" rx="2"/>
26
- <!-- Military crossbar (shorter horizontal bar across runway) -->
27
- <rect x="-18" y="-4" width="36" height="8" fill="black" rx="1"/>
28
- <!-- Label -->
29
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
30
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Military airport</text>
31
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">paved runway</text>
32
- </g>
33
-
34
- <!-- ===== C) Civil airport with unpaved runway ===== -->
35
- <g transform="translate(375, 120)">
36
- <!-- Circle only, no fill runway bar -->
37
- <circle cx="0" cy="0" r="18" fill="none" stroke="black" stroke-width="2"/>
38
- <!-- Runway line (open/outline style to show unpaved) -->
39
- <rect x="-5" y="-22" width="10" height="44" fill="none" stroke="black" stroke-width="2" rx="2"/>
40
- <!-- Label -->
41
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
42
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Civil airport</text>
43
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">unpaved runway</text>
44
- </g>
45
-
46
- <!-- ===== D) Heliport ===== -->
47
- <g transform="translate(525, 120)">
48
- <!-- Square with H -->
49
- <rect x="-20" y="-20" width="40" height="40" fill="none" stroke="black" stroke-width="2"/>
50
- <text x="0" y="8" font-family="Arial, sans-serif" font-size="24" font-weight="bold" text-anchor="middle" fill="black">H</text>
51
- <!-- Label -->
52
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
53
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Heliport</text>
54
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black"> </text>
55
- </g>
56
-
57
- <!-- Dividers -->
58
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
59
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
60
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
61
-
62
- <!-- Border -->
63
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
64
-</svg>
SPL Exam Questions FR/figures/t30_q29 2.svg
deleted file mode 100644
....@@ -1,67 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8"?>
2
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 250" width="600" height="250">
3
- <rect width="600" height="250" fill="white"/>
4
-
5
- <!-- Title -->
6
- <text x="300" y="28" font-family="Arial, sans-serif" font-size="16" font-weight="bold" text-anchor="middle" fill="black">ICAO Chart Symbols — Spot Elevations</text>
7
-
8
- <!-- ===== A) General spot elevation ===== -->
9
- <g transform="translate(75, 120)">
10
- <!-- Small dot -->
11
- <circle cx="0" cy="0" r="3" fill="black"/>
12
- <!-- Elevation number next to dot -->
13
- <text x="10" y="5" font-family="Arial, sans-serif" font-size="14" fill="black">1234</text>
14
- <!-- Label -->
15
- <text x="20" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">A)</text>
16
- <text x="20" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">General spot</text>
17
- <text x="20" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">elevation</text>
18
- </g>
19
-
20
- <!-- ===== B) Highest spot elevation on chart ===== -->
21
- <g transform="translate(225, 120)">
22
- <!-- Larger bold dot -->
23
- <circle cx="0" cy="0" r="5" fill="black"/>
24
- <!-- Bold elevation number -->
25
- <text x="10" y="6" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="black">4808</text>
26
- <!-- Underline to indicate highest -->
27
- <line x1="10" y1="10" x2="54" y2="10" stroke="black" stroke-width="1.5"/>
28
- <!-- Label -->
29
- <text x="25" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">B)</text>
30
- <text x="25" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Highest spot</text>
31
- <text x="25" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">elevation on chart</text>
32
- </g>
33
-
34
- <!-- ===== C) Mountain peak / summit (filled triangle) ===== -->
35
- <g transform="translate(390, 120)">
36
- <!-- Filled triangle pointing up -->
37
- <polygon points="0,-22 -16,12 16,12" fill="black"/>
38
- <!-- Elevation number -->
39
- <text x="22" y="-10" font-family="Arial, sans-serif" font-size="13" fill="black">2962</text>
40
- <!-- Label -->
41
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">C)</text>
42
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Mountain peak</text>
43
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">/ summit</text>
44
- </g>
45
-
46
- <!-- ===== D) Trigonometric point ===== -->
47
- <g transform="translate(530, 120)">
48
- <!-- Open triangle -->
49
- <polygon points="0,-22 -16,12 16,12" fill="none" stroke="black" stroke-width="2"/>
50
- <!-- Dot in center -->
51
- <circle cx="0" cy="3" r="3" fill="black"/>
52
- <!-- Elevation number -->
53
- <text x="22" y="-10" font-family="Arial, sans-serif" font-size="13" fill="black">1543</text>
54
- <!-- Label -->
55
- <text x="0" y="38" font-family="Arial, sans-serif" font-size="13" font-weight="bold" text-anchor="middle" fill="black">D)</text>
56
- <text x="0" y="53" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">Trigonometric</text>
57
- <text x="0" y="66" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="black">point</text>
58
- </g>
59
-
60
- <!-- Dividers -->
61
- <line x1="150" y1="50" x2="150" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
62
- <line x1="300" y1="50" x2="300" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
63
- <line x1="450" y1="50" x2="450" y2="220" stroke="#cccccc" stroke-width="1" stroke-dasharray="4,4"/>
64
-
65
- <!-- Border -->
66
- <rect width="598" height="248" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
67
-</svg>
SPL Exam Questions FR/figures/t30_q36 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q40 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q41 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q44 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q46 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q47 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q57 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q58 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q59 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q61 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q62 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q63 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q68 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q69 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q72 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q75 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q77 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q80 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q81 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q82 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q83 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q88 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q91 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q92 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q93 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q94 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q95 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q96 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q111 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q114 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q48 2.svg
deleted file mode 100644
....@@ -1,102 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8"?>
2
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 350" width="500" height="350" style="background:white; font-family: Arial, sans-serif;">
3
-
4
- <defs>
5
- <marker id="arrowAxis" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
6
- <polygon points="0,0 10,3.5 0,7" fill="black"/>
7
- </marker>
8
- </defs>
9
-
10
- <!-- Axes -->
11
- <!-- Y axis (Performance) -->
12
- <line x1="70" y1="290" x2="70" y2="30" stroke="black" stroke-width="2" marker-end="url(#arrowAxis)"/>
13
- <!-- X axis (Arousal) -->
14
- <line x1="70" y1="290" x2="460" y2="290" stroke="black" stroke-width="2" marker-end="url(#arrowAxis)"/>
15
-
16
- <!-- Axis labels -->
17
- <text x="250" y="325" text-anchor="middle" font-size="15" font-weight="bold" fill="black">A (Arousal / Stress)</text>
18
- <!-- Y-axis label (rotated) -->
19
- <text x="22" y="165" text-anchor="middle" font-size="15" font-weight="bold" fill="black"
20
- transform="rotate(-90, 22, 165)">P (Performance)</text>
21
-
22
- <!-- Inverted-U curve
23
- X range: 70 to 450 (arousal: low to high)
24
- Y range: 290 (low) to 50 (high performance)
25
- Peak at arousal midpoint ~x=260, y=55
26
- A: (90, 270) low arousal, low performance
27
- B: (260, 55) peak
28
- C: (360, 140) high arousal, declining
29
- D: (430, 270) very high, very low
30
-
31
- Bezier: from A(90,270) through B(260,55) to D(430,270)
32
- Control points to create smooth inverted-U:
33
- CP1: (155, 55) pulling curve up
34
- CP2: (355, 55) holding it up then falling
35
- -->
36
- <path d="M 90,270 C 155,55 355,55 430,270"
37
- fill="none" stroke="#2255aa" stroke-width="3"/>
38
-
39
- <!-- Shaded zone around peak (optimal performance zone) -->
40
- <!-- Light band between x=200 and x=320 -->
41
- <path d="M 200,290 L 200,78 C 225,60 295,60 320,78 L 320,290 Z"
42
- fill="#e8f0ff" stroke="none" opacity="0.5"/>
43
-
44
- <!-- Point A: low arousal, low performance -->
45
- <!-- On curve at x=90: y=270 -->
46
- <circle cx="90" cy="270" r="7" fill="#c00" stroke="black" stroke-width="1.5"/>
47
- <text x="70" y="265" text-anchor="end" font-size="14" font-weight="bold" fill="#c00">A</text>
48
- <text x="55" y="248" text-anchor="middle" font-size="11" fill="#444">Low arousal,</text>
49
- <text x="55" y="261" text-anchor="middle" font-size="11" fill="#444">low performance</text>
50
-
51
- <!-- Point B: optimal, peak performance -->
52
- <!-- On curve at x=260, peak: y ~ 55 + small deviation from bezier calc -->
53
- <!-- At t=0.5 for cubic bezier A(90,270) CP1(155,55) CP2(355,55) D(430,270):
54
- x = (1-t)^3*90 + 3(1-t)^2*t*155 + 3(1-t)*t^2*355 + t^3*430
55
- = 0.125*90 + 0.375*155 + 0.375*355 + 0.125*430
56
- = 11.25 + 58.125 + 133.125 + 53.75 = 256.25
57
- y = 0.125*270 + 0.375*55 + 0.375*55 + 0.125*270
58
- = 33.75 + 20.625 + 20.625 + 33.75 = 108.75
59
- Hmm, mid-bezier y=109, not 55. The peak is NOT at t=0.5 for this bezier.
60
- The actual peak (minimum y) is at the top of the curve.
61
- Since CP1.y = CP2.y = 55, and A.y=D.y=270, the peak of the curve is AT y=55.
62
- The x-midpoint of control points: (155+355)/2 = 255. So peak is around x=255, y close to 55. -->
63
- <!-- Let's just use x=258, y=57 for point B (approximately correct) -->
64
- <circle cx="258" cy="62" r="7" fill="#007700" stroke="black" stroke-width="1.5"/>
65
- <text x="258" y="50" text-anchor="middle" font-size="14" font-weight="bold" fill="#007700">B</text>
66
- <text x="258" y="35" text-anchor="middle" font-size="12" fill="#007700" font-weight="bold">Optimal</text>
67
- <text x="258" y="18" text-anchor="middle" font-size="11" fill="#444">Peak performance</text>
68
-
69
- <!-- Point C: high arousal, declining -->
70
- <!-- Approximate on curve: x=360 -->
71
- <!-- t such that x=360:
72
- 90(1-t)^3 + 3*155(1-t)^2*t + 3*355(1-t)*t^2 + 430*t^3 = 360
73
- Rough estimate: t~0.73 gives x~360
74
- y at t=0.73: 0.0219*270 + 3*0.0729*0.73*55 + 3*0.27*0.5329*55 + 0.389*270
75
- = 5.9 + 8.8 + 23.8 + 105 = 143.5 ≈ 144 -->
76
- <circle cx="362" cy="144" r="7" fill="#e87000" stroke="black" stroke-width="1.5"/>
77
- <text x="375" y="140" text-anchor="start" font-size="14" font-weight="bold" fill="#e87000">C</text>
78
- <text x="390" y="125" text-anchor="middle" font-size="11" fill="#444">High arousal,</text>
79
- <text x="390" y="138" text-anchor="middle" font-size="11" fill="#444">declining</text>
80
-
81
- <!-- Point D: very high arousal, very low performance -->
82
- <circle cx="430" cy="270" r="7" fill="#c00" stroke="black" stroke-width="1.5"/>
83
- <text x="445" y="268" text-anchor="start" font-size="14" font-weight="bold" fill="#c00">D</text>
84
- <text x="445" y="285" text-anchor="start" font-size="11" fill="#444">Very low</text>
85
- <text x="445" y="298" text-anchor="start" font-size="11" fill="#444">performance</text>
86
-
87
- <!-- Axis tick labels -->
88
- <text x="65" y="295" text-anchor="end" font-size="11" fill="#666">Low</text>
89
- <text x="455" y="295" text-anchor="end" font-size="11" fill="#666">High</text>
90
- <text x="65" y="295" text-anchor="end" font-size="11" fill="#666">Low</text>
91
-
92
- <!-- Y axis: Low at bottom, High at top -->
93
- <text x="65" y="290" text-anchor="end" font-size="11" fill="#666">Low</text>
94
- <text x="65" y="50" text-anchor="end" font-size="11" fill="#666">High</text>
95
-
96
- <!-- Optimal zone label -->
97
- <text x="260" y="215" text-anchor="middle" font-size="11" fill="#2255aa" font-style="italic">Optimal zone</text>
98
-
99
- <!-- Title -->
100
- <text x="250" y="345" text-anchor="middle" font-size="14" font-weight="bold" fill="black">Yerkes-Dodson Curve</text>
101
-
102
-</svg>
SPL Exam Questions FR/figures/t40_q49 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q101 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q103 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q107 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q129 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q131 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q145 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q162 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q179 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q182 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q200 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q57 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q61 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q65 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q66 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q71 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q73 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q76 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q77 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q82 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q90 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q92 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q93 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q153 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q161 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q164 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q167 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q171 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q46 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q6 2.svg
deleted file mode 100644
....@@ -1,82 +0,0 @@
1
-<?xml version="1.0" encoding="UTF-8"?>
2
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
3
- <rect width="400" height="400" fill="white"/>
4
-
5
- <!-- Clip path for globe interior -->
6
- <defs>
7
- <clipPath id="globeClip">
8
- <circle cx="200" cy="200" r="150"/>
9
- </clipPath>
10
- </defs>
11
-
12
- <!-- Globe fill (light blue) -->
13
- <circle cx="200" cy="200" r="150" fill="#e8f4fc" stroke="black" stroke-width="2"/>
14
-
15
- <!-- Latitude lines (clipped to globe) -->
16
- <!-- 60N -->
17
- <ellipse cx="200" cy="125" rx="130" ry="20" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
18
- <!-- 30N -->
19
- <ellipse cx="200" cy="162" rx="150" ry="28" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
20
- <!-- Equator (0) — drawn separately, bold -->
21
- <ellipse cx="200" cy="200" rx="150" ry="32" fill="none" stroke="black" stroke-width="2" clip-path="url(#globeClip)"/>
22
- <!-- 30S -->
23
- <ellipse cx="200" cy="238" rx="150" ry="28" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
24
- <!-- 60S -->
25
- <ellipse cx="200" cy="275" rx="130" ry="20" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
26
-
27
- <!-- Longitude lines (meridians) — vertical ellipses, clipped -->
28
- <!-- Prime meridian (0°) -->
29
- <ellipse cx="200" cy="200" rx="10" ry="150" fill="none" stroke="black" stroke-width="1.5" clip-path="url(#globeClip)"/>
30
- <!-- 30W / 150E -->
31
- <ellipse cx="200" cy="200" rx="75" ry="150" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
32
- <!-- 60W / 120E -->
33
- <ellipse cx="200" cy="200" rx="130" ry="150" fill="none" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
34
- <!-- 90W / 90E — just the axis line -->
35
- <line x1="200" y1="50" x2="200" y2="350" stroke="#aaaaaa" stroke-width="0.8" clip-path="url(#globeClip)"/>
36
-
37
- <!-- Globe outer border (drawn again on top to clean up edges) -->
38
- <circle cx="200" cy="200" r="150" fill="none" stroke="black" stroke-width="2"/>
39
-
40
- <!-- North / South pole dots -->
41
- <circle cx="200" cy="50" r="3" fill="black"/>
42
- <circle cx="200" cy="350" r="3" fill="black"/>
43
-
44
- <!-- Pole labels -->
45
- <text x="200" y="38" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="black">North Pole</text>
46
- <text x="200" y="370" font-family="Arial, sans-serif" font-size="14" font-weight="bold" text-anchor="middle" fill="black">South Pole</text>
47
-
48
- <!-- Equator label -->
49
- <text x="362" y="204" font-family="Arial, sans-serif" font-size="12" text-anchor="start" fill="black">Equator</text>
50
- <line x1="350" y1="200" x2="362" y2="202" stroke="black" stroke-width="1"/>
51
-
52
- <!-- Equator circumference annotation -->
53
- <text x="200" y="245" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#333333">≈ 40,075 km / ≈ 21,600 NM</text>
54
-
55
- <!-- Axis line (N-S, dashed) -->
56
- <line x1="200" y1="50" x2="200" y2="350" stroke="#555555" stroke-width="1" stroke-dasharray="6,4"/>
57
-
58
- <!-- Latitude label 30N -->
59
- <text x="356" y="165" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">30°N</text>
60
- <line x1="349" y1="162" x2="356" y2="163" stroke="#555555" stroke-width="0.8"/>
61
-
62
- <!-- Latitude label 60N -->
63
- <text x="338" y="128" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">60°N</text>
64
- <line x1="330" y1="125" x2="338" y2="126" stroke="#555555" stroke-width="0.8"/>
65
-
66
- <!-- Latitude label 30S -->
67
- <text x="356" y="241" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">30°S</text>
68
- <line x1="349" y1="238" x2="356" y2="239" stroke="#555555" stroke-width="0.8"/>
69
-
70
- <!-- Latitude label 60S -->
71
- <text x="338" y="278" font-family="Arial, sans-serif" font-size="11" text-anchor="start" fill="#555555">60°S</text>
72
- <line x1="330" y1="275" x2="338" y2="276" stroke="#555555" stroke-width="0.8"/>
73
-
74
- <!-- Prime meridian label -->
75
- <text x="200" y="395" font-family="Arial, sans-serif" font-size="11" text-anchor="middle" fill="#555555">0° / Prime Meridian</text>
76
-
77
- <!-- Title -->
78
- <text x="200" y="20" font-family="Arial, sans-serif" font-size="15" font-weight="bold" text-anchor="middle" fill="black">Earth — Latitude and Longitude</text>
79
-
80
- <!-- Border -->
81
- <rect width="398" height="398" x="1" y="1" fill="none" stroke="#333333" stroke-width="1"/>
82
-</svg>
SPL Exam Questions FR/figures/t80_q102 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q111 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q112 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q113 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q123 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q129 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q132 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q150 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q151 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q152 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q66 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q75 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q87 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q90 2.png
deleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q95 2.png
deleted file mode 100644Binary files differ
app/glidr
....@@ -0,0 +1 @@
1
+/Users/i052341/dev/apps/glidr
articles/how-it-works.md
....@@ -0,0 +1,67 @@
1
+# How Glidr Helps You Learn
2
+
3
+## The Problem with Cramming
4
+
5
+German psychologist Hermann Ebbinghaus measured in 1885 that without reinforcement, your brain discards roughly half of new information within 30 minutes, and up to 80% within 24 hours.
6
+
7
+For your SPL theory exam, forgetting is not just inconvenient - it is dangerous. You need knowledge that stays with you in the cockpit, not just until the test.
8
+
9
+## The Solution: Spaced Repetition
10
+
11
+Spaced repetition works by showing you a card just before you would forget it. Each time you successfully recall something, the next review is pushed further into the future. Each successful recall also strengthens the memory trace, making it harder to lose.
12
+
13
+The result: you spend less time reviewing things you already know well, and more time on the things that actually need attention.
14
+
15
+## The SM-2 Algorithm
16
+
17
+In 1987, Polish researcher Piotr Wozniak created SuperMemo, the first program to automate this process. His algorithm, SM-2, became the foundation for nearly every modern flashcard app - including Glidr.
18
+
19
+Every card has a personal interval (days until it appears again) and an easiness factor (how quickly that interval grows). Your ratings update both values, so the schedule adapts to you.
20
+
21
+## How Your Rating Controls Everything
22
+
23
+After each card, you rate how well you recalled the answer:
24
+
25
+- **Again** - did not remember. Card resets to tomorrow.
26
+- **Hard** - remembered, but a real struggle. Returns soon.
27
+- **Good** - remembered with normal effort. Standard growth.
28
+- **Easy** - instant recall. Interval jumps forward.
29
+
30
+Be honest. Rating "Easy" when you hesitated tells the algorithm to wait longer than it should - and you will have forgotten it by the time it returns.
31
+
32
+## The Spacing Effect
33
+
34
+A card rated "Good" might be scheduled for:
35
+
36
+- First review: 1 day
37
+- Second review: 6 days
38
+- Third review: ~15 days
39
+- Fourth review: ~38 days
40
+
41
+After three or four successful reviews, you may not see a card for months - because it no longer needs reinforcement. That is the algorithm doing exactly what it was designed to do.
42
+
43
+## Study Mode vs Cram Mode
44
+
45
+**Study mode** uses spaced repetition. Cards appear according to the SM-2 schedule - only cards due today, in the order that helps you most. This builds deep, lasting knowledge. Do not skip days if you can avoid it.
46
+
47
+**Cram mode** ignores the schedule entirely. It is useful the night before your exam as a final confidence check, or when previewing a new topic. It does not update intervals, so your long-term schedule is unaffected.
48
+
49
+- Daily sessions: Study Mode
50
+- Night before the exam: Cram Mode
51
+- New chapter preview: Cram, then switch to Study
52
+
53
+## Tips for SPL Exam Prep
54
+
55
+1. **Start early.** Begin 6-8 weeks before your exam to give the algorithm room to space intervals properly.
56
+
57
+2. **Do your due cards every day.** A consistent 10-minute session every morning is far more effective than a 2-hour marathon once a week.
58
+
59
+3. **Aim for 20-30 new cards per day.** Introducing too many creates an overwhelming backlog within a week.
60
+
61
+4. **Use the ratings honestly.** There is no score - no one sees your ratings. Admit when you struggled.
62
+
63
+5. **Trust the schedule.** If a card is not due, reviewing early does not help and disrupts the spacing that makes the system work.
64
+
65
+Good luck, and good soaring.
66
+
67
+*Based on the SM-2 algorithm by Dr. Piotr Wozniak (1987) - [supermemo.com](https://supermemo.com)*
articles/supermemo-wired.md
....@@ -0,0 +1,91 @@
1
+# Want to Remember Everything You'll Ever Learn? Surrender to This Algorithm
2
+
3
+*By Gary Wolf, Wired Magazine, April 2008*
4
+
5
+## The Man Behind SuperMemo
6
+
7
+The winter sun sets in mid-afternoon in Kolobrzeg, Poland, but the early twilight does not deter people from taking their regular outdoor promenade. Bundled up in parkas with fur-trimmed hoods, strolling hand in mittened hand along the edge of the Baltic Sea, off-season tourists from Germany stop openmouthed when they see a tall, well-built, nearly naked man running up and down the sand.
8
+
9
+"Kalt? Kalt?" one of them calls out. The man gives a polite but vague answer, then turns and dives into the waves. After swimming back and forth in the 40-degree water for a few minutes, he emerges from the surf and jogs briefly along the shore. The wind is strong, but the man makes no move to get dressed.
10
+
11
+Piotr Wozniak's quest for anonymity has been successful. Nobody along this string of little beach resorts recognizes him as the inventor of a technique to turn people into geniuses. A portion of this technique, embodied in a software program called SuperMemo, has enthusiastic users around the world. They apply it mainly to learning languages, and it's popular among people for whom fluency is a necessity - students from Poland or other poor countries aiming to score well enough on English-language exams to study abroad.
12
+
13
+## The Core Insight: Timing Is Everything
14
+
15
+SuperMemo is based on the insight that there is an ideal moment to practice what you've learned. Practice too soon and you waste your time. Practice too late and you've forgotten the material and have to relearn it. The right time to practice is just at the moment you're about to forget. Unfortunately, this moment is different for every person and each bit of information.
16
+
17
+Fortunately, human forgetting follows a pattern. We forget exponentially. A graph of our likelihood of getting the correct answer on a quiz sweeps quickly downward over time and then levels off. This pattern has long been known to cognitive psychology, but it has been difficult to put to practical use. It's too complex for us to employ with our naked brains.
18
+
19
+Twenty years ago, Wozniak realized that computers could easily calculate the moment of forgetting if he could discover the right algorithm. SuperMemo is the result of his research. It predicts the future state of a person's memory and schedules information reviews at the optimal time.
20
+
21
+## How SuperMemo Works
22
+
23
+SuperMemo is a program that keeps track of discrete bits of information you've learned and want to retain. Your chance of recalling a given word when you need it declines over time according to a predictable pattern. SuperMemo tracks this so-called forgetting curve and reminds you to rehearse your knowledge when your chance of recalling it has dropped to, say, 90 percent. When you first learn a new vocabulary word, your chance of recalling it will drop quickly. But after SuperMemo reminds you of the word, the rate of forgetting levels out. The program tracks this new decline and waits longer to quiz you the next time.
24
+
25
+## Ebbinghaus and the Forgetting Curve
26
+
27
+In the late 1800s, a German scientist named Hermann Ebbinghaus made up lists of nonsense syllables and measured how long it took to forget and then relearn them. In experiments of breathtaking rigor and tedium, Ebbinghaus practiced and recited from memory 2.5 nonsense syllables a second, then rested for a bit and started again. Finally, in 1885, he published a monograph called *Memory: A Contribution to Experimental Psychology*. The book became the founding classic of a new discipline.
28
+
29
+Ebbinghaus discovered many lawlike regularities of mental life. He was the first to draw a learning curve. Among his original observations was an account of a strange phenomenon: the spacing effect.
30
+
31
+## The Spacing Effect: Psychology's Best-Kept Secret
32
+
33
+Ebbinghaus showed that it's possible to dramatically improve learning by correctly spacing practice sessions. The efficiencies created by precise spacing are so large, and the improvement in performance so predictable, that from nearly the moment Ebbinghaus described the spacing effect, psychologists have been urging educators to use it to accelerate human progress.
34
+
35
+The spacing effect is "one of the most remarkable phenomena to emerge from laboratory research on learning," the psychologist Frank Dempster wrote in 1988, in a paper titled "The Spacing Effect: A Case Study in the Failure to Apply the Results of Psychological Research." How would computer scientists feel if people continued to use slide rules for engineering calculations? Psychologists who studied the spacing effect thought they possessed a solution to a problem that had frustrated humankind since before written language: how to remember what's been learned.
36
+
37
+## Wozniak's Quest: From Paper Cards to Algorithm
38
+
39
+As a student at the Poznan University of Technology in western Poland in the 1980s, Wozniak was overwhelmed by the sheer number of things he was expected to learn. He wasn't just trying to pass his exams; he was trying to learn. He couldn't help noticing that within a few months of completing a class, only a fraction of the knowledge he had so painfully acquired remained in his mind.
40
+
41
+So he created an analog database, with each entry consisting of a question and answer on a piece of paper. Every time he reviewed a word, phrase, or fact, he meticulously noted the date and marked whether he had forgotten it. By 1984, Wozniak's database contained 3,000 English words and phrases and 1,400 facts culled from biology, each with a complete repetition history.
42
+
43
+## The Impossible Math of Memorization
44
+
45
+According to Wozniak's first calculations, success was impossible. The problem wasn't learning the material; it was retaining it. He found that 40 percent of his English vocabulary vanished over time. Sixty percent of his biology answers evaporated. Using some simple calculations, he figured out that with his normal method of study, it would require two hours of practice every day to learn and retain a modest English vocabulary of 15,000 words.
46
+
47
+As Wozniak later wrote: "The process of increasing the size of my databases gradually progressed at the cost of knowledge retention." In other words, as his list grew, so did his forgetting. He was climbing a mountain of loose gravel and making less and less progress at each step.
48
+
49
+## Why Memorization Matters
50
+
51
+"The people who criticize memorization - how happy would they be to spell out every letter of every word they read?" asks Robert Bjork, chair of UCLA's psychology department and one of the most eminent memory researchers. "You can't escape memorization. There is an initial process of learning the names of things. That's a stage we all go through. It's all the more important to go through it rapidly."
52
+
53
+## Retrieval Strength vs Storage Strength
54
+
55
+Long-term memory, the Bjorks said, can be characterized by two components: retrieval strength and storage strength. Retrieval strength measures how likely you are to recall something right now. Storage strength measures how deeply the memory is rooted.
56
+
57
+Some memories may have high storage strength but low retrieval strength. Take an old address or phone number. Try to think of it; you may feel that it's gone. But a single reminder could be enough to restore it for months or years. Conversely, some memories have high retrieval strength but low storage strength - easily accessible now but likely forgotten in days.
58
+
59
+The amount of storage strength you gain from practice is inversely correlated with the current retrieval strength. In other words, the harder you have to work to get the right answer, the more the answer is sealed in memory. Precisely those things that seem to signal we're learning well - easy performance on drills, fluency during a lesson - are misleading when it comes to predicting whether we will remember it in the future.
60
+
61
+## The Optimal Moment to Study
62
+
63
+Robert Bjork, working with Thomas Landauer of Bell Labs, published results involving nearly 700 undergraduate students. They were looking for the optimal moment to rehearse something so that it would later be remembered. Their results were impressive: **The best time to study something is at the moment you are about to forget it.**
64
+
65
+Obviously, computers were the answer. What was needed was not an academic psychologist but a tinkerer, somebody with a lot of time on his hands, a talent for mathematics, and a strangely literal temperament that made him think he should actually recall the things he learned.
66
+
67
+## From Punch Cards to Personal Computers
68
+
69
+All of Wozniak's early work was done on paper. "We had a single mainframe of Polish-Russian design, with punch cards," he recalls. The personal computer revolution was already far along in the US by the time Wozniak managed to get his hands on an Amstrad PC 1512, imported through quasi-legal means from Hamburg, Germany. With this he was able to compute the difficulty of any fact or study item and adjust the predicted forgetting curve for every item and user.
70
+
71
+After the collapse of Polish communism, Wozniak and some fellow students formed a company, SuperMemo World. By 1995, their program became the first Polish product shown at Comdex in Las Vegas.
72
+
73
+## The Algorithmic Life
74
+
75
+The reason the inventor of SuperMemo pursues extreme anonymity is not because he's paranoid but because he wants to avoid random interruptions to a long-running experiment he's conducting on himself. Wozniak is a kind of algorithmic man. He's exploring what it's like to live in strict obedience to reason.
76
+
77
+His days are blocked into distinct periods: a creative period, a reading and studying period, an exercise period, an eating period, a resting period, and then a second creative period. He doesn't get up at a regular hour and is passionate against alarm clocks.
78
+
79
+## Incremental Reading: Beyond Flashcards
80
+
81
+Wozniak has invented a way to apply his learning system to unstructured information from books and articles, winnowing written material down to discrete chunks that can be memorized, and then scheduling them for efficient learning. He calls it incremental reading, and it has come to dominate his intellectual life.
82
+
83
+"Once you get the snippets you need," Wozniak says, "your books disappear. They gradually evaporate. They have been translated into knowledge."
84
+
85
+## The Cost of Genius
86
+
87
+Wozniak wrote a checklist describing how to become a genius. His advice was straightforward yet strangely terrible: You must clarify your goals, gain knowledge through spaced repetition, preserve health, work steadily, minimize stress, refuse interruption, and never resist sleep when tired. This should lead to radically improved intelligence and creativity. The only cost: turning your back on every convention of social life.
88
+
89
+It is a severe prescription. And yet, when linked to genuine rewards, even the chilliest of systems can have a certain visceral appeal. By projecting the achievement of extreme memory back along the forgetting curve, by provably linking the distant future - when we will know so much - to the few minutes we devote to studying today, Wozniak has found a way to condition his temperament along with his memory. He is making the future noticeable. He is trying not just to learn many things but to warm the process of learning itself with a draft of utopian ecstasy.
90
+
91
+*Originally published in Wired Magazine, April 21, 2008*
business.md
....@@ -0,0 +1,186 @@
1
+- generic [ref=e1]:
2
+ - banner "App Store Connect" [ref=e3]:
3
+ - generic [ref=e4]:
4
+ - heading "App Store Connect" [level=1] [ref=e20]:
5
+ - link "App Store Connect" [ref=e21] [cursor=pointer]:
6
+ - /url: /
7
+ - navigation "Global" [ref=e7]:
8
+ - list [ref=e23]:
9
+ - listitem [ref=e24]:
10
+ - link "Apps" [ref=e25] [cursor=pointer]:
11
+ - /url: /apps
12
+ - listitem [ref=e26]:
13
+ - link "Analytics" [ref=e27] [cursor=pointer]:
14
+ - /url: /analytics
15
+ - listitem [ref=e28]:
16
+ - link "Trends" [ref=e29] [cursor=pointer]:
17
+ - /url: /trends
18
+ - listitem [ref=e30]:
19
+ - link "Reports" [ref=e31] [cursor=pointer]:
20
+ - /url: /itc/payments_and_financial_reports
21
+ - listitem [ref=e32]:
22
+ - link "Business" [ref=e33] [cursor=pointer]:
23
+ - /url: /business
24
+ - listitem [ref=e34]:
25
+ - link "Users and Access" [ref=e35] [cursor=pointer]:
26
+ - /url: /access/users
27
+ - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:
28
+ - generic:
29
+ - generic: Matthias Nott
30
+ - generic: Matthias Nott
31
+ - img [ref=e38]
32
+ - generic [ref=e46]:
33
+ - generic [ref=e47]:
34
+ - heading "Business" [level=1] [ref=e48]
35
+ - navigation [ref=e49]:
36
+ - listitem [ref=e50] [cursor=pointer]:
37
+ - generic [ref=e51]: Agreements
38
+ - generic [ref=e54]:
39
+ - img [ref=e55]
40
+ - paragraph [ref=e57]:
41
+ - generic [ref=e58]:
42
+ - text: The Apple Developer Program License Agreement has been updated and needs to be reviewed. In order to update your existing apps and submit new apps, the Account Holder must review and accept the updated agreement by signing in to their
43
+ - link "account" [active] [ref=e59] [cursor=pointer]:
44
+ - /url: https://developer.apple.com/account
45
+ - text: .
46
+ - generic [ref=e61]:
47
+ - heading "Matthias Nott" [level=2] [ref=e63]
48
+ - generic [ref=e64]:
49
+ - generic [ref=e65]:
50
+ - paragraph [ref=e66]: ch de la Tarpa 8A
51
+ - paragraph [ref=e67]: Troistorrents, Valais 1872
52
+ - paragraph [ref=e68]: Switzerland
53
+ - generic [ref=e69]:
54
+ - paragraph [ref=e71]: "85427149"
55
+ - generic [ref=e73]:
56
+ - paragraph [ref=e74]: 175 Countries or Regions
57
+ - paragraph [ref=e75]:
58
+ - button "View" [ref=e76] [cursor=pointer]
59
+ - generic [ref=e78]:
60
+ - heading "Agreements" [level=2] [ref=e79]
61
+ - table [ref=e80]:
62
+ - rowgroup [ref=e81]:
63
+ - row "Type Countries or Regions Effective Date Status" [ref=e82]:
64
+ - columnheader "Type" [ref=e83]
65
+ - columnheader "Countries or Regions" [ref=e84]
66
+ - columnheader "Effective Date" [ref=e85]
67
+ - columnheader "Status" [ref=e86]
68
+ - columnheader [ref=e87]
69
+ - rowgroup [ref=e88]:
70
+ - row "Paid Apps Agreement All Countries or Regions View 30 Dec 2025 - 30 Dec 2026 Active" [ref=e89]:
71
+ - cell "Paid Apps Agreement" [ref=e90]
72
+ - cell "All Countries or Regions View" [ref=e91]:
73
+ - generic [ref=e93]:
74
+ - paragraph [ref=e94]: All Countries or Regions
75
+ - paragraph [ref=e95]:
76
+ - button "View" [ref=e96] [cursor=pointer]
77
+ - cell "30 Dec 2025 - 30 Dec 2026" [ref=e97]
78
+ - cell "Active" [ref=e98]
79
+ - cell [ref=e99]:
80
+ - button [ref=e101] [cursor=pointer]:
81
+ - img [ref=e102]
82
+ - row "Free Apps Agreement All Countries or Regions View 30 Dec 2025 - 30 Dec 2026 Active (New Agreement Available)" [ref=e104]:
83
+ - cell "Free Apps Agreement" [ref=e105]
84
+ - cell "All Countries or Regions View" [ref=e106]:
85
+ - generic [ref=e108]:
86
+ - paragraph [ref=e109]: All Countries or Regions
87
+ - paragraph [ref=e110]:
88
+ - button "View" [ref=e111] [cursor=pointer]
89
+ - cell "30 Dec 2025 - 30 Dec 2026" [ref=e112]
90
+ - cell "Active (New Agreement Available)" [ref=e113]
91
+ - cell [ref=e114]
92
+ - generic [ref=e116]:
93
+ - generic [ref=e117]:
94
+ - heading "Bank Accounts" [level=2] [ref=e118]:
95
+ - text: Bank Accounts
96
+ - button [ref=e121] [cursor=pointer]:
97
+ - img [ref=e122]
98
+ - paragraph [ref=e124]:
99
+ - button "See More" [ref=e125] [cursor=pointer]
100
+ - table [ref=e126]:
101
+ - rowgroup [ref=e127]:
102
+ - row "Account Country or Region Bank Currency Royalty Currencies Status" [ref=e128]:
103
+ - columnheader "Account" [ref=e129]
104
+ - columnheader "Country or Region" [ref=e130]
105
+ - columnheader "Bank Currency" [ref=e131]
106
+ - columnheader "Royalty Currencies" [ref=e132]
107
+ - columnheader "Status" [ref=e133]
108
+ - columnheader [ref=e134]
109
+ - rowgroup [ref=e135]:
110
+ - row "UBS4000 (840L) Switzerland CHF USD Active" [ref=e136]:
111
+ - cell "UBS4000 (840L)" [ref=e137]:
112
+ - button "UBS4000 (840L)" [ref=e138] [cursor=pointer]
113
+ - cell "Switzerland" [ref=e139]
114
+ - cell "CHF" [ref=e140]
115
+ - cell "USD" [ref=e141]:
116
+ - paragraph [ref=e142]: USD
117
+ - cell "Active" [ref=e143]
118
+ - cell [ref=e144]:
119
+ - button [ref=e147] [cursor=pointer]:
120
+ - img [ref=e148]
121
+ - generic [ref=e151]:
122
+ - heading "Tax Forms" [level=2] [ref=e152]:
123
+ - text: Tax Forms
124
+ - button [ref=e153] [cursor=pointer]:
125
+ - img [ref=e154]
126
+ - table [ref=e156]:
127
+ - rowgroup [ref=e157]:
128
+ - row "Tax Form Nickname Date Submitted Status" [ref=e158]:
129
+ - columnheader "Tax Form" [ref=e159]
130
+ - columnheader "Nickname" [ref=e160]
131
+ - columnheader "Date Submitted" [ref=e161]
132
+ - columnheader "Status" [ref=e162]
133
+ - columnheader [ref=e163]
134
+ - rowgroup [ref=e164]:
135
+ - row "U.S. Certificate of Foreign Status of Beneficial Owner - 22 Oct 2011 Active" [ref=e165]:
136
+ - cell "U.S. Certificate of Foreign Status of Beneficial Owner" [ref=e166]:
137
+ - button "U.S. Certificate of Foreign Status of Beneficial Owner" [ref=e167] [cursor=pointer]
138
+ - cell "-" [ref=e168]
139
+ - cell "22 Oct 2011" [ref=e169]
140
+ - cell "Active" [ref=e170]:
141
+ - paragraph [ref=e171]:
142
+ - generic [ref=e172]: Active
143
+ - cell [ref=e173]
144
+ - generic [ref=e175]:
145
+ - heading "Compliance" [level=2] [ref=e176]
146
+ - table [ref=e177]:
147
+ - rowgroup [ref=e178]:
148
+ - row "Regulation Countries or Regions Last Updated Status" [ref=e179]:
149
+ - columnheader "Regulation" [ref=e180]
150
+ - columnheader "Countries or Regions" [ref=e181]
151
+ - columnheader "Last Updated" [ref=e182]
152
+ - columnheader "Status" [ref=e183]
153
+ - columnheader [ref=e184]
154
+ - rowgroup [ref=e185]:
155
+ - row "Digital Services Act 27 Countries or Regions View 30 Dec 2025 Active" [ref=e186]:
156
+ - cell "Digital Services Act" [ref=e187]:
157
+ - button "Digital Services Act" [ref=e188] [cursor=pointer]:
158
+ - paragraph [ref=e189]: Digital Services Act
159
+ - cell "27 Countries or Regions View" [ref=e190]:
160
+ - generic [ref=e192]:
161
+ - paragraph [ref=e193]: 27 Countries or Regions
162
+ - paragraph [ref=e194]:
163
+ - button "View" [ref=e195] [cursor=pointer]
164
+ - cell "30 Dec 2025" [ref=e196]
165
+ - cell "Active" [ref=e197]:
166
+ - paragraph [ref=e198]: Active
167
+ - cell [ref=e199]
168
+ - contentinfo [ref=e10]:
169
+ - generic [ref=e11]:
170
+ - list [ref=e200]:
171
+ - listitem [ref=e201]:
172
+ - link "App Store Connect" [ref=e202] [cursor=pointer]:
173
+ - /url: /apps
174
+ - list [ref=e12]:
175
+ - listitem [ref=e13]: Copyright © 2026 Apple Inc. All rights reserved. |
176
+ - listitem [ref=e14]:
177
+ - link "Terms of Service" [ref=e15] [cursor=pointer]:
178
+ - /url: /WebObjects/iTunesConnect.woa/wa/termsOfService
179
+ - text: "|"
180
+ - listitem [ref=e16]:
181
+ - link "Privacy Policy" [ref=e17] [cursor=pointer]:
182
+ - /url: https://www.apple.com/legal/privacy
183
+ - text: "|"
184
+ - listitem [ref=e18]:
185
+ - link "Contact Us" [ref=e19] [cursor=pointer]:
186
+ - /url: /contact-us
fix_explanation_formatting.py
....@@ -0,0 +1,254 @@
1
+#!/usr/bin/env python3
2
+"""
3
+Fix explanation formatting in SPL exam question files.
4
+
5
+Converts parenthetical option references like "(A)" in prose sentences
6
+into bullet points with bolded option references like "**(A)**".
7
+
8
+Pattern:
9
+ "Some intro. La construction métallique (A) utilise des feuilles. La construction (B) utilise..."
10
+Becomes:
11
+ "Some intro.
12
+ - La construction métallique **(A)** utilise des feuilles.
13
+ - La construction **(B)** utilise..."
14
+"""
15
+
16
+import re
17
+import os
18
+import glob
19
+
20
+BASE_DIR = "/Users/i052341/Daten/Cloud/04 - Ablage/Ablage 2020 - 2029/Ablage 2025/Hobbies 2025/Segelflug/Theorie/Glidr"
21
+
22
+# Pattern to detect option references (A), (B), (C), (D)
23
+OPTION_REF_PATTERN = re.compile(r'\([ABCD]\)')
24
+
25
+
26
+def bold_option_refs(text):
27
+ """Replace (A) with **(A)** in text."""
28
+ return re.sub(r'\(([ABCD])\)', r'**(\1)**', text)
29
+
30
+
31
+def sentence_contains_option(sentence):
32
+ """Check if a sentence contains a parenthetical option reference."""
33
+ return bool(OPTION_REF_PATTERN.search(sentence))
34
+
35
+
36
+def split_into_sentences(text):
37
+ """
38
+ Split text into sentences at '. ' boundaries where next sentence
39
+ starts with an uppercase letter (including accented chars).
40
+ """
41
+ parts = re.split(r'(?<=\w)\.\s+(?=[A-ZÀÂÄÈÉÊËÎÏÔÙÛÜÇ])', text)
42
+ return parts
43
+
44
+
45
+def join_sentences(sentences):
46
+ """Join sentences back into a paragraph, adding periods where needed."""
47
+ parts = []
48
+ for s in sentences:
49
+ s = s.strip()
50
+ if not s:
51
+ continue
52
+ if not s.endswith('.'):
53
+ s = s + '.'
54
+ parts.append(s)
55
+ return ' '.join(parts)
56
+
57
+
58
+def process_explanation_text(text):
59
+ """
60
+ Process a block of explanation text (one paragraph / multiple sentences).
61
+
62
+ If the text contains option references in multiple sentences,
63
+ split those into bullets.
64
+
65
+ Returns the processed text as a string (may contain newlines for bullets).
66
+ """
67
+ stripped = text.strip()
68
+
69
+ # Already a bullet - leave it alone
70
+ if stripped.startswith('- ') or stripped.startswith('* '):
71
+ return text
72
+
73
+ # No option references - leave it alone
74
+ if not OPTION_REF_PATTERN.search(text):
75
+ return text
76
+
77
+ # Split into sentences
78
+ sentences = split_into_sentences(stripped)
79
+
80
+ if len(sentences) <= 1:
81
+ # Single sentence - just bold the option refs
82
+ return bold_option_refs(text)
83
+
84
+ # Count how many sentences have option refs
85
+ option_sentence_indices = [i for i, s in enumerate(sentences) if sentence_contains_option(s)]
86
+
87
+ if len(option_sentence_indices) <= 1:
88
+ # Only one sentence has option refs - just bold them inline
89
+ return bold_option_refs(text)
90
+
91
+ # Multiple sentences have option refs - convert them to bullets
92
+ first_opt_idx = option_sentence_indices[0]
93
+ last_opt_idx = option_sentence_indices[-1]
94
+
95
+ intro_sentences = sentences[:first_opt_idx]
96
+ middle_sentences = sentences[first_opt_idx:last_opt_idx + 1]
97
+ outro_sentences = sentences[last_opt_idx + 1:]
98
+
99
+ output_lines = []
100
+
101
+ # Intro as regular text
102
+ if intro_sentences:
103
+ output_lines.append(join_sentences(intro_sentences))
104
+
105
+ # Middle sentences (option-containing and any in between) as bullets
106
+ for s in middle_sentences:
107
+ s_clean = s.strip().rstrip('.')
108
+ bolded = bold_option_refs(s_clean)
109
+ output_lines.append(f'- {bolded}.')
110
+
111
+ # Outro as regular text
112
+ if outro_sentences:
113
+ output_lines.append(join_sentences(outro_sentences))
114
+
115
+ return '\n'.join(output_lines)
116
+
117
+
118
+def process_explanation_block(lines):
119
+ """
120
+ Process a block of lines from an explanation section.
121
+ Groups consecutive non-special lines into paragraphs and processes each.
122
+ """
123
+ result = []
124
+ i = 0
125
+
126
+ while i < len(lines):
127
+ line = lines[i]
128
+
129
+ # Empty line - keep as is
130
+ if not line.strip():
131
+ result.append(line)
132
+ i += 1
133
+ continue
134
+
135
+ # Already a bullet line - keep as is
136
+ if line.strip().startswith('- ') or line.strip().startswith('* '):
137
+ result.append(line)
138
+ i += 1
139
+ continue
140
+
141
+ # Header line - keep as is
142
+ if line.strip().startswith('#'):
143
+ result.append(line)
144
+ i += 1
145
+ continue
146
+
147
+ # Regular text line - collect into a paragraph
148
+ para_lines = []
149
+ while i < len(lines):
150
+ current = lines[i]
151
+ # Stop at empty lines, bullets, or headers
152
+ if not current.strip():
153
+ break
154
+ if current.strip().startswith('- ') or current.strip().startswith('* '):
155
+ break
156
+ if current.strip().startswith('#'):
157
+ break
158
+ para_lines.append(current)
159
+ i += 1
160
+
161
+ if not para_lines:
162
+ i += 1
163
+ continue
164
+
165
+ # Join the paragraph lines and process
166
+ para_text = ' '.join(l.strip() for l in para_lines)
167
+ processed = process_explanation_text(para_text)
168
+
169
+ # Add processed text (may be multiple lines due to bullets)
170
+ result.extend(processed.split('\n'))
171
+
172
+ return result
173
+
174
+
175
+def process_file(filepath):
176
+ """Process a single markdown file, fixing explanation formatting."""
177
+ with open(filepath, 'r', encoding='utf-8') as f:
178
+ content = f.read()
179
+
180
+ lines = content.split('\n')
181
+ result_lines = []
182
+ changes_made = 0
183
+ i = 0
184
+
185
+ while i < len(lines):
186
+ line = lines[i]
187
+
188
+ # Check if this is an explanation header
189
+ if re.match(r'^#### (Explanation|Erklärung|Explication)\s*$', line.strip()):
190
+ result_lines.append(line)
191
+ i += 1
192
+
193
+ # Collect lines until next #### or ### header
194
+ explanation_lines = []
195
+ while i < len(lines):
196
+ current = lines[i]
197
+ if re.match(r'^####? ', current) or re.match(r'^### ', current):
198
+ break
199
+ explanation_lines.append(current)
200
+ i += 1
201
+
202
+ # Process the explanation block
203
+ processed = process_explanation_block(explanation_lines)
204
+
205
+ # Count if there was a change
206
+ if explanation_lines != processed:
207
+ changes_made += 1
208
+
209
+ result_lines.extend(processed)
210
+ else:
211
+ result_lines.append(line)
212
+ i += 1
213
+
214
+ new_content = '\n'.join(result_lines)
215
+
216
+ if new_content != content:
217
+ with open(filepath, 'w', encoding='utf-8') as f:
218
+ f.write(new_content)
219
+
220
+ return changes_made
221
+
222
+
223
+def main():
224
+ """Process all SPL exam question files."""
225
+ patterns = [
226
+ os.path.join(BASE_DIR, "SPL Exam Questions EN", "*.md"),
227
+ os.path.join(BASE_DIR, "SPL Exam Questions DE", "*.md"),
228
+ os.path.join(BASE_DIR, "SPL Exam Questions FR", "*.md"),
229
+ ]
230
+
231
+ total_files = 0
232
+ total_changes = 0
233
+
234
+ for pattern in patterns:
235
+ files = sorted(glob.glob(pattern))
236
+ for filepath in files:
237
+ filename = os.path.basename(filepath)
238
+ # Skip combined index files
239
+ if filename.startswith("SPL Exam Questions"):
240
+ continue
241
+
242
+ changes = process_file(filepath)
243
+ total_files += 1
244
+ total_changes += changes
245
+
246
+ lang_folder = os.path.basename(os.path.dirname(filepath))
247
+ status = f" {changes} explanations converted" if changes > 0 else " (no changes)"
248
+ print(f"[{lang_folder}] {filename}{status}")
249
+
250
+ print(f"\nTotal: {total_files} files processed, {total_changes} explanations converted to bullets")
251
+
252
+
253
+if __name__ == "__main__":
254
+ main()
privacy.md
....@@ -0,0 +1,196 @@
1
+- generic [active] [ref=e1]:
2
+ - banner "App Store Connect" [ref=e3]:
3
+ - generic [ref=e4]:
4
+ - heading "App Store Connect" [level=1] [ref=e20]:
5
+ - link "App Store Connect" [ref=e21] [cursor=pointer]:
6
+ - /url: /
7
+ - navigation "Global" [ref=e7]:
8
+ - list [ref=e23]:
9
+ - listitem [ref=e24]:
10
+ - link "Apps" [ref=e25] [cursor=pointer]:
11
+ - /url: /apps
12
+ - listitem [ref=e26]:
13
+ - link "Analytics" [ref=e27] [cursor=pointer]:
14
+ - /url: /analytics
15
+ - listitem [ref=e28]:
16
+ - link "Trends" [ref=e29] [cursor=pointer]:
17
+ - /url: /trends
18
+ - listitem [ref=e30]:
19
+ - link "Reports" [ref=e31] [cursor=pointer]:
20
+ - /url: /itc/payments_and_financial_reports
21
+ - listitem [ref=e32]:
22
+ - link "Business" [ref=e33] [cursor=pointer]:
23
+ - /url: /business
24
+ - listitem [ref=e34]:
25
+ - link "Users and Access" [ref=e35] [cursor=pointer]:
26
+ - /url: /access/users
27
+ - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:
28
+ - generic:
29
+ - generic: Matthias Nott
30
+ - generic: Matthias Nott
31
+ - img [ref=e38]
32
+ - generic [ref=e43]:
33
+ - button "Apps menu, Glider Pilot, selected" [ref=e48] [cursor=pointer]:
34
+ - generic [ref=e49]:
35
+ - generic "Glider Pilot" [ref=e50]:
36
+ - img "Glider Pilot" [ref=e51]
37
+ - generic [ref=e52]: Glider Pilot
38
+ - img [ref=e54]
39
+ - navigation "Apps" [ref=e57]:
40
+ - list [ref=e58]:
41
+ - listitem [ref=e59]:
42
+ - link "Distribution" [ref=e60] [cursor=pointer]:
43
+ - /url: /apps/6760631689/distribution
44
+ - listitem [ref=e61]:
45
+ - link "TestFlight" [ref=e62] [cursor=pointer]:
46
+ - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/testflight
47
+ - listitem [ref=e63]:
48
+ - link "Xcode Cloud" [ref=e64] [cursor=pointer]:
49
+ - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/ci
50
+ - main [ref=e68]:
51
+ - generic [ref=e73]:
52
+ - navigation "Distribution" [ref=e75]:
53
+ - list [ref=e76]:
54
+ - listitem [ref=e77]:
55
+ - generic [ref=e78]:
56
+ - heading "iOS App" [level=2] [ref=e80]
57
+ - list [ref=e81]:
58
+ - listitem [ref=e82]:
59
+ - link "1.0 Prepare for Submission" [ref=e83] [cursor=pointer]:
60
+ - /url: /apps/6760631689/distribution/ios/version/inflight
61
+ - generic [ref=e84]:
62
+ - img [ref=e85]
63
+ - text: 1.0 Prepare for Submission
64
+ - button "Add Platform" [ref=e87] [cursor=pointer]
65
+ - listitem [ref=e88]:
66
+ - separator [ref=e89]
67
+ - listitem [ref=e90]:
68
+ - heading "General" [level=2] [ref=e92]
69
+ - list [ref=e93]:
70
+ - listitem [ref=e94]:
71
+ - link "App Information" [ref=e95] [cursor=pointer]:
72
+ - /url: /apps/6760631689/distribution/info
73
+ - generic [ref=e96]: App Information
74
+ - listitem [ref=e97]:
75
+ - link "App Review" [ref=e98] [cursor=pointer]:
76
+ - /url: /apps/6760631689/distribution/reviewsubmissions
77
+ - generic [ref=e99]: App Review
78
+ - listitem [ref=e100]:
79
+ - link "History" [ref=e101] [cursor=pointer]:
80
+ - /url: /apps/6760631689/distribution/activity/ios/versions
81
+ - generic [ref=e102]: History
82
+ - listitem [ref=e103]:
83
+ - separator [ref=e104]
84
+ - listitem [ref=e105]:
85
+ - heading "App Store" [level=2] [ref=e107]
86
+ - generic [ref=e108]:
87
+ - heading "Trust & Safety" [level=3] [ref=e110]
88
+ - list [ref=e111]:
89
+ - listitem [ref=e112]:
90
+ - link "App Privacy" [ref=e113] [cursor=pointer]:
91
+ - /url: /apps/6760631689/distribution/privacy
92
+ - generic [ref=e114]: App Privacy
93
+ - listitem [ref=e115]:
94
+ - link "App Accessibility" [ref=e116] [cursor=pointer]:
95
+ - /url: /apps/6760631689/distribution/accessibility
96
+ - generic [ref=e117]: App Accessibility
97
+ - listitem [ref=e118]:
98
+ - link "Ratings and Reviews" [ref=e119] [cursor=pointer]:
99
+ - /url: /apps/6760631689/distribution/ratings/ios
100
+ - generic [ref=e120]: Ratings and Reviews
101
+ - generic [ref=e121]:
102
+ - heading "Growth & Marketing" [level=3] [ref=e123]
103
+ - list [ref=e124]:
104
+ - listitem [ref=e125]:
105
+ - link "In-App Events" [ref=e126] [cursor=pointer]:
106
+ - /url: /apps/6760631689/distribution/events
107
+ - generic [ref=e127]: In-App Events
108
+ - listitem [ref=e128]:
109
+ - link "Custom Product Pages" [ref=e129] [cursor=pointer]:
110
+ - /url: /apps/6760631689/distribution/productpages
111
+ - generic [ref=e130]: Custom Product Pages
112
+ - listitem [ref=e131]:
113
+ - link "Product Page Optimization" [ref=e132] [cursor=pointer]:
114
+ - /url: /apps/6760631689/distribution/optimization
115
+ - generic [ref=e133]: Product Page Optimization
116
+ - listitem [ref=e134]:
117
+ - link "Promo Codes" [ref=e135] [cursor=pointer]:
118
+ - /url: /apps/6760631689/distribution/promo_codes/generate
119
+ - generic [ref=e136]: Promo Codes
120
+ - listitem [ref=e137]:
121
+ - link "Game Center" [ref=e138] [cursor=pointer]:
122
+ - /url: /apps/6760631689/distribution/gamecenter
123
+ - generic [ref=e139]: Game Center
124
+ - generic [ref=e140]:
125
+ - heading "Monetization" [level=3] [ref=e142]
126
+ - list [ref=e143]:
127
+ - listitem [ref=e144]:
128
+ - link "Pricing and Availability" [ref=e145] [cursor=pointer]:
129
+ - /url: /apps/6760631689/distribution/pricing
130
+ - generic [ref=e146]: Pricing and Availability
131
+ - listitem [ref=e147]:
132
+ - link "In-App Purchases" [ref=e148] [cursor=pointer]:
133
+ - /url: /apps/6760631689/distribution/iaps
134
+ - generic [ref=e149]: In-App Purchases
135
+ - listitem [ref=e150]:
136
+ - link "Subscriptions" [ref=e151] [cursor=pointer]:
137
+ - /url: /apps/6760631689/distribution/subscriptions
138
+ - generic [ref=e152]: Subscriptions
139
+ - generic [ref=e153]:
140
+ - heading "Featuring" [level=3] [ref=e155]
141
+ - list [ref=e156]:
142
+ - listitem [ref=e157]:
143
+ - link "Nominations" [ref=e158] [cursor=pointer]:
144
+ - /url: /apps/6760631689/distribution/nominations
145
+ - generic [ref=e159]: Nominations
146
+ - generic [ref=e161]:
147
+ - generic [ref=e163]:
148
+ - heading "App Privacy" [level=2] [ref=e165]
149
+ - button "Publish" [disabled] [ref=e167]
150
+ - separator [ref=e168]
151
+ - generic [ref=e169]:
152
+ - generic [ref=e170]:
153
+ - generic [ref=e171]:
154
+ - heading "Privacy Policy" [level=3] [ref=e172]
155
+ - button "Edit" [ref=e173] [cursor=pointer]
156
+ - generic [ref=e175]:
157
+ - button "English (U.S.)" [disabled] [ref=e176]
158
+ - button "?" [ref=e179] [cursor=pointer]
159
+ - generic [ref=e180]:
160
+ - generic [ref=e181]:
161
+ - generic [ref=e183]:
162
+ - generic [ref=e184]: Privacy Policy URL
163
+ - button "More information" [ref=e186] [cursor=pointer]: "?"
164
+ - paragraph [ref=e187]: –
165
+ - generic [ref=e188]:
166
+ - generic [ref=e189]:
167
+ - generic [ref=e190]: User Privacy Choices URL
168
+ - paragraph [ref=e191]: (Optional)
169
+ - button "More information" [ref=e193] [cursor=pointer]: "?"
170
+ - paragraph [ref=e194]: –
171
+ - separator [ref=e195]
172
+ - generic [ref=e197]:
173
+ - paragraph [ref=e198]:
174
+ - generic [ref=e199]:
175
+ - paragraph [ref=e200]: The App Store is designed to be a safe and trusted place for people to discover apps from talented developers just like you. Your app can influence culture and change lives, so that's why we're counting on you to help us protect users' privacy.
176
+ - paragraph [ref=e201]: After clicking Get Started, you'll be asked to provide some information about your app's data collection practices. This information will appear on your app's product page, where users can see what data your app collects and how it's used.
177
+ - button "Get Started" [ref=e203] [cursor=pointer]
178
+ - contentinfo [ref=e10]:
179
+ - generic [ref=e11]:
180
+ - list [ref=e204]:
181
+ - listitem [ref=e205]:
182
+ - link "App Store Connect" [ref=e206] [cursor=pointer]:
183
+ - /url: /apps
184
+ - list [ref=e12]:
185
+ - listitem [ref=e13]: Copyright © 2026 Apple Inc. All rights reserved. |
186
+ - listitem [ref=e14]:
187
+ - link "Terms of Service" [ref=e15] [cursor=pointer]:
188
+ - /url: /WebObjects/iTunesConnect.woa/wa/termsOfService
189
+ - text: "|"
190
+ - listitem [ref=e16]:
191
+ - link "Privacy Policy" [ref=e17] [cursor=pointer]:
192
+ - /url: https://www.apple.com/legal/privacy
193
+ - text: "|"
194
+ - listitem [ref=e18]:
195
+ - link "Contact Us" [ref=e19] [cursor=pointer]:
196
+ - /url: /contact-us
screenshots/IMG_0737.PNG
Binary files differ
screenshots/IMG_0738.PNG
Binary files differ
screenshots/IMG_0739.PNG
Binary files differ
screenshots/IMG_0740.PNG
Binary files differ
screenshots/IMG_0741.PNG
Binary files differ
screenshots/IMG_0742.PNG
Binary files differ
screenshots/IMG_0743.PNG
Binary files differ
screenshots/ipad/ipad_home.png
Binary files differ
tasks/PRD-Glidr.md
....@@ -0,0 +1,1182 @@
1
+# PRD: Glidr — SPL Exam Preparation App
2
+
3
+**Version:** 1.0
4
+**Date:** 2026-03-15
5
+**Status:** Ready for Implementation
6
+**Author:** Atlas (Principal Software Architect)
7
+
8
+---
9
+
10
+## 1. Executive Summary
11
+
12
+### 1.1 Product Overview
13
+
14
+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.
15
+
16
+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.
17
+
18
+### 1.2 Target Users
19
+
20
+- Primary: German/Swiss/European glider pilot students preparing for SPL theoretical exam
21
+- Secondary: Existing pilots refreshing knowledge or preparing for recurrency checks
22
+- Language: English and French (bilingual, selectable in-app)
23
+
24
+### 1.3 Success Metrics
25
+
26
+- App Store rating >= 4.5 stars
27
+- >= 80% of purchased users complete at least one full subject review cycle
28
+- Content update delivery: corrections visible to users within 24 hours of server update
29
+- Exam pass rate correlation: users who complete >= 3 full cycles pass exam at >= 90%
30
+
31
+### 1.4 Timeline Estimate
32
+
33
+| Phase | Duration | Deliverable |
34
+|-------|----------|-------------|
35
+| Phase 1: Foundation | 3 weeks | App shell, DB schema, content download, question display |
36
+| Phase 2: Learning Engine | 2 weeks | SM-2 implementation, review queue, self-assessment |
37
+| Phase 3: Cram Mode | 1 week | Cram mode, session stats |
38
+| Phase 4: Purchase + Polish | 2 weeks | IAP, onboarding, UI polish, accessibility |
39
+| Phase 5: Backend | 1 week | Server setup, API, manifest system |
40
+| Phase 6: Testing + Release | 2 weeks | QA, beta, App Store submission |
41
+| **Total** | **~11 weeks** | |
42
+
43
+---
44
+
45
+## 2. Product Requirements
46
+
47
+### 2.1 Functional Requirements
48
+
49
+#### FR-01: Content Display
50
+- Display multiple choice questions with 4 answer options (A, B, C, D)
51
+- Show question text in selected language (English or French)
52
+- Support embedded figures (PNG and SVG) within questions
53
+- Figures must be zoomable via pinch gesture and double-tap
54
+- Display correct answer and explanation after user selects an answer
55
+- Explanation collapses/expands on tap
56
+
57
+#### FR-02: Subject Navigation
58
+- List all 9 subjects on home screen with progress indicators
59
+- Each subject shows: total cards, due today, learned cards, new cards
60
+- Tap subject to begin study session
61
+
62
+#### FR-03: Spaced Repetition Mode
63
+- Implement SuperMemo SM-2 algorithm exactly as specified
64
+- Present cards due today in review sessions
65
+- After revealing answer, user rates recall using 4-button self-assessment: Again / Hard / Good / Easy
66
+- SM-2 state (n, EF, interval) updated immediately after each rating
67
+- Session ends when all due cards have been reviewed
68
+- Show session summary: cards reviewed, correct count, next due date
69
+
70
+#### FR-04: Cram Mode
71
+- Accessible from each subject screen
72
+- Shows ALL cards in a subject (ignoring SM-2 schedule)
73
+- User answers, then sees correct answer + explanation
74
+- No self-assessment rating in cram mode
75
+- Cram progress does not modify SM-2 state
76
+- Session score shown at end (X/Y correct)
77
+
78
+#### FR-05: Content Updates
79
+- On each app launch: check remote manifest for content version changes
80
+- If new version available: download updated subject JSON in background
81
+- Update local SQLite database with new/changed questions
82
+- Show user notification when update is available and downloaded
83
+- Full offline operation after initial download
84
+
85
+#### FR-06: Purchase / Unlock
86
+- Free tier: first 10 questions of each subject are accessible without purchase
87
+- Paid tier: one-time purchase unlocks all 9 subjects fully
88
+- Restore Purchases button in Settings
89
+- Purchase state persisted in flutter_secure_storage
90
+
91
+#### FR-07: Language Selection
92
+- Language toggle (EN / FR) accessible from Settings and from subject screen
93
+- Switching language immediately updates all displayed content
94
+- Language preference persisted across app sessions
95
+
96
+#### FR-08: Progress and Statistics
97
+- Per-subject statistics: total, new, learning, review, mature (interval > 21 days)
98
+- Overall retention rate (correct / total reviews)
99
+- Study streak counter (consecutive days with at least 1 review)
100
+- Estimated exam readiness per subject (percentage of cards with EF >= 2.0 and interval >= 7)
101
+
102
+#### FR-09: Settings
103
+- Language selection (EN / FR)
104
+- New cards per day per subject (1-20, default 5)
105
+- Review reminder notification (optional, time picker)
106
+- Restore purchases
107
+- App version + build number
108
+
109
+### 2.2 Non-Functional Requirements
110
+
111
+#### NFR-01: Performance
112
+- App launch to home screen: < 1.5 seconds (cold start)
113
+- Question display including image: < 300ms
114
+- SM-2 state save after rating: < 50ms
115
+- Content manifest check: non-blocking (background thread)
116
+
117
+#### NFR-02: Offline Support
118
+- Full functionality after initial content download (no network required)
119
+- Initial download required only once; graceful offline handling after
120
+
121
+#### NFR-03: Platform Support
122
+- iOS 16.0+ (primary)
123
+- Android 10+ (API level 29+) (secondary, same codebase)
124
+- Tested on iPhone 13/14/15 (primary); Pixel 6/7 and Samsung Galaxy S21/S22 (Android)
125
+
126
+#### NFR-04: Accessibility
127
+- Dynamic Type support (text scales with system font size)
128
+- VoiceOver / TalkBack support for all interactive elements
129
+- Minimum tap target size: 44x44pt
130
+
131
+#### NFR-05: Data Privacy
132
+- No user accounts, no personal data collected
133
+- No analytics in v1 (add opt-in analytics in v2)
134
+- Purchase state stored locally only
135
+- GDPR-compliant: no EU data transfer
136
+
137
+---
138
+
139
+## 3. System Architecture
140
+
141
+### 3.1 High-Level Architecture
142
+
143
+```
144
+┌─────────────────────────────────────────────────┐
145
+│ GLIDR iOS/Android App │
146
+│ │
147
+│ ┌──────────────┐ ┌───────────────────────┐ │
148
+│ │ Flutter UI │ │ Business Logic │ │
149
+│ │ (Widgets) │◄──►│ (Riverpod Providers) │ │
150
+│ └──────────────┘ └───────────┬───────────┘ │
151
+│ │ │
152
+│ ┌───────────────────────────────▼─────────────┐ │
153
+│ │ Repository Layer (Drift ORM) │ │
154
+│ └───────────────────────────────┬─────────────┘ │
155
+│ │ │
156
+│ ┌───────────────┐ ┌────────────▼──────────────┐ │
157
+│ │ Secure Storage│ │ SQLite Database (Drift) │ │
158
+│ │ (flutter_ │ │ questions, progress, │ │
159
+│ │ secure_ │ │ sessions, settings │ │
160
+│ │ storage) │ └───────────────────────────┘ │
161
+│ └───────────────┘ │
162
+│ │
163
+│ ┌──────────────────────────────────────────────┐ │
164
+│ │ Content Sync Service (Dio HTTP) │ │
165
+│ └──────────────────┬───────────────────────────┘ │
166
+└─────────────────────│───────────────────────────┘
167
+ │ HTTPS + X-Glidr-Key header
168
+ ▼
169
+┌─────────────────────────────────────────────────┐
170
+│ tekmidian.com/glidr/api/v1/ │
171
+│ │
172
+│ manifest.json Apache/PHP auth layer │
173
+│ subjects/air_law.json Rate limiting │
174
+│ subjects/meteo.json Static file serving │
175
+│ figures/*.png HTTPS (Let's Encrypt) │
176
+│ figures/*.svg │
177
+└─────────────────────────────────────────────────┘
178
+```
179
+
180
+### 3.2 Flutter App Layer Architecture
181
+
182
+```
183
+lib/
184
+├── main.dart
185
+├── app.dart # MaterialApp, routing, theme
186
+├── core/
187
+│ ├── constants.dart # API base URL, API key (from --dart-define)
188
+│ ├── router.dart # go_router route definitions
189
+│ └── theme.dart # Color scheme, typography
190
+├── data/
191
+│ ├── database/
192
+│ │ ├── database.dart # Drift database definition
193
+│ │ ├── tables/ # Drift table definitions
194
+│ │ └── daos/ # Data access objects per domain
195
+│ ├── models/ # Freezed immutable data classes
196
+│ │ ├── question.dart
197
+│ │ ├── card_progress.dart
198
+│ │ ├── study_session.dart
199
+│ │ └── subject.dart
200
+│ ├── repositories/ # Repository interfaces + implementations
201
+│ │ ├── question_repository.dart
202
+│ │ ├── progress_repository.dart
203
+│ │ └── content_sync_repository.dart
204
+│ └── api/
205
+│ ├── glidr_api_client.dart # Dio client with auth interceptor
206
+│ └── models/ # API response models
207
+├── domain/
208
+│ └── sm2/
209
+│ ├── sm2_algorithm.dart # Pure SM-2 calculation functions
210
+│ └── review_scheduler.dart # Queue building logic
211
+├── presentation/
212
+│ ├── home/
213
+│ │ ├── home_screen.dart
214
+│ │ └── home_provider.dart
215
+│ ├── study/
216
+│ │ ├── study_screen.dart # SM-2 review session
217
+│ │ ├── study_provider.dart
218
+│ │ └── widgets/
219
+│ │ ├── question_card.dart
220
+│ │ ├── answer_options.dart
221
+│ │ ├── rating_buttons.dart
222
+│ │ └── figure_viewer.dart # photo_view zoomable image
223
+│ ├── cram/
224
+│ │ ├── cram_screen.dart
225
+│ │ └── cram_provider.dart
226
+│ ├── subject_detail/
227
+│ │ ├── subject_detail_screen.dart
228
+│ │ └── subject_detail_provider.dart
229
+│ ├── stats/
230
+│ │ ├── stats_screen.dart
231
+│ │ └── stats_provider.dart
232
+│ └── settings/
233
+│ ├── settings_screen.dart
234
+│ └── settings_provider.dart
235
+└── l10n/
236
+ ├── app_en.arb
237
+ └── app_fr.arb
238
+```
239
+
240
+---
241
+
242
+## 4. Data Models
243
+
244
+### 4.1 Question JSON Format (Remote API)
245
+
246
+Each subject is served as a single JSON file. Example structure:
247
+
248
+```json
249
+{
250
+ "subject_id": "air_law",
251
+ "subject_code": "01",
252
+ "name": {
253
+ "en": "Air Law",
254
+ "fr": "Droit aérien"
255
+ },
256
+ "version": "1.0.0",
257
+ "updated_at": "2026-03-15T00:00:00Z",
258
+ "questions": [
259
+ {
260
+ "id": "air_law_q1",
261
+ "number": 1,
262
+ "text": {
263
+ "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?",
264
+ "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?"
265
+ },
266
+ "options": {
267
+ "en": {
268
+ "A": "Winch and bungee.",
269
+ "B": "Winch, bungee and aero-tow.",
270
+ "C": "Winch and aero-tow.",
271
+ "D": "Aero-tow and bungee."
272
+ },
273
+ "fr": {
274
+ "A": "Treuil et sandow.",
275
+ "B": "Treuil, sandow et remorqué.",
276
+ "C": "Treuil et remorqué.",
277
+ "D": "Remorqué et sandow."
278
+ }
279
+ },
280
+ "correct": "A",
281
+ "explanation": {
282
+ "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...",
283
+ "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..."
284
+ },
285
+ "figures": []
286
+ },
287
+ {
288
+ "id": "pfp_q14",
289
+ "number": 14,
290
+ "text": {
291
+ "en": "What is the maximum payload according to the loading table?",
292
+ "fr": "Quelle est la charge utile maximale selon le tableau de chargement?"
293
+ },
294
+ "options": {
295
+ "en": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" },
296
+ "fr": { "A": "580 kg", "B": "450 kg", "C": "525 kg", "D": "600 kg" }
297
+ },
298
+ "correct": "B",
299
+ "explanation": {
300
+ "en": "According to the Discus loading table shown...",
301
+ "fr": "Selon le tableau de chargement du Discus affiché..."
302
+ },
303
+ "figures": [
304
+ {
305
+ "filename": "bazl_30_q14_discus_loading_table.png",
306
+ "type": "png",
307
+ "alt_en": "Discus aircraft loading table",
308
+ "alt_fr": "Tableau de chargement de l'aéronef Discus",
309
+ "position": "question"
310
+ }
311
+ ]
312
+ }
313
+ ]
314
+}
315
+```
316
+
317
+### 4.2 Manifest JSON Format
318
+
319
+```json
320
+{
321
+ "api_version": "1",
322
+ "manifest_version": "1.0.0",
323
+ "updated_at": "2026-03-15T00:00:00Z",
324
+ "subjects": {
325
+ "air_law": { "version": "1.0.0", "sha256": "abc...", "question_count": 110, "size_bytes": 85000 },
326
+ "aircraft_general_knowledge": { "version": "1.0.0", "sha256": "def...", "question_count": 110, "size_bytes": 92000 },
327
+ "communications": { "version": "1.0.0", "sha256": "ghi...", "question_count": 90, "size_bytes": 71000 },
328
+ "flight_performance": { "version": "1.0.0", "sha256": "jkl...", "question_count": 90, "size_bytes": 68000 },
329
+ "human_performance": { "version": "1.0.0", "sha256": "mno...", "question_count": 110, "size_bytes": 83000 },
330
+ "meteorology": { "version": "1.0.0", "sha256": "pqr...", "question_count": 110, "size_bytes": 88000 },
331
+ "navigation": { "version": "1.0.0", "sha256": "stu...", "question_count": 141, "size_bytes": 115000 },
332
+ "operational_procedures": { "version": "1.0.0", "sha256": "vwx...", "question_count": 110, "size_bytes": 84000 },
333
+ "principles_of_flight": { "version": "1.0.0", "sha256": "yza...", "question_count": 110, "size_bytes": 86000 }
334
+ }
335
+}
336
+```
337
+
338
+### 4.3 SQLite Database Schema (Drift)
339
+
340
+```sql
341
+-- Questions table (populated from downloaded JSON)
342
+CREATE TABLE questions (
343
+ id TEXT NOT NULL PRIMARY KEY,
344
+ subject_id TEXT NOT NULL,
345
+ number INTEGER NOT NULL,
346
+ text_en TEXT NOT NULL,
347
+ text_fr TEXT NOT NULL,
348
+ options_en TEXT NOT NULL, -- JSON string: {"A":"...","B":"...","C":"...","D":"..."}
349
+ options_fr TEXT NOT NULL,
350
+ correct TEXT NOT NULL, -- "A" | "B" | "C" | "D"
351
+ explanation_en TEXT NOT NULL,
352
+ explanation_fr TEXT NOT NULL,
353
+ figures TEXT NOT NULL -- JSON array string (empty "[]" if no figures)
354
+);
355
+
356
+-- SM-2 progress per card
357
+CREATE TABLE card_progress (
358
+ question_id TEXT NOT NULL PRIMARY KEY,
359
+ repetition_number INTEGER NOT NULL DEFAULT 0,
360
+ easiness_factor REAL NOT NULL DEFAULT 2.5,
361
+ interval_days INTEGER NOT NULL DEFAULT 1,
362
+ next_review_date TEXT, -- "YYYY-MM-DD", NULL = new (never reviewed)
363
+ last_review_date TEXT, -- "YYYY-MM-DD"
364
+ total_reviews INTEGER NOT NULL DEFAULT 0,
365
+ correct_reviews INTEGER NOT NULL DEFAULT 0,
366
+ created_at TEXT NOT NULL,
367
+ updated_at TEXT NOT NULL,
368
+ FOREIGN KEY (question_id) REFERENCES questions(id)
369
+);
370
+
371
+-- Study sessions
372
+CREATE TABLE study_sessions (
373
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
374
+ started_at TEXT NOT NULL, -- ISO 8601
375
+ ended_at TEXT,
376
+ mode TEXT NOT NULL, -- "spaced_repetition" | "cram"
377
+ subject_id TEXT, -- NULL = mixed
378
+ cards_reviewed INTEGER NOT NULL DEFAULT 0,
379
+ cards_correct INTEGER NOT NULL DEFAULT 0
380
+);
381
+
382
+-- Individual review events
383
+CREATE TABLE review_log (
384
+ id TEXT NOT NULL PRIMARY KEY, -- UUID
385
+ session_id TEXT NOT NULL,
386
+ question_id TEXT NOT NULL,
387
+ reviewed_at TEXT NOT NULL, -- ISO 8601
388
+ quality INTEGER NOT NULL, -- 0-5 (SM-2 grade) or -1 for cram
389
+ time_to_answer_ms INTEGER,
390
+ was_cram INTEGER NOT NULL DEFAULT 0,
391
+ FOREIGN KEY (session_id) REFERENCES study_sessions(id),
392
+ FOREIGN KEY (question_id) REFERENCES questions(id)
393
+);
394
+
395
+-- Content version tracking
396
+CREATE TABLE content_manifest (
397
+ subject_id TEXT NOT NULL PRIMARY KEY,
398
+ version TEXT NOT NULL,
399
+ sha256 TEXT NOT NULL,
400
+ downloaded_at TEXT NOT NULL,
401
+ question_count INTEGER NOT NULL
402
+);
403
+
404
+-- App settings (key-value)
405
+CREATE TABLE settings (
406
+ key TEXT NOT NULL PRIMARY KEY,
407
+ value TEXT NOT NULL
408
+);
409
+-- Default settings:
410
+-- language: "en"
411
+-- new_cards_per_day: "5"
412
+-- reminder_enabled: "false"
413
+-- reminder_time: "19:00"
414
+-- is_unlocked: "false"
415
+-- onboarding_complete: "false"
416
+```
417
+
418
+---
419
+
420
+## 5. Learning Algorithm: SM-2 Implementation
421
+
422
+### 5.1 Core Algorithm
423
+
424
+```dart
425
+// lib/domain/sm2/sm2_algorithm.dart
426
+
427
+class SM2Result {
428
+ final int repetitionNumber;
429
+ final double easinessFactor;
430
+ final int intervalDays;
431
+ final DateTime nextReviewDate;
432
+
433
+ const SM2Result({
434
+ required this.repetitionNumber,
435
+ required this.easinessFactor,
436
+ required this.intervalDays,
437
+ required this.nextReviewDate,
438
+ });
439
+}
440
+
441
+/// Grade 0: complete blackout
442
+/// Grade 1: incorrect, remembered on seeing answer
443
+/// Grade 2: incorrect, correct seemed easy after
444
+/// Grade 3: correct with serious difficulty
445
+/// Grade 4: correct after hesitation
446
+/// Grade 5: perfect response
447
+SM2Result computeNextInterval({
448
+ required int currentRepetitionNumber, // n
449
+ required double currentEasinessFactor, // EF
450
+ required int currentIntervalDays, // I
451
+ required int quality, // 0-5
452
+}) {
453
+ assert(quality >= 0 && quality <= 5);
454
+
455
+ int n = currentRepetitionNumber;
456
+ double ef = currentEasinessFactor;
457
+ int interval = currentIntervalDays;
458
+
459
+ if (quality < 3) {
460
+ // Failed review: restart interval sequence, keep EF
461
+ n = 0;
462
+ interval = 1;
463
+ } else {
464
+ // Successful review: advance interval
465
+ switch (n) {
466
+ case 0:
467
+ interval = 1;
468
+ case 1:
469
+ interval = 6;
470
+ default:
471
+ interval = (interval * ef).ceil();
472
+ }
473
+ n += 1;
474
+ }
475
+
476
+ // Update EF (clamp to minimum 1.3)
477
+ ef = ef + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
478
+ ef = ef.clamp(1.3, double.infinity);
479
+
480
+ final nextDate = DateTime.now().add(Duration(days: interval));
481
+
482
+ return SM2Result(
483
+ repetitionNumber: n,
484
+ easinessFactor: ef,
485
+ intervalDays: interval,
486
+ nextReviewDate: DateTime(nextDate.year, nextDate.month, nextDate.day),
487
+ );
488
+}
489
+```
490
+
491
+### 5.2 Quality Grade Mapping (4-Button UI)
492
+
493
+The app presents 4 buttons (Anki-style simplification of SM-2's 0-5 scale):
494
+
495
+| Button | Color | SM-2 Grade | Effect |
496
+|--------|-------|-----------|--------|
497
+| Again | Red | 1 | Reset interval to 1 day, EF -0.54 |
498
+| Hard | Orange | 3 | EF -0.14, interval advances slowly |
499
+| Good | Green | 4 | EF unchanged, interval advances normally |
500
+| Easy | Blue | 5 | EF +0.10, interval advances faster |
501
+
502
+### 5.3 Daily Review Queue
503
+
504
+```dart
505
+// lib/domain/sm2/review_scheduler.dart
506
+
507
+Future<List<Question>> buildDailyQueue({
508
+ required String? subjectId, // null = all subjects
509
+ required int maxNewCards,
510
+}) async {
511
+ final today = DateTime.now();
512
+ final todayStr = '${today.year}-${today.month.toString().padLeft(2,'0')}-${today.day.toString().padLeft(2,'0')}';
513
+
514
+ // 1. Due cards (have been reviewed before, scheduled for today or earlier)
515
+ final dueCards = await _progressDao.getDueCards(
516
+ subjectId: subjectId,
517
+ asOf: todayStr,
518
+ );
519
+
520
+ // 2. New cards (never reviewed, next_review_date IS NULL)
521
+ final newCards = await _progressDao.getNewCards(
522
+ subjectId: subjectId,
523
+ limit: maxNewCards,
524
+ );
525
+
526
+ // Shuffle each group independently, then concatenate: due first, then new
527
+ dueCards.shuffle();
528
+ newCards.shuffle();
529
+
530
+ return [...dueCards, ...newCards];
531
+}
532
+```
533
+
534
+### 5.4 Cram Mode
535
+
536
+```dart
537
+Future<List<Question>> buildCramQueue({
538
+ required String subjectId,
539
+ required bool shuffled,
540
+}) async {
541
+ final questions = await _questionDao.getAllForSubject(subjectId);
542
+ if (shuffled) questions.shuffle();
543
+ return questions;
544
+}
545
+
546
+// Cram review recording — does NOT update card_progress SM-2 state
547
+Future<void> recordCramReview({
548
+ required String sessionId,
549
+ required String questionId,
550
+ required bool wasCorrect,
551
+}) async {
552
+ await _reviewLogDao.insert(ReviewLogEntry(
553
+ id: const Uuid().v4(),
554
+ sessionId: sessionId,
555
+ questionId: questionId,
556
+ reviewedAt: DateTime.now().toIso8601String(),
557
+ quality: wasCorrect ? 4 : 1, // approximate mapping for stats
558
+ wasCram: true,
559
+ ));
560
+ // card_progress is intentionally NOT updated
561
+}
562
+```
563
+
564
+---
565
+
566
+## 6. API Design
567
+
568
+### 6.1 Base Configuration
569
+
570
+```
571
+Base URL: https://tekmidian.com/glidr/api/v1
572
+Auth Header: X-Glidr-Key: {compile-time-injected-secret}
573
+Content-Type: application/json
574
+```
575
+
576
+### 6.2 Endpoints
577
+
578
+#### GET /manifest.json
579
+Returns the current version hash for all subjects. Used to detect if local content needs updating.
580
+
581
+Response:
582
+```json
583
+{
584
+ "api_version": "1",
585
+ "manifest_version": "1.2.0",
586
+ "updated_at": "2026-03-15T00:00:00Z",
587
+ "subjects": {
588
+ "air_law": { "version": "1.2.0", "sha256": "abc123...", "question_count": 110, "size_bytes": 85000 },
589
+ ...
590
+ }
591
+}
592
+```
593
+
594
+#### GET /subjects/{subject_id}.json
595
+Returns full question set for a subject. Only called when local hash differs from manifest.
596
+
597
+Path parameters:
598
+- `subject_id`: one of `air_law`, `aircraft_general_knowledge`, `communications`, `flight_performance`, `human_performance`, `meteorology`, `navigation`, `operational_procedures`, `principles_of_flight`
599
+
600
+Response: Full subject JSON as described in Section 4.1.
601
+
602
+#### GET /figures/{filename}
603
+Serves a figure file (PNG or SVG). Called only if figures are NOT bundled with the app.
604
+
605
+Note: For v1, all figures are bundled as Flutter assets. This endpoint is a fallback for future use or figure-only updates.
606
+
607
+### 6.3 Error Handling
608
+
609
+| HTTP Status | Meaning | App Behavior |
610
+|------------|---------|-------------|
611
+| 200 | Success | Process response |
612
+| 304 | Not Modified (future: ETag support) | Use cached content |
613
+| 403 | Invalid API key | Log error silently, use cached content |
614
+| 404 | Subject not found | Log error, skip that subject update |
615
+| 429 | Rate limited | Retry after 60 seconds |
616
+| 500 | Server error | Use cached content, retry next launch |
617
+
618
+All errors are non-fatal: the app always falls back to cached local content.
619
+
620
+### 6.4 Content Sync Flow
621
+
622
+```
623
+App Launch
624
+ │
625
+ ├── Is local content available?
626
+ │ NO → Show "Downloading content..." screen
627
+ │ Download all 9 subjects + figures sequentially
628
+ │ Show progress bar
629
+ │ On complete → proceed to Home
630
+ │
631
+ │ YES → Proceed to Home immediately
632
+ │ Background: check manifest
633
+ │ │
634
+ │ ├── Hash differs for any subject?
635
+ │ │ YES → Download updated subject JSON
636
+ │ │ Update SQLite questions table
637
+ │ │ Show subtle "Content updated" banner
638
+ │ │
639
+ │ └── No changes → do nothing
640
+ │
641
+ └── Home Screen
642
+```
643
+
644
+---
645
+
646
+## 7. UI Flows and Screens
647
+
648
+### 7.1 Screen Inventory
649
+
650
+| Screen | Route | Purpose |
651
+|--------|-------|---------|
652
+| Splash | `/` | App init, content check, auth check |
653
+| Onboarding | `/onboarding` | First-run: explain app, prompt purchase or free trial |
654
+| Home | `/home` | Subject list with progress indicators |
655
+| Subject Detail | `/subject/:id` | Subject overview, start study/cram |
656
+| Study Session | `/study/:id` | SM-2 review session |
657
+| Cram Session | `/cram/:id` | Cram mode session |
658
+| Session Complete | `/session-complete` | Post-session summary |
659
+| Statistics | `/stats` | Overall and per-subject stats |
660
+| Settings | `/settings` | Language, IAP, preferences |
661
+| Paywall | `/paywall` | Purchase screen for locked content |
662
+
663
+### 7.2 Home Screen Layout
664
+
665
+```
666
+┌─────────────────────────────┐
667
+│ Glidr [Stats] [⚙]│
668
+├─────────────────────────────┤
669
+│ Good morning! │
670
+│ Review streak: 🔥 7 days │
671
+│ │
672
+│ Due today: 23 cards │
673
+│ [Start All Reviews] │
674
+├─────────────────────────────┤
675
+│ Subjects │
676
+│ │
677
+│ 01 Air Law ■■■□□ │
678
+│ Due: 3 · New: 5 │
679
+│ │
680
+│ 02 Aircraft General ■■□□□ │
681
+│ Due: 7 · New: 5 │
682
+│ │
683
+│ 03 Communications ■□□□□ │
684
+│ Due: 0 · New: 5 │
685
+│ │
686
+│ ... (scrollable list) │
687
+└─────────────────────────────┘
688
+```
689
+
690
+### 7.3 Study Session Flow
691
+
692
+```
693
+1. Question Screen
694
+ ┌─────────────────────────────┐
695
+ │ Air Law Q23 of 34 │
696
+ │ Progress bar ████░░░░░░ │
697
+ ├─────────────────────────────┤
698
+ │ │
699
+ │ Question text here... │
700
+ │ │
701
+ │ [Figure if present, │
702
+ │ pinch to zoom] │
703
+ │ │
704
+ ├─────────────────────────────┤
705
+ │ ○ A) Option text │
706
+ │ ○ B) Option text │
707
+ │ ○ C) Option text │
708
+ │ ○ D) Option text │
709
+ └─────────────────────────────┘
710
+
711
+2. Answer Revealed Screen (after tapping an option)
712
+ ┌─────────────────────────────┐
713
+ │ Air Law Q23 of 34 │
714
+ ├─────────────────────────────┤
715
+ │ │
716
+ │ Question text... │
717
+ │ │
718
+ ├─────────────────────────────┤
719
+ │ ○ A) Option ← Wrong │
720
+ │ ✓ B) Option ← CORRECT │
721
+ │ ○ C) Option │
722
+ │ ○ D) Option │
723
+ ├─────────────────────────────┤
724
+ │ > Explanation (tap to │
725
+ │ expand) │
726
+ │ Under Part-SFCL... │
727
+ ├─────────────────────────────┤
728
+ │ How well did you know it? │
729
+ │ [Again] [Hard] [Good][Easy]│
730
+ └─────────────────────────────┘
731
+
732
+3. Session Complete Screen
733
+ ┌─────────────────────────────┐
734
+ │ Session Complete! │
735
+ │ │
736
+ │ Reviewed: 23 cards │
737
+ │ Correct: 18 (78%) │
738
+ │ Next review: Tomorrow │
739
+ │ │
740
+ │ [Back to Home] │
741
+ └─────────────────────────────┘
742
+```
743
+
744
+### 7.4 Cram Mode Flow
745
+
746
+```
747
+1. Subject screen → [Cram Mode] button
748
+2. Cram session: question → tap to reveal → correct/incorrect tap
749
+3. No self-rating buttons (just "Next" arrow)
750
+4. Session end: score summary (X/Y)
751
+```
752
+
753
+### 7.5 Figure Viewer
754
+
755
+When a question includes a figure:
756
+- Figure displayed inline in the question card
757
+- `photo_view` widget wraps the image
758
+- Pinch-to-zoom with min/max scale (0.5x - 5.0x)
759
+- Double-tap resets to fit
760
+- Single-tap on figure expands to full-screen overlay
761
+- Full-screen overlay: same zoom behavior + close button (X)
762
+
763
+---
764
+
765
+## 8. Technology Stack
766
+
767
+### 8.1 Dependencies (pubspec.yaml)
768
+
769
+```yaml
770
+dependencies:
771
+ flutter:
772
+ sdk: flutter
773
+
774
+ # State management
775
+ flutter_riverpod: ^2.5.0
776
+ riverpod_annotation: ^2.3.0
777
+
778
+ # Database
779
+ drift: ^2.18.0
780
+ sqlite3_flutter_libs: ^0.5.0
781
+ path_provider: ^2.1.0
782
+ path: ^1.9.0
783
+
784
+ # HTTP
785
+ dio: ^5.4.0
786
+
787
+ # Immutable data models
788
+ freezed_annotation: ^2.4.0
789
+ json_annotation: ^4.9.0
790
+
791
+ # Image zoom
792
+ photo_view: ^0.15.0
793
+
794
+ # In-app purchase
795
+ in_app_purchase: ^3.1.0
796
+
797
+ # Secure storage (API key, purchase state)
798
+ flutter_secure_storage: ^9.0.0
799
+
800
+ # Navigation
801
+ go_router: ^13.0.0
802
+
803
+ # UUID generation
804
+ uuid: ^4.4.0
805
+
806
+ # Crypto (SHA-256 for manifest verification)
807
+ crypto: ^3.0.0
808
+
809
+ # SVG rendering
810
+ flutter_svg: ^2.0.0
811
+
812
+ # Internationalization
813
+ flutter_localizations:
814
+ sdk: flutter
815
+ intl: ^0.19.0
816
+
817
+dev_dependencies:
818
+ flutter_test:
819
+ sdk: flutter
820
+ drift_dev: ^2.18.0
821
+ riverpod_generator: ^2.3.0
822
+ build_runner: ^2.4.0
823
+ freezed: ^2.4.0
824
+ json_serializable: ^6.7.0
825
+ flutter_lints: ^4.0.0
826
+```
827
+
828
+### 8.2 Flutter Configuration
829
+
830
+```
831
+flutter:
832
+ assets:
833
+ - assets/figures/ # All 58 figure files (PNG + SVG)
834
+ - assets/fonts/ # Custom fonts if used
835
+
836
+ generate: true # Enable l10n generation
837
+```
838
+
839
+Build-time injection of API key (never in source):
840
+```bash
841
+flutter build ios --dart-define=GLIDR_API_KEY=your_secret_here
842
+flutter build appbundle --dart-define=GLIDR_API_KEY=your_secret_here
843
+```
844
+
845
+---
846
+
847
+## 9. Backend Setup (tekmidian.com)
848
+
849
+### 9.1 Directory Structure
850
+
851
+```
852
+/var/www/tekmidian.com/public_html/glidr/
853
+├── api/
854
+│ └── v1/
855
+│ ├── .htaccess # Auth + CORS headers
856
+│ ├── manifest.json # Regenerated on each content update
857
+│ ├── subjects/
858
+│ │ ├── air_law.json
859
+│ │ ├── aircraft_general_knowledge.json
860
+│ │ ├── communications.json
861
+│ │ ├── flight_performance.json
862
+│ │ ├── human_performance.json
863
+│ │ ├── meteorology.json
864
+│ │ ├── navigation.json
865
+│ │ ├── operational_procedures.json
866
+│ │ └── principles_of_flight.json
867
+│ └── figures/
868
+│ ├── *.png (50 files)
869
+│ └── *.svg (8 files)
870
+└── tools/
871
+ └── generate_manifest.php # CLI tool to regenerate manifest.json
872
+```
873
+
874
+### 9.2 .htaccess Auth Configuration
875
+
876
+```apache
877
+# /glidr/api/v1/.htaccess
878
+Options -Indexes
879
+
880
+# CORS for potential web use
881
+Header always set Access-Control-Allow-Origin "*"
882
+Header always set Access-Control-Allow-Headers "X-Glidr-Key, Content-Type"
883
+
884
+# Auth check via environment variable
885
+SetEnvIf HTTP_X_GLIDR_KEY "^your_secret_here$" GLIDR_AUTH=1
886
+Order Deny,Allow
887
+Deny from all
888
+Allow from env=GLIDR_AUTH
889
+
890
+# Better: use a PHP wrapper for auth to avoid key in .htaccess
891
+```
892
+
893
+Better approach — PHP auth wrapper:
894
+
895
+```php
896
+<?php
897
+// auth.php — included by all JSON-serving endpoints
898
+$provided_key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';
899
+$expected_key = getenv('GLIDR_API_KEY'); // Set in Apache/server environment
900
+
901
+if (empty($expected_key) || $provided_key !== $expected_key) {
902
+ http_response_code(403);
903
+ header('Content-Type: application/json');
904
+ echo json_encode(['error' => 'Forbidden']);
905
+ exit;
906
+}
907
+```
908
+
909
+### 9.3 Content Update Workflow
910
+
911
+When questions need to be corrected or updated:
912
+
913
+1. Edit the source Markdown files (already in Obsidian vault)
914
+2. Run the conversion tool (see Section 10.4) to regenerate JSON files
915
+3. Upload new JSON files to server via SFTP/rsync
916
+4. Run `generate_manifest.php` to update version hashes
917
+5. App users receive update on next launch
918
+
919
+---
920
+
921
+## 10. Implementation Checklists
922
+
923
+### 10.1 Phase 1: Foundation (3 weeks)
924
+
925
+#### Database & Models
926
+- [ ] Set up Flutter project with all dependencies listed in Section 8.1
927
+- [ ] Create Drift database with all tables from Section 4.3
928
+- [ ] Generate Drift DAOs for questions, card_progress, review_log, settings
929
+- [ ] Create Freezed data models for Question, CardProgress, StudySession
930
+- [ ] Write unit tests for all DAOs
931
+
932
+#### Content Download
933
+- [ ] Implement Dio HTTP client with X-Glidr-Key auth interceptor
934
+- [ ] Implement manifest.json fetch and local version comparison
935
+- [ ] Implement subject JSON download with SHA-256 verification
936
+- [ ] Implement SQLite import from downloaded JSON
937
+- [ ] Implement first-run download screen with progress indicator
938
+- [ ] Implement background update check on subsequent launches
939
+
940
+#### Question Display
941
+- [ ] Implement question card widget (text + options)
942
+- [ ] Implement figure viewer widget using photo_view
943
+- [ ] Implement SVG rendering via flutter_svg
944
+- [ ] Implement answer selection with color feedback (correct/incorrect)
945
+- [ ] Implement explanation collapsible section
946
+
947
+#### App Shell
948
+- [ ] Set up go_router with all routes from Section 7.1
949
+- [ ] Implement Riverpod provider structure
950
+- [ ] Implement home screen with subject list
951
+- [ ] Implement subject detail screen
952
+- [ ] Implement bilingual content switching (EN/FR)
953
+
954
+#### Testing Checklist — Phase 1
955
+- [ ] Unit tests: JSON parsing for all 9 subjects
956
+- [ ] Unit tests: SHA-256 manifest verification
957
+- [ ] Unit tests: SQLite import idempotency (re-importing same data is safe)
958
+- [ ] Widget tests: question card displays correctly for question with figure
959
+- [ ] Widget tests: question card displays correctly for question without figure
960
+- [ ] Integration test: full download flow on fresh install
961
+- [ ] Manual test: figure zoom works on real device
962
+
963
+### 10.2 Phase 2: Learning Engine (2 weeks)
964
+
965
+#### SM-2 Algorithm
966
+- [ ] Implement `computeNextInterval()` function (Section 5.1)
967
+- [ ] Unit test all quality grades 0-5 with known expected outputs
968
+- [ ] Unit test EF clamping at 1.3 minimum
969
+- [ ] Unit test interval reset on quality < 3 (keeps EF, resets n and I)
970
+- [ ] Unit test progression: n=0→1day, n=1→6days, n=2→6*EF days
971
+
972
+#### Review Queue
973
+- [ ] Implement `buildDailyQueue()` — due cards + new cards (Section 5.3)
974
+- [ ] Implement "new cards per day" limit from settings
975
+- [ ] Implement queue shuffle
976
+- [ ] Unit test queue: due cards before new cards
977
+- [ ] Unit test queue: respects new card limit
978
+
979
+#### Study Session UI
980
+- [ ] Implement study session screen with question display
981
+- [ ] Implement progress bar (X of Y reviewed)
982
+- [ ] Implement answer tap → reveal flow
983
+- [ ] Implement 4-button rating row (Again / Hard / Good / Easy)
984
+- [ ] On rating: call `computeNextInterval()`, persist to card_progress
985
+- [ ] Log review to review_log table
986
+- [ ] Implement within-session re-show for quality < 3 cards
987
+- [ ] Implement session completion detection
988
+- [ ] Implement session summary screen
989
+
990
+#### Testing Checklist — Phase 2
991
+- [ ] Unit tests: all SM-2 branches (quality 0-5, all n values, EF edge cases)
992
+- [ ] Unit tests: daily queue respects new card limit
993
+- [ ] Integration test: complete a study session of 10 cards, verify DB state
994
+- [ ] Integration test: rating "Again" re-shows card in same session
995
+- [ ] Integration test: session completion triggers summary screen
996
+- [ ] Manual test: study 20 cards in one session, check review_log entries
997
+
998
+### 10.3 Phase 3: Cram Mode (1 week)
999
+
1000
+- [ ] Implement cram mode queue builder (all cards in subject, shuffled)
1001
+- [ ] Implement cram session screen (question → reveal → next, no rating buttons)
1002
+- [ ] Implement cram correct/incorrect tap (for session stats only)
1003
+- [ ] Verify cram reviews do NOT modify card_progress SM-2 state
1004
+- [ ] Log cram reviews to review_log with was_cram=1
1005
+- [ ] Implement cram session summary screen
1006
+
1007
+#### Testing Checklist — Phase 3
1008
+- [ ] Unit test: cram review does not modify card_progress
1009
+- [ ] Integration test: cram session records in review_log with was_cram=1
1010
+- [ ] Integration test: SM-2 state unchanged after cram session
1011
+
1012
+### 10.4 Phase 4: Purchase + Polish (2 weeks)
1013
+
1014
+#### In-App Purchase
1015
+- [ ] Create non-consumable IAP product in App Store Connect
1016
+- [ ] Create one-time product in Google Play Console
1017
+- [ ] Implement `in_app_purchase` purchase flow
1018
+- [ ] Implement "Restore Purchases" button
1019
+- [ ] Persist unlock state in flutter_secure_storage
1020
+- [ ] Implement paywall screen with subject preview (first 10 questions free)
1021
+- [ ] Gate subject access: > Q10 requires purchase
1022
+
1023
+#### Statistics Screen
1024
+- [ ] Implement per-subject stats: new / learning / review / mature counts
1025
+- [ ] Implement overall retention rate calculation
1026
+- [ ] Implement study streak counter
1027
+- [ ] Implement "exam readiness" indicator per subject
1028
+
1029
+#### Onboarding
1030
+- [ ] Implement first-run onboarding (3-4 screens: welcome, how it works, language, purchase/trial)
1031
+- [ ] Implement first-run content download trigger
1032
+
1033
+#### UI Polish
1034
+- [ ] Implement review notification (local notification at configured time)
1035
+- [ ] Implement settings screen (language, new cards/day, reminder, restore, version)
1036
+- [ ] Accessibility: VoiceOver labels on all interactive elements
1037
+- [ ] Dynamic Type: test at all font sizes
1038
+- [ ] Dark mode support
1039
+
1040
+#### Testing Checklist — Phase 4
1041
+- [ ] Manual test: purchase flow on Sandbox environment (iOS)
1042
+- [ ] Manual test: restore purchases after app delete and reinstall
1043
+- [ ] Manual test: free tier correctly limits to 10 questions per subject
1044
+- [ ] Accessibility audit: VoiceOver navigate entire study session
1045
+- [ ] Manual test: dark mode on iOS and Android
1046
+
1047
+### 10.5 Phase 5: Backend (1 week)
1048
+
1049
+- [ ] Provision `tekmidian.com/glidr/api/v1/` directory
1050
+- [ ] Set GLIDR_API_KEY server environment variable
1051
+- [ ] Write and deploy `.htaccess` auth rules
1052
+- [ ] Write Markdown-to-JSON conversion script (Python or PHP)
1053
+- [ ] Convert all 9 subject Markdown files to JSON format
1054
+- [ ] Convert French Markdown files and merge into bilingual JSON
1055
+- [ ] Copy all figures to server
1056
+- [ ] Write and run `generate_manifest.php` to create initial manifest.json
1057
+- [ ] Test all endpoints with curl (with and without API key)
1058
+- [ ] Test rate limiting (verify 429 on abuse)
1059
+- [ ] Document update workflow in a README
1060
+
1061
+#### Security Checklist — Phase 5
1062
+- [ ] HTTPS with valid SSL certificate on tekmidian.com
1063
+- [ ] API key not present in any git repository (use environment variable)
1064
+- [ ] Directory listing disabled (Options -Indexes)
1065
+- [ ] Server error pages don't expose stack traces
1066
+- [ ] Test: direct URL access without API key returns 403
1067
+
1068
+### 10.6 Phase 6: Testing + Release (2 weeks)
1069
+
1070
+- [ ] Full regression test on iPhone 13/14/15 (real devices)
1071
+- [ ] Full regression test on Android Pixel 6/7 and Samsung Galaxy S21
1072
+- [ ] TestFlight beta (iOS): 5-10 beta testers
1073
+- [ ] Beta feedback incorporated
1074
+- [ ] App Store listing: screenshots, description (EN + FR), keywords
1075
+- [ ] App Store Review Guidelines compliance check
1076
+- [ ] Privacy Policy published (required for App Store)
1077
+- [ ] Submit for App Store review
1078
+- [ ] Google Play closed testing track
1079
+- [ ] Submit for Play Store review
1080
+
1081
+---
1082
+
1083
+## 11. Content Conversion Tool
1084
+
1085
+A command-line Python script to convert the existing Obsidian Markdown files to the JSON format expected by the API.
1086
+
1087
+### 11.1 Script Purpose
1088
+
1089
+Input: `/SPL Exam Questions/01 - Air Law.md` + `/SPL Exam Questions FR/01 - Droit aérien.md`
1090
+Output: `air_law.json` matching the schema in Section 4.1
1091
+
1092
+### 11.2 Parsing Logic
1093
+
1094
+The script must handle:
1095
+1. Parse `### Q{N}: {text} ^q{N}` as question text and number
1096
+2. Parse `- A) ... B) ... C) ... D) ...` as answer options
1097
+3. Parse `**Correct: {letter})**` as correct answer
1098
+4. Parse `> **Explanation:** {text}` as explanation
1099
+5. Parse `![[figures/{filename}]]` and `![alt](figures/{filename})` as figure references
1100
+6. Match each English question to its French counterpart by question number
1101
+7. Combine into bilingual JSON structure
1102
+
1103
+### 11.3 Script Location
1104
+
1105
+`/Users/i052341/dev/apps/glidr/tools/convert_questions.py`
1106
+
1107
+(To be created as part of Phase 5)
1108
+
1109
+---
1110
+
1111
+## 12. Risk Assessment
1112
+
1113
+| Risk | Probability | Impact | Mitigation |
1114
+|------|-------------|--------|-----------|
1115
+| App Store rejection (IAP implementation) | Low | High | Follow IAP guidelines strictly; add required Restore Purchases button |
1116
+| Question count discrepancy (Markdown vs spec) | Medium | Low | Conversion script validates counts and flags mismatches |
1117
+| tekmidian.com SSL certificate expiry breaking content sync | Low | Medium | Set calendar reminder for cert renewal; app falls back to cached content |
1118
+| API key extracted from binary | Low | Low | App contains no user data; question bank has limited commercial value; rotate key if abused |
1119
+| Flutter major version breaking dependencies | Low | Medium | Pin to Flutter stable channel; update dependencies deliberately, not on latest |
1120
+| French translation gaps | Medium | Medium | Conversion script flags any English questions without a matching French question |
1121
+
1122
+---
1123
+
1124
+## 13. Future Enhancements (v2 Backlog)
1125
+
1126
+- User accounts: sync progress across devices via iCloud/server
1127
+- More content: additional national question banks (Germany, Austria, UK)
1128
+- Mock exam mode: simulate real SPL exam (random selection, timed, per EASA format)
1129
+- Detailed analytics: per-topic weak areas, study time tracking
1130
+- Opt-in analytics: understand which questions users find hardest
1131
+- Push notifications: study reminders
1132
+- Certificate pinning: harden API against MITM attacks
1133
+- Apple App Attest: prevent automated bulk downloading
1134
+- Widget: iOS home screen widget showing "cards due today"
1135
+- Apple Watch: quick review on watch
1136
+- Web version: study on desktop browser (Flutter Web)
1137
+
1138
+---
1139
+
1140
+## Appendix A: Subject IDs Reference
1141
+
1142
+| Code | Subject Name (EN) | Subject Name (FR) | subject_id |
1143
+|------|-------------------|-------------------|------------|
1144
+| 01 | Air Law | Droit aérien | air_law |
1145
+| 02 | Aircraft General Knowledge | Connaissances générales de l'aéronef | aircraft_general_knowledge |
1146
+| 03 | Communications | Communications | communications |
1147
+| 04 | Flight Performance and Planning | Performances et planification du vol | flight_performance |
1148
+| 05 | Human Performance | Performance humaine | human_performance |
1149
+| 06 | Meteorology | Météorologie | meteorology |
1150
+| 07 | Navigation | Navigation | navigation |
1151
+| 08 | Operational Procedures | Procédures opérationnelles | operational_procedures |
1152
+| 09 | Principles of Flight | Principes du vol | principles_of_flight |
1153
+
1154
+## Appendix B: Question Count Reference
1155
+
1156
+| Subject | Questions in Current Files | Target (Full Bank) |
1157
+|---------|---------------------------|-------------------|
1158
+| Air Law | 50 (QuizVDS) | 110 |
1159
+| Aircraft General Knowledge | 50 (QuizVDS) | 110 |
1160
+| Communications | 50 (QuizVDS) | 90 |
1161
+| Flight Performance and Planning | 30 (QuizVDS) | 90 |
1162
+| Human Performance | 50 (QuizVDS) | 110 |
1163
+| Meteorology | 50 (QuizVDS) | 110 |
1164
+| Navigation | 80 (QuizVDS + SFVS) | 141 |
1165
+| Operational Procedures | 50 (QuizVDS) | 110 |
1166
+| Principles of Flight | 50 (QuizVDS) | 110 |
1167
+| **Total** | **510** | **981** |
1168
+
1169
+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.
1170
+
1171
+## Appendix C: SM-2 EF Delta at Each Quality Grade
1172
+
1173
+| Quality | EF Delta | Example: EF=2.5 → |
1174
+|---------|----------|-------------------|
1175
+| 5 | +0.10 | 2.60 |
1176
+| 4 | 0.00 | 2.50 |
1177
+| 3 | -0.14 | 2.36 |
1178
+| 2 | -0.32 | 2.18 |
1179
+| 1 | -0.54 | 1.96 |
1180
+| 0 | -0.80 | 1.70 |
1181
+
1182
+Formula: `delta = 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)`
tasks/research-api-security.md
....@@ -0,0 +1,200 @@
1
+# API Security Research: Content Protection for Glidr
2
+
3
+## Threat Model
4
+
5
+What we are protecting:
6
+- A question bank (981 questions with explanations) that represents significant editorial work
7
+- Not financial data, medical data, or personal information
8
+- Business goal: prevent competitors from bulk-downloading questions for free
9
+
10
+Realistic threats:
11
+1. **Casual scraping** — someone writes a script to download all content
12
+2. **Competitor copying** — another app developer bulk-downloads the question bank
13
+3. NOT a concern: sophisticated nation-state attackers, reverse engineers with unlimited resources
14
+
15
+Conclusion: We need "good enough" security, not military-grade protection. A determined attacker with the app binary can always extract keys, but we want to raise the bar above casual scraping.
16
+
17
+---
18
+
19
+## Security Options Evaluated
20
+
21
+### Option 1: API Key in App Binary (Basic, Recommended for v1)
22
+
23
+**How it works:**
24
+- A shared secret (e.g., `X-Glidr-Key: abc123...`) is embedded in the app
25
+- Server validates this header on all content endpoints
26
+- Key is obfuscated in the binary using compile-time string splitting or encryption
27
+
28
+**Implementation:**
29
+```dart
30
+// Store as environment variable injected at build time
31
+// Never hardcode in plain text in source
32
+const apiKey = String.fromEnvironment('GLIDR_API_KEY');
33
+
34
+// In HTTP client
35
+dio.options.headers['X-Glidr-Key'] = apiKey;
36
+```
37
+
38
+**Server side (PHP example):**
39
+```php
40
+$key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';
41
+if ($key !== getenv('GLIDR_API_KEY')) {
42
+ http_response_code(403);
43
+ die('Forbidden');
44
+}
45
+```
46
+
47
+**Pros:** Simple, zero infrastructure cost, stops casual scrapers
48
+**Cons:** Key is extractable from app binary by determined attacker
49
+**Verdict:** Sufficient for protecting a $49.99 exam app's question bank
50
+
51
+---
52
+
53
+### Option 2: Certificate Pinning
54
+
55
+**How it works:**
56
+- App validates that tekmidian.com presents the exact SSL certificate it expects
57
+- Prevents man-in-the-middle attacks even on compromised networks
58
+
59
+**Implementation in Dio:**
60
+```dart
61
+(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (client) {
62
+ client.badCertificateCallback = (cert, host, port) => false;
63
+ SecurityContext context = SecurityContext();
64
+ context.setTrustedCertificatesBytes(pinnedCertBytes);
65
+ return HttpClient(context: context);
66
+};
67
+```
68
+
69
+**Pros:** Prevents MITM interception of API key
70
+**Cons:** Certificate rotation causes app to break; requires app update when cert expires
71
+**Verdict:** Nice-to-have, add in v2 if abuse becomes a problem
72
+
73
+---
74
+
75
+### Option 3: Apple App Attest + Google Play Integrity
76
+
77
+**How it works:**
78
+- Apple/Google cryptographically verify the app is genuine and unmodified
79
+- Backend receives attestation token, validates with Apple/Google servers
80
+- Only genuine, unmodified app instances can download content
81
+
82
+**Pros:** Strongest protection available; cannot be bypassed without jailbreak
83
+**Cons:**
84
+ - Requires backend infrastructure to validate attestation tokens
85
+ - Apple App Attest has rate limits (free tier: limited attestations/day)
86
+ - Adds significant implementation complexity
87
+ - Overkill for a small hobby/niche app
88
+
89
+**Verdict:** Not needed for v1. Consider for v2 if piracy becomes a real problem.
90
+
91
+---
92
+
93
+### Option 4: JWT Tokens with Device Fingerprinting
94
+
95
+**How it works:**
96
+- App registers with backend using device ID + purchase receipt
97
+- Backend issues a JWT valid for this device
98
+- JWT is used for all content requests
99
+- Content is tied to a specific "account"
100
+
101
+**Pros:** Can revoke access per device; enables future account features
102
+**Cons:** Requires user accounts infrastructure; adds backend complexity
103
+**Verdict:** Not needed for v1 one-time purchase model with no accounts
104
+
105
+---
106
+
107
+## Recommended Security Architecture for Glidr v1
108
+
109
+### Layer 1: HTTPS (mandatory, baseline)
110
+All API traffic over HTTPS. tekmidian.com must have a valid SSL certificate.
111
+
112
+### Layer 2: API Key Header
113
+- Obfuscated compile-time constant injected via `--dart-define`
114
+- Never committed to source control
115
+- Stored in CI/CD environment variables
116
+- Rotatable by releasing a new app version
117
+
118
+### Layer 3: Rate Limiting on Server
119
+- Limit to 100 requests/hour per IP
120
+- Limit to 10 full-bank downloads per day globally
121
+- Simple nginx or PHP-level throttling
122
+
123
+### Layer 4: Content Structure (Defense in Depth)
124
+- Serve individual subject JSON files, not one giant file
125
+- App downloads only what it needs (avoids one-request full dump)
126
+
127
+### Layer 5: Monitoring
128
+- Server access logs reviewed periodically
129
+- Alert on unusual download patterns
130
+
131
+---
132
+
133
+## API Endpoint Design
134
+
135
+Base URL: `https://tekmidian.com/glidr/api/v1/`
136
+
137
+All requests require header: `X-Glidr-Key: {secret}`
138
+
139
+```
140
+GET /manifest.json
141
+ Response: { "version": "1.2.0", "subjects": { "air_law": "abc123hash", ... } }
142
+
143
+GET /subjects/{subject_id}.json
144
+ Response: Full subject JSON with all questions
145
+ Example: /subjects/air_law.json
146
+
147
+GET /figures/{filename}
148
+ Response: Image file (PNG or SVG)
149
+ Example: /figures/bazl_30_q08_ask21_speed_polar.png
150
+```
151
+
152
+### Manifest Response Example
153
+```json
154
+{
155
+ "version": "1.2.0",
156
+ "updated_at": "2026-03-15T00:00:00Z",
157
+ "subjects": {
158
+ "air_law": { "hash": "sha256:abc...", "question_count": 110, "size_bytes": 45000 },
159
+ "meteorology": { "hash": "sha256:def...", "question_count": 110, "size_bytes": 52000 }
160
+ },
161
+ "figures": {
162
+ "hash": "sha256:ghi...",
163
+ "count": 58,
164
+ "size_bytes": 4200000
165
+ }
166
+}
167
+```
168
+
169
+App caches subject hashes locally. On launch, compares cached hash vs remote manifest. Downloads only changed subjects.
170
+
171
+---
172
+
173
+## Server Setup on tekmidian.com
174
+
175
+Minimal PHP implementation:
176
+
177
+```
178
+/glidr/
179
+ api/
180
+ v1/
181
+ .htaccess # auth check + routing
182
+ auth.php # API key validation
183
+ manifest.json # pre-generated static file
184
+ subjects/
185
+ air_law.json
186
+ meteorology.json
187
+ ... (9 files)
188
+ figures/
189
+ *.png
190
+ *.svg
191
+```
192
+
193
+`.htaccess`:
194
+```apache
195
+RewriteEngine On
196
+RewriteCond %{HTTP:X-Glidr-Key} !={GLIDR_API_KEY}
197
+RewriteRule ^ - [F,L]
198
+```
199
+
200
+This is the simplest possible implementation — Apache validates the key before serving any file.
tasks/research-content-analysis.md
....@@ -0,0 +1,140 @@
1
+# Content Analysis: SPL Exam Question Files
2
+
3
+## Summary
4
+
5
+**Total questions: 510 (English) + matching French translation**
6
+
7
+| Subject | File | EN Questions | Notes |
8
+|---------|------|-------------|-------|
9
+| 01 Air Law | 01 - Air Law.md | 50 | Source: QuizVDS.it EASA ECQB-SPL |
10
+| 02 Aircraft General Knowledge | 02 - Aircraft General Knowledge.md | 50 | |
11
+| 03 Communications | 03 - Communications.md | 50 | |
12
+| 04 Flight Performance and Planning | 04 - Flight Performance and Planning.md | 30 | (shorter) |
13
+| 05 Human Performance | 05 - Human Performance.md | 50 | |
14
+| 06 Meteorology | 06 - Meteorology.md | 50 | |
15
+| 07 Navigation | 07 - Navigation.md | 80 | Incl. Swiss SFVS exercises |
16
+| 08 Operational Procedures | 08 - Operational Procedures.md | 50 | |
17
+| 09 Principles of Flight | 09 - Principles of Flight.md | 50 | |
18
+
19
+Note: SPL Exam Questions.md index page states the full bank has 981 questions total across all sources (QuizVDS, SFVS, BAZL mock exams). The files counted above represent the QuizVDS-sourced questions. Navigation has 141 total per index including Swiss exercises.
20
+
21
+## Question Format (Markdown)
22
+
23
+Each question follows this exact structure:
24
+
25
+```markdown
26
+### Q{N}: {Question text} ^q{N}
27
+> *[FR](../SPL Exam Questions FR/01 - Droit aérien.md#^q{N})*
28
+- A) {option}
29
+- B) {option}
30
+- C) {option}
31
+- D) {option}
32
+**Correct: {letter})**
33
+
34
+> **Explanation:** {detailed explanation paragraph}
35
+```
36
+
37
+### Key structural observations:
38
+- Questions use `### Q{N}:` headers with Obsidian block reference anchors `^q{N}`
39
+- Each English question links directly to its French counterpart via relative path
40
+- 4 answer options always labeled A, B, C, D
41
+- Correct answer on its own line: `**Correct: X)**`
42
+- Explanation in a blockquote immediately after
43
+- Some questions embed images using Obsidian wiki-link syntax: `![[figures/filename.png]]`
44
+- Some use standard markdown: `![Alt text](figures/filename.svg)`
45
+
46
+## Image/Figure References
47
+
48
+Two syntaxes in use:
49
+1. **Obsidian wiki-link**: `![[figures/bazl_30_q08_ask21_speed_polar.png]]` - used in BAZL mock exam questions
50
+2. **Standard markdown**: `![Earth Globe](figures/NAV-002-earth-globe.svg)` - used in custom-created figures
51
+
52
+### Figures directory: 58 total files
53
+- 8 SVG files (custom-drawn navigation diagrams)
54
+- 50 PNG files (BAZL official exam images: speed polars, charts, maps, airport diagrams)
55
+
56
+Images primarily appear in:
57
+- Navigation (SVG diagrams)
58
+- Flight Performance and Planning (BAZL speed polars, loading tables, approach charts, maps)
59
+- Operational Procedures (ground signals)
60
+
61
+## French Version
62
+
63
+- Identical structure to English
64
+- Same filenames (French titles): `01 - Droit aérien.md`, `06 - Météorologie.md`, etc.
65
+- Same question count confirmed for files checked (50 Air Law, 50 Meteorology)
66
+- Questions use `^q{N}` anchors matching English versions
67
+- Links back to English: `> *[EN](../SPL Exam Questions/01 - Air Law.md#^q1)*`
68
+- Shares the same `figures/` directory (images are language-neutral)
69
+
70
+## Recommended JSON Format for App
71
+
72
+```json
73
+{
74
+ "version": "1.0.0",
75
+ "updated_at": "2026-03-15T00:00:00Z",
76
+ "subjects": [
77
+ {
78
+ "id": "air_law",
79
+ "code": "01",
80
+ "name": {
81
+ "en": "Air Law",
82
+ "fr": "Droit aérien"
83
+ },
84
+ "questions": [
85
+ {
86
+ "id": "air_law_q1",
87
+ "number": 1,
88
+ "text": {
89
+ "en": "The holder of an SPL license...",
90
+ "fr": "Le titulaire d'une licence SPL..."
91
+ },
92
+ "options": {
93
+ "en": {
94
+ "A": "Winch and bungee.",
95
+ "B": "Winch, bungee and aero-tow.",
96
+ "C": "Winch and aero-tow.",
97
+ "D": "Aero-tow and bungee."
98
+ },
99
+ "fr": {
100
+ "A": "Treuil et sandow.",
101
+ "B": "...",
102
+ "C": "...",
103
+ "D": "..."
104
+ }
105
+ },
106
+ "correct": "A",
107
+ "explanation": {
108
+ "en": "Under Part-SFCL...",
109
+ "fr": "Selon la Part-SFCL..."
110
+ },
111
+ "figures": []
112
+ },
113
+ {
114
+ "id": "pfp_q14",
115
+ "number": 14,
116
+ "text": { "en": "...", "fr": "..." },
117
+ "options": { "en": {...}, "fr": {...} },
118
+ "correct": "B",
119
+ "explanation": { "en": "...", "fr": "..." },
120
+ "figures": [
121
+ {
122
+ "filename": "bazl_30_q14_discus_loading_table.png",
123
+ "type": "png",
124
+ "alt": "Discus loading table"
125
+ }
126
+ ]
127
+ }
128
+ ]
129
+ }
130
+ ]
131
+}
132
+```
133
+
134
+### Key design decisions:
135
+- Bilingual content embedded in single question object (not separate files)
136
+- `figures` array per question (empty array if no figures)
137
+- `id` format: `{subject_code}_{qN}` for stable cross-referencing
138
+- `version` + `updated_at` at top level for update detection
139
+- Correct answer stored as letter string "A"/"B"/"C"/"D"
140
+- Subjects have both `id` (slug) and `code` (01-09 for ordering)
tasks/research-supermemo-sm2.md
....@@ -0,0 +1,172 @@
1
+# SuperMemo SM-2 Algorithm Research
2
+
3
+## The SM-2 Algorithm (Complete Technical Specification)
4
+
5
+Source: https://super-memory.com/english/ol/sm2.htm
6
+
7
+### Per-Card State Variables
8
+
9
+Each card (flashcard item) tracks three values:
10
+- `n` — repetition number (count of successful reviews, starts at 0)
11
+- `EF` — easiness factor (starts at 2.5, minimum 1.3)
12
+- `I` — current interval in days (time until next review)
13
+
14
+### Interval Schedule
15
+
16
+```
17
+I(1) = 1 # after first successful review: 1 day
18
+I(2) = 6 # after second successful review: 6 days
19
+I(n) = I(n-1) * EF # for n > 2: multiply previous interval by EF
20
+```
21
+
22
+Fractional intervals are rounded up to the nearest integer day.
23
+
24
+### Quality Grades (0–5 scale)
25
+
26
+| Grade | Meaning |
27
+|-------|---------|
28
+| 5 | Perfect response — no hesitation |
29
+| 4 | Correct after hesitation |
30
+| 3 | Correct with serious difficulty |
31
+| 2 | Incorrect — correct answer seemed easy to recall after seeing it |
32
+| 1 | Incorrect — correct answer remembered after seeing it |
33
+| 0 | Complete blackout — no recollection |
34
+
35
+### Easiness Factor Adjustment Formula
36
+
37
+After each review:
38
+```
39
+EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))
40
+```
41
+
42
+Where `q` is the quality grade (0–5). Results at each grade:
43
+- q=5: EF increases by +0.10
44
+- q=4: EF unchanged (delta = 0)
45
+- q=3: EF decreases by -0.14
46
+- q=2: EF decreases by -0.32
47
+- q=1: EF decreases by -0.54
48
+- q=0: EF decreases by -0.80
49
+
50
+EF is clamped to minimum 1.3. Items with EF below 1.3 were found to be "annoyingly often repeated" and indicate a poorly-formed question.
51
+
52
+### Reset Rule (Quality < 3)
53
+
54
+When a review scores 0, 1, or 2:
55
+- Reset repetition counter to n=1 (restart interval sequence: 1 day, then 6 days)
56
+- **Do NOT change the EF** (keep accumulated EF to influence future spacing)
57
+- Re-show the card the same session until it scores >= 4
58
+
59
+### Session Behavior
60
+
61
+Within a single study session:
62
+- All items scoring < 4 are re-shown in the same session
63
+- Session continues until all shown items score >= 4
64
+- New intervals only schedule for the NEXT day's review (not within-session repetitions)
65
+
66
+---
67
+
68
+## Practical Implementation Notes
69
+
70
+### Data Model Per Card
71
+
72
+```typescript
73
+interface CardProgress {
74
+ cardId: string;
75
+ repetitionNumber: number; // n (0 = never studied)
76
+ easinessFactor: number; // EF (default 2.5, min 1.3)
77
+ intervalDays: number; // I (days until next review)
78
+ nextReviewDate: string; // ISO date string
79
+ lastReviewDate: string | null;
80
+ totalReviews: number;
81
+ correctReviews: number;
82
+}
83
+```
84
+
85
+### Scheduling Logic
86
+
87
+```typescript
88
+function scheduleNextReview(card: CardProgress, quality: number): CardProgress {
89
+ let { n, EF, I } = card;
90
+
91
+ if (quality < 3) {
92
+ // Failed: reset sequence but keep EF
93
+ n = 0;
94
+ I = 1;
95
+ } else {
96
+ // Successful: advance sequence
97
+ if (n === 0) I = 1;
98
+ else if (n === 1) I = 6;
99
+ else I = Math.ceil(I * EF);
100
+ n += 1;
101
+ }
102
+
103
+ // Update EF (clamp to 1.3 minimum)
104
+ EF = Math.max(1.3, EF + 0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
105
+
106
+ const nextDate = addDays(today(), I);
107
+ return { ...card, n, EF, I, nextReviewDate: nextDate };
108
+}
109
+```
110
+
111
+---
112
+
113
+## Cram Mode
114
+
115
+Cram mode ignores all SM-2 scheduling — it simply shows all cards in a subject (or selected subset) sequentially or randomly, without affecting the SM-2 state. Two approaches:
116
+
117
+### Option A: Fully Isolated Cram
118
+- Cram sessions use a separate "cram_progress" table
119
+- SM-2 state for normal mode is untouched
120
+- Good for: pre-exam cramming without corrupting spacing data
121
+
122
+### Option B: Cram Reads but Doesn't Write SM-2
123
+- Cram shows cards from the full bank
124
+- Correct/incorrect tracked for session stats only
125
+- SM-2 nextReviewDate is not modified
126
+- Simpler implementation, recommended approach
127
+
128
+### Recommended Cram Mode UX
129
+- Show cards in random order within a subject
130
+- Show question → user answers → reveal correct answer + explanation
131
+- Show session score at end (X/Y correct, per-subject breakdown)
132
+- No self-rating needed (just correct/incorrect tap)
133
+
134
+---
135
+
136
+## Multi-Subject / Multi-Deck Handling
137
+
138
+Each subject is a separate "deck" but SM-2 state is unified per-card. Recommended approach:
139
+
140
+### Daily Review Queue
141
+- At app open: query all cards where `nextReviewDate <= today`
142
+- Group by subject for display
143
+- Allow "study all due" or "study due in [subject]"
144
+
145
+### New Card Introduction
146
+- Introduce N new cards per day per subject (configurable, default: 5)
147
+- New cards have n=0 and no scheduled date
148
+- Introduce after reviewing due cards (or before — user preference)
149
+
150
+### Statistics
151
+- Per-subject: total cards, due today, learned (n>=2), new
152
+- Overall retention rate (quality >= 3 / total reviews)
153
+- Streak tracking (days in a row with at least 1 review)
154
+
155
+---
156
+
157
+## Self-Assessment UX for SPL App
158
+
159
+Recommended: Use a simplified 3-button rating instead of the full 0-5 scale (less cognitive load for mobile):
160
+
161
+| Button | Label | Maps to SM-2 Grade |
162
+|--------|-------|--------------------|
163
+| Again | Didn't know it | 1 |
164
+| Hard | Knew it with difficulty | 3 |
165
+| Good | Knew it well | 4 |
166
+| Easy | Perfect recall | 5 |
167
+
168
+This is the same simplification Anki uses. The 4-button approach covers all meaningful SM-2 branches:
169
+- "Again" → reset (q=1)
170
+- "Hard" → decrease EF significantly (q=3)
171
+- "Good" → keep EF stable (q=4)
172
+- "Easy" → increase EF (q=5)
tasks/research-tech-stack.md
....@@ -0,0 +1,171 @@
1
+# Tech Stack Research: Cross-Platform Mobile Framework
2
+
3
+## Decision: Flutter
4
+
5
+**Recommendation: Flutter with Dart**
6
+
7
+### Justification
8
+
9
+| Criterion | Flutter | React Native | Winner |
10
+|-----------|---------|-------------|--------|
11
+| UI consistency | Custom renderer, pixel-perfect on all devices | Native widgets, slight platform differences | Flutter |
12
+| Performance | 60/120fps via Impeller engine, compiled to native | Improved via New Architecture (Fabric), near-native | Flutter (slight edge) |
13
+| Offline data (SQLite) | sqflite package, excellent | better-sqlite3/op-sqlite, good | Tie |
14
+| Image handling + zoom | photo_view package, InteractiveViewer widget | react-native-image-zoom-viewer, good | Tie |
15
+| App Store IAP | in_app_purchase plugin (official Flutter team) | react-native-iap, community | Flutter (official plugin) |
16
+| Remote content download | dio + path_provider, well-documented | react-native-fs + axios, good | Tie |
17
+| Learning curve | Dart (2-3 week ramp for new devs) | JavaScript/React (huge talent pool) | React Native |
18
+| iOS-first development | Excellent, first-class Xcode integration | Excellent | Tie |
19
+| Educational app examples | Many quiz/flashcard apps | Many | Tie |
20
+| Bundle size | Larger (~10MB base) | Smaller (~5MB base) | React Native |
21
+| Hot reload speed | Fast | Fast | Tie |
22
+
23
+**Key deciding factors for Glidr:**
24
+1. **Pixel-perfect UI** matters for zoomable aviation charts/diagrams
25
+2. **Official IAP plugin** reduces risk for the one-time purchase model
26
+3. **Flutter 46% market share** in 2025, strong momentum
27
+4. **No JavaScript bridge** for image rendering means smoother zoom/pan experience
28
+5. App is data-heavy but not UI-heavy — Flutter's widget library handles quiz UX cleanly
29
+
30
+---
31
+
32
+## Recommended Full Stack
33
+
34
+### Mobile App
35
+- **Framework**: Flutter 3.x (Dart)
36
+- **State Management**: Riverpod 2.x (Provider successor, better testability)
37
+- **Local Database**: SQLite via `sqflite` + `sqflite_common_ffi` for tests
38
+- **ORM/Query builder**: `drift` (type-safe SQLite layer, formerly Moor)
39
+- **HTTP Client**: `dio` with interceptors for auth headers
40
+- **Image zoom**: `photo_view` package
41
+- **IAP**: `in_app_purchase` (official Flutter plugin, supports both App Store + Play Store)
42
+- **Secure storage**: `flutter_secure_storage` (Keychain on iOS, EncryptedSharedPreferences on Android)
43
+- **Internationalization**: Flutter's built-in `intl` + ARB files (en, fr)
44
+- **JSON serialization**: `json_serializable` + `freezed` for immutable data classes
45
+
46
+### Backend (Content API)
47
+- **Hosting**: tekmidian.com — static file server or lightweight Node.js/PHP
48
+- **Language**: PHP 8.x or Node.js (whatever tekmidian.com already runs)
49
+- **Content delivery**: Pre-built JSON files served with auth header validation
50
+- **No database needed** for v1 — question bank is static JSON files per subject
51
+
52
+### Content Update Mechanism
53
+1. App checks `/api/v1/manifest.json` on launch (version hash per subject)
54
+2. If local version hash differs from remote: download updated subject JSON
55
+3. Questions stored in SQLite after first download
56
+4. Images bundled with app for v1 (to avoid complex asset downloading); OR served from CDN with local cache
57
+
58
+---
59
+
60
+## Local Database Schema (SQLite via Drift)
61
+
62
+### Tables
63
+
64
+```sql
65
+-- Question bank (populated from downloaded JSON)
66
+CREATE TABLE questions (
67
+ id TEXT PRIMARY KEY, -- "air_law_q1"
68
+ subject_id TEXT NOT NULL, -- "air_law"
69
+ number INTEGER NOT NULL,
70
+ text_en TEXT NOT NULL,
71
+ text_fr TEXT NOT NULL,
72
+ options_en TEXT NOT NULL, -- JSON: {"A":"...", "B":"...", "C":"...", "D":"..."}
73
+ options_fr TEXT NOT NULL,
74
+ correct TEXT NOT NULL, -- "A", "B", "C", or "D"
75
+ explanation_en TEXT NOT NULL,
76
+ explanation_fr TEXT NOT NULL,
77
+ figures TEXT NOT NULL -- JSON array: [{"filename":"...", "type":"png"}]
78
+);
79
+
80
+-- SM-2 progress per card
81
+CREATE TABLE card_progress (
82
+ question_id TEXT PRIMARY KEY,
83
+ repetition_number INTEGER NOT NULL DEFAULT 0,
84
+ easiness_factor REAL NOT NULL DEFAULT 2.5,
85
+ interval_days INTEGER NOT NULL DEFAULT 1,
86
+ next_review_date TEXT, -- ISO date "2026-03-16", NULL = new card
87
+ last_review_date TEXT,
88
+ total_reviews INTEGER NOT NULL DEFAULT 0,
89
+ correct_reviews INTEGER NOT NULL DEFAULT 0,
90
+ created_at TEXT NOT NULL,
91
+ updated_at TEXT NOT NULL
92
+);
93
+
94
+-- Study session log (for stats/history)
95
+CREATE TABLE study_sessions (
96
+ id TEXT PRIMARY KEY,
97
+ started_at TEXT NOT NULL,
98
+ ended_at TEXT,
99
+ mode TEXT NOT NULL, -- "spaced_repetition" | "cram"
100
+ subject_id TEXT, -- NULL = mixed/all
101
+ cards_reviewed INTEGER NOT NULL DEFAULT 0,
102
+ cards_correct INTEGER NOT NULL DEFAULT 0
103
+);
104
+
105
+-- Per-review log (for detailed analytics)
106
+CREATE TABLE review_log (
107
+ id TEXT PRIMARY KEY,
108
+ session_id TEXT NOT NULL,
109
+ question_id TEXT NOT NULL,
110
+ reviewed_at TEXT NOT NULL,
111
+ quality INTEGER NOT NULL, -- 0-5
112
+ time_to_answer_ms INTEGER, -- optional: track hesitation time
113
+ was_cram INTEGER NOT NULL DEFAULT 0 -- boolean
114
+);
115
+
116
+-- Content manifest (version tracking)
117
+CREATE TABLE content_manifest (
118
+ subject_id TEXT PRIMARY KEY,
119
+ version TEXT NOT NULL,
120
+ downloaded_at TEXT NOT NULL,
121
+ question_count INTEGER NOT NULL
122
+);
123
+
124
+-- App settings
125
+CREATE TABLE settings (
126
+ key TEXT PRIMARY KEY,
127
+ value TEXT NOT NULL
128
+);
129
+```
130
+
131
+---
132
+
133
+## Image Handling Strategy
134
+
135
+### Option A: Bundle All Images with App (Recommended for v1)
136
+- All 58 figures (50 PNG + 8 SVG) = estimated ~5-10 MB total
137
+- Bundle in Flutter assets: `assets/figures/`
138
+- Display with `photo_view` for pinch-zoom
139
+- Pro: works offline immediately, no download complexity
140
+- Con: app binary is larger, updating images requires app update
141
+
142
+### Option B: Download with Question Pack
143
+- Images served from tekmidian.com alongside JSON
144
+- Downloaded to local app documents directory
145
+- Cached permanently
146
+- Pro: images can be updated without app release
147
+- Con: more complex implementation, first-run requires download
148
+
149
+**Recommendation**: Bundle images in v1. Total size is small (<10MB). Move to Option B if the question bank grows significantly or if images need frequent correction.
150
+
151
+---
152
+
153
+## One-Time Purchase Model
154
+
155
+### App Store (iOS)
156
+- IAP type: **Non-Consumable** (purchased once, restored across devices)
157
+- Product ID: `com.tekmidian.glidr.fullaccess` (example)
158
+- Free tier: first 10 questions of each subject (preview)
159
+- Paid tier: full question bank (unlock all 9 subjects)
160
+- Restore purchases button required by App Store guidelines
161
+
162
+### Play Store (Android)
163
+- IAP type: **One-time product** (equivalent to non-consumable)
164
+- Same product unlock logic
165
+
166
+### Implementation with `in_app_purchase` plugin
167
+```dart
168
+// Check purchase status at app start
169
+final purchases = await InAppPurchase.instance.queryPastPurchases();
170
+final isUnlocked = purchases.any((p) => p.productID == 'com.tekmidian.glidr.fullaccess');
171
+```
tasks/restructure_explanations.py
....@@ -0,0 +1,275 @@
1
+#!/usr/bin/env python3
2
+"""
3
+Restructure explanation text in SPL exam question files to use markdown bullets
4
+for option analysis sentences.
5
+"""
6
+
7
+import re
8
+import os
9
+from pathlib import Path
10
+
11
+BASE_DIR = Path("/Users/i052341/Daten/Cloud/04 - Ablage/Ablage 2020 - 2029/Ablage 2025/Hobbies 2025/Segelflug/Theorie/Glidr")
12
+
13
+SUBJECT_DIRS = {
14
+ "EN": BASE_DIR / "SPL Exam Questions EN",
15
+ "DE": BASE_DIR / "SPL Exam Questions DE",
16
+ "FR": BASE_DIR / "SPL Exam Questions FR",
17
+}
18
+
19
+EXPLANATION_HEADERS = {
20
+ "EN": "#### Explanation",
21
+ "DE": "#### Erklärung",
22
+ "FR": "#### Explication",
23
+}
24
+
25
+KEY_TERMS_HEADERS = {
26
+ "EN": "#### Key Terms",
27
+ "DE": "#### Begriffe",
28
+ "FR": "#### Termes clés",
29
+}
30
+
31
+# Patterns that identify option-analysis sentences.
32
+# Each pattern must match the START of a sentence (after splitting on sentence boundaries).
33
+# Strategy: a sentence is an option sentence if it STARTS with an option reference.
34
+# This catches all forms: "Option A is...", "Option A (label) contains...", "Options A and B are...", etc.
35
+OPTION_PATTERNS = [
36
+ # EN/DE: sentence starts with "Option" or "Options" followed by one or more letters A-D
37
+ r"^Options?\s+[A-D]",
38
+ # EN/DE: "Die Option A" or "Nur Option A"
39
+ r"^(?:Die|Nur|Only)\s+Options?\s+[A-D]",
40
+ # EN bare letter: "A is wrong", "B is incorrect" (only when followed by a form of "to be")
41
+ r"^[A-D]\s+(?:is|are|was|were|would|can|cannot|does|did|has|have)\b",
42
+ # DE bare letter: "A ist falsch"
43
+ r"^[A-D]\s+(?:ist|sind|war|w\u00e4re)\b",
44
+ # FR: "L'option A" or "L\u2019option A"
45
+ r"^L['\u2019]?options?\s+[A-D]",
46
+ # FR bare letter: "A est incorrecte"
47
+ r"^[A-D]\s+est\b",
48
+ # Generic: "Option A:" or "Option A —" or "Option A–"
49
+ r"^Options?\s+[A-D](?:\s*(?:,|and|und|et)\s*(?:and\s+|und\s+|et\s+)?[A-D])*\s*(?::|—|–)",
50
+ # "Seule l'option C" (FR), "Nur Option C" (DE), "Only Option C" (EN) - correct answer callouts
51
+ r"^(?:Seule\s+l['\u2019]?option|Nur\s+Option|Only\s+Option)\s+[A-D]",
52
+]
53
+
54
+# Compiled combined pattern (case-sensitive)
55
+# We split by detecting sentence starts matching any option pattern.
56
+# Strategy: split the explanation text into sentences, then bucket them.
57
+
58
+def split_into_sentences(text):
59
+ """
60
+ Split text into sentences. Split on sentence-ending punctuation followed by
61
+ a space and a capital letter. Handles:
62
+ - ". Capital"
63
+ - '." Capital' (period inside quotes)
64
+ - ".'" / ".»" etc.
65
+ """
66
+ # Split on: period (optionally followed by closing quote/bracket) then whitespace then capital
67
+ parts = re.split(r'(?<=\.)["\'\u201d\u2019»]?\s+(?=[A-Z\xdc\xc4\xd6L\'"«\u201c\u2018])', text.strip())
68
+ return parts
69
+
70
+
71
+def is_option_sentence(sentence, lang):
72
+ """Return True if the sentence is an option-analysis sentence."""
73
+ s = sentence.strip()
74
+ for pat in OPTION_PATTERNS:
75
+ if re.match(pat, s, re.IGNORECASE):
76
+ return True
77
+ return False
78
+
79
+
80
+def bold_option_reference(sentence):
81
+ """
82
+ Bold the initial option reference in a sentence.
83
+ e.g. "Option A is wrong..." -> "**Option A** is wrong..."
84
+ "Option A (label) contains..." -> "**Option A** (label) contains..."
85
+ "Options A and B are..." -> "**Options A and B** are..."
86
+ "L'option A est..." -> "**L'option A** est..."
87
+ "Only Option C correctly..." -> "Only **Option C** correctly..."
88
+ "A is wrong" -> "**A** is wrong"
89
+ """
90
+ s = sentence.strip()
91
+
92
+ patterns_to_bold = [
93
+ # EN/DE multi: "Options A, B, and C" / "Options A und B" (must come before single)
94
+ (r'^(Options?\s+[A-D](?:\s*(?:,|and|und)\s*(?:and\s+|und\s+)?[A-D])+)', r'**\1**'),
95
+ # EN/DE single: "Option A"
96
+ (r'^(Options?\s+[A-D])\b', r'**\1**'),
97
+ # FR multi: "L'option A et B"
98
+ (r"^(L['\u2019]?options?\s+[A-D](?:\s*(?:,|et)\s*(?:et\s+)?[A-D])+)", r'**\1**'),
99
+ # FR single: "L'option A"
100
+ (r"^(L['\u2019]?options?\s+[A-D])\b", r'**\1**'),
101
+ # EN "Only Option C" -> "Only **Option C**"
102
+ (r'^(Only\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
103
+ # DE "Nur Option C"
104
+ (r'^(Nur\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
105
+ # FR "Seule l'option C"
106
+ (r"^(Seule\s+)(l['\u2019]?options?\s+[A-D])\b", r'\1**\2**'),
107
+ # DE "Die Option A"
108
+ (r'^(Die\s+)(Options?\s+[A-D])\b', r'\1**\2**'),
109
+ # Bare letter: "A is", "B est"
110
+ (r'^([A-D])(\s+(?:is|are|ist|sind|est|sont|was|w\u00e4re|serait)\b)', r'**\1**\2'),
111
+ ]
112
+
113
+ for pat, repl in patterns_to_bold:
114
+ new_s = re.sub(pat, repl, s, count=1, flags=re.IGNORECASE)
115
+ if new_s != s:
116
+ return new_s
117
+
118
+ return s
119
+
120
+
121
+def restructure_explanation(explanation_text, lang):
122
+ """
123
+ Given the raw explanation text (without the header line), restructure it:
124
+ - Sentences before the first option-analysis sentence -> main paragraph
125
+ - Option-analysis sentences -> bullet points
126
+ Returns (new_text, was_changed: bool)
127
+ """
128
+ text = explanation_text.strip()
129
+ if not text:
130
+ return text, False
131
+
132
+ sentences = split_into_sentences(text)
133
+ if not sentences:
134
+ return text, False
135
+
136
+ # Find index of first option sentence
137
+ first_option_idx = None
138
+ for i, s in enumerate(sentences):
139
+ if is_option_sentence(s, lang):
140
+ first_option_idx = i
141
+ break
142
+
143
+ if first_option_idx is None:
144
+ # No option sentences found, leave as-is
145
+ return text, False
146
+
147
+ # Main paragraph: sentences before first option sentence
148
+ main_sentences = sentences[:first_option_idx]
149
+ option_sentences = sentences[first_option_idx:]
150
+
151
+ # Verify that option_sentences are indeed all option-like (some trailing sentences might not be)
152
+ # We keep going: once we see an option sentence, everything else goes to bullets
153
+ # (this matches the described format where option analysis is at the end)
154
+
155
+ main_paragraph = " ".join(s.strip() for s in main_sentences if s.strip())
156
+
157
+ bullets = []
158
+ for s in option_sentences:
159
+ s = s.strip()
160
+ if not s:
161
+ continue
162
+ # Remove trailing period for bullet, then re-add
163
+ s_clean = s.rstrip(".")
164
+ bolded = bold_option_reference(s_clean)
165
+ bullets.append(f"- {bolded}.")
166
+
167
+ if not bullets:
168
+ return text, False
169
+
170
+ # Build new text
171
+ parts = []
172
+ if main_paragraph:
173
+ parts.append(main_paragraph)
174
+ parts.append("") # blank line
175
+ parts.extend(bullets)
176
+
177
+ new_text = "\n".join(parts)
178
+
179
+ # Only report change if something actually changed
180
+ changed = new_text.strip() != text.strip()
181
+ return new_text, changed
182
+
183
+
184
+def process_file(filepath, lang, explanation_header, key_terms_header):
185
+ """
186
+ Process a single markdown file. Returns (content, count_restructured).
187
+ """
188
+ content = filepath.read_text(encoding="utf-8")
189
+ lines = content.split("\n")
190
+
191
+ count = 0
192
+ result_lines = []
193
+ i = 0
194
+
195
+ while i < len(lines):
196
+ line = lines[i]
197
+
198
+ # Check if this line is the explanation header
199
+ if line.strip() == explanation_header:
200
+ result_lines.append(line)
201
+ i += 1
202
+
203
+ # Collect blank lines after header
204
+ while i < len(lines) and lines[i].strip() == "":
205
+ result_lines.append(lines[i])
206
+ i += 1
207
+
208
+ # Collect the explanation body until we hit:
209
+ # - key terms header
210
+ # - next question (### Q)
211
+ # - end of file
212
+ explanation_body_lines = []
213
+ while i < len(lines):
214
+ l = lines[i]
215
+ if (l.strip() == key_terms_header or
216
+ l.strip().startswith("### Q") or
217
+ l.strip().startswith("#### ")):
218
+ break
219
+ explanation_body_lines.append(l)
220
+ i += 1
221
+
222
+ # The explanation body (trim trailing blanks)
223
+ raw_body = "\n".join(explanation_body_lines).rstrip()
224
+
225
+ new_body, changed = restructure_explanation(raw_body, lang)
226
+
227
+ if changed:
228
+ count += 1
229
+ result_lines.append(new_body)
230
+ result_lines.append("") # trailing blank line
231
+ else:
232
+ # Restore original lines
233
+ for bl in explanation_body_lines:
234
+ result_lines.append(bl)
235
+
236
+ else:
237
+ result_lines.append(line)
238
+ i += 1
239
+
240
+ new_content = "\n".join(result_lines)
241
+ return new_content, count
242
+
243
+
244
+def main():
245
+ total_restructured = 0
246
+ total_files = 0
247
+
248
+ for lang, dir_path in SUBJECT_DIRS.items():
249
+ explanation_header = EXPLANATION_HEADERS[lang]
250
+ key_terms_header = KEY_TERMS_HEADERS[lang]
251
+
252
+ md_files = sorted(dir_path.glob("*.md"))
253
+ for filepath in md_files:
254
+ # Skip the combined file
255
+ if "SPL Exam Questions" in filepath.name:
256
+ continue
257
+
258
+ new_content, count = process_file(
259
+ filepath, lang, explanation_header, key_terms_header
260
+ )
261
+
262
+ if count > 0:
263
+ filepath.write_text(new_content, encoding="utf-8")
264
+ print(f" [{lang}] {filepath.name}: {count} explanation(s) restructured")
265
+ total_restructured += count
266
+ else:
267
+ print(f" [{lang}] {filepath.name}: no changes")
268
+
269
+ total_files += 1
270
+
271
+ print(f"\nDone. {total_restructured} explanations restructured across {total_files} files.")
272
+
273
+
274
+if __name__ == "__main__":
275
+ main()
tasks/todo.md
....@@ -0,0 +1,117 @@
1
+# Glidr - TODO
2
+
3
+## Session 2026-03-16: Xcode Build + App Store Submission
4
+
5
+### What Was Done
6
+
7
+#### Xcode Project Setup
8
+- Created Glidr.xcodeproj / Glidr.xcworkspace (renamed from Runner)
9
+- All Runner references replaced with Glidr in: pbxproj, Podfile, xcscheme, xcworkspacedata, xcconfig files, storyboards
10
+- Team ID: 7KU642K5ZL, Bundle ID: com.tekmidian.glidr
11
+- Signing: Automatic with Apple Development: Matthias Nott
12
+- Created Runner.xcscheme symlink -> Glidr.xcscheme (needed for flutter CLI)
13
+
14
+#### Build Issues Fixed
15
+- Homebrew rsync 3.4.1 incompatible with Xcode distribution (causes "Copy failed")
16
+ - Fix: `mv /opt/homebrew/bin/rsync /opt/homebrew/bin/rsync.bak` before distribution, restore after
17
+- Icon alpha channels: App Store rejects transparent icons
18
+ - Fix: Python PIL script removes alpha, saves as RGB
19
+- Overflow UI indicator ("RFLOWED BY" text): leadingWidth too small in home_screen.dart
20
+ - Fix: Wrapped in ClipRect + OverflowBox, increased leadingWidth to 110
21
+
22
+#### App Store Connect (Apple ID: 6760631689)
23
+- App name: Glider Pilot (Glidr was taken)
24
+- Apple account: mn@mnsoft.org
25
+- Build 1.0.0 uploaded successfully (with dSYM warning - non-blocking)
26
+- Pricing: FREE ($0.00) - changed from initial $49.99 mistake
27
+- In-App Purchase created:
28
+ - Name: Full Access
29
+ - Product ID: com.tekmidian.glidr.fullaccess
30
+ - Type: Non-Consumable
31
+ - Price: $49.99 (all 175 regions)
32
+ - Localization: "Full Access - All Questions" / "Unlock all 950 SPL exam questions in EN/FR/DE."
33
+- Category: Education
34
+- Age Rating: 4+ (all "None" for all categories)
35
+- Content Rights: No third-party content
36
+- Encryption: None (no custom encryption)
37
+- 7 screenshots uploaded (resized from 1320x2868 to 1284x2778 for 6.5" display)
38
+- Description, promotional text, keywords, support URL, marketing URL all filled
39
+- Build attached to version, compliance handled
40
+- Review contact: Matthias Nott, mn@mnsoft.org, +41 79 000 0000 (placeholder)
41
+- Review notes filled
42
+
43
+#### App Store Submission Blockers (remaining)
44
+- [x] Privacy Policy URL - set to https://youdrill.com/glidr/privacy.html
45
+- [x] Content Rights - set to "no third-party content"
46
+- [ ] iPad 13-inch screenshot - created at screenshots/ipad/ipad_home.png but NOT uploaded yet
47
+- [ ] App Privacy practices - need to click "Get Started" and fill in (app collects no data)
48
+- [ ] Agreement Update dialog keeps appearing - may need to re-accept developer agreement
49
+- [ ] Submit for review (click "Add for Review")
50
+
51
+#### Docker / youdrill.com Server
52
+- Created youdrill-website container (nginx:alpine) alongside existing youdrill-api (node:22-alpine)
53
+- docker-compose.yml at /opt/data/youdrill/docker-compose.yml - has both services
54
+- Traefik config at /opt/data/traefik/dynamic/youdrill.yaml - routes /glidr/ to website, rest to API
55
+- Static files at /opt/data/youdrill/website/
56
+ - /glidr/privacy.html - privacy policy (live, verified 200)
57
+ - /glidr/support.html - support page
58
+ - /glidr/index.html - landing page
59
+ - /index.html - root (for healthcheck)
60
+- Container is healthy
61
+
62
+#### Code Changes Made
63
+- ios/Glidr/Info.plist: CFBundleDisplayName changed to "Glider Pilot"
64
+- lib/screens/home_screen.dart: title changed to "Glider Pilot", leading wrapped in ClipRect/OverflowBox
65
+- create_icon.py: OUTPUT_DIR path updated from Runner to Glidr
66
+- ios/Glidr.xcodeproj/xcshareddata/xcschemes/Glidr.xcscheme: LaunchAction buildConfiguration changed to Release
67
+- Runner.xcscheme symlink created pointing to Glidr.xcscheme
68
+
69
+#### App Icon
70
+- Smiling glider plane icon restored from git history (commit a8a0c56)
71
+- Alpha channels removed from all 15 icon PNGs for App Store compliance
72
+- Icons in ios/Glidr/Assets.xcassets/AppIcon.appiconset/
73
+
74
+### Known Issues
75
+- App crashes on iPhone restart when deployed via Xcode Cmd+R (development signing issue)
76
+ - Dev-signed apps have get-task-allow=true, iOS 26 kills process without debugger
77
+ - Fix: Use flutter build ipa --release + devicectl install, or TestFlight
78
+ - flutter build ipa --release --no-pub (then xcrun devicectl device install app)
79
+- Homebrew rsync must be temporarily moved aside for Xcode distribution
80
+- Scheme rename to "Glider Pilot" broke flutter CLI - reverted to "Glidr" with Runner symlink
81
+- Simulators must be shut down before flutter device detection (xcrun simctl shutdown all)
82
+- flutter run --release hangs on device detection with Xcode 26 beta
83
+
84
+### Deploy Commands
85
+```bash
86
+# Build release IPA
87
+cd /Users/i052341/dev/apps/glidr
88
+flutter build ipa --release --no-pub
89
+
90
+# Install on iPhone
91
+xcrun devicectl device install app --device 00008150-001609EA3CEA401C build/ios/ipa/glidr.ipa
92
+
93
+# Archive and distribute (Xcode GUI)
94
+# 1. Move homebrew rsync: mv /opt/homebrew/bin/rsync /opt/homebrew/bin/rsync.bak
95
+# 2. Open Glidr.xcworkspace, set destination to "Any iOS Device"
96
+# 3. Product > Archive
97
+# 4. Distribute App > App Store Connect > Distribute
98
+# 5. Restore rsync: mv /opt/homebrew/bin/rsync.bak /opt/homebrew/bin/rsync
99
+```
100
+
101
+## Previously Completed (2026-03-15)
102
+
103
+- [x] Flutter app built and deployed to iPhone (release build)
104
+- [x] 950 questions across 9 subjects in EN/FR/DE
105
+- [x] SM-2 spaced repetition, cram mode, browse all, statistics
106
+- [x] Smiling glider app icon
107
+- [x] Content sync from youdrill.com
108
+- [x] Articles system
109
+- [x] Converter handles all 3 languages
110
+
111
+## Next Up
112
+- [ ] Upload iPad screenshot to ASC
113
+- [ ] Complete App Privacy practices in ASC
114
+- [ ] Submit for App Review
115
+- [ ] Update phone number in review contact
116
+- [ ] Test release build on iPhone (standalone restart)
117
+- [ ] TestFlight testing
xcode_initial.png
Binary files differ