.gitignore
.. .. @@ -0,0 +1,5 @@ 1 +.DS_Store2 +.obsidian3 +__pycache__4 +*.pyc5 +Glidr.md
.. .. @@ -0,0 +1,6 @@ 1 +---2 +icloud-sync: true3 +icloud-sync-exclude:4 + - "app/"5 +---6 +SPL Exam Questions FR/figures/t10_q114 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q70 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q77 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q88 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t10_q94 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q103 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q87 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q90 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t20_q96 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q19 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q20 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q21 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q27 2.svgdeleted 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.svgdeleted 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.svgdeleted 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.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q40 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q41 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q44 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q46 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q47 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q57 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q58 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q59 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q61 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q62 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q63 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q68 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q69 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q72 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q75 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q77 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q80 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q81 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q82 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q83 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q88 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q91 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q92 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q93 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q94 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q95 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t30_q96 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q111 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q114 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t40_q48 2.svgdeleted 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 curve23 - 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=5526 - A: (90, 270) low arousal, low performance27 - B: (260, 55) peak28 - C: (360, 140) high arousal, declining29 - D: (430, 270) very high, very low30 -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 up34 - CP2: (355, 55) holding it up then falling35 - -->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*43055 - = 0.125*90 + 0.375*155 + 0.375*355 + 0.125*43056 - = 11.25 + 58.125 + 133.125 + 53.75 = 256.2557 - y = 0.125*270 + 0.375*55 + 0.375*55 + 0.125*27058 - = 33.75 + 20.625 + 20.625 + 33.75 = 108.7559 - 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 = 36073 - Rough estimate: t~0.73 gives x~36074 - y at t=0.73: 0.0219*270 + 3*0.0729*0.73*55 + 3*0.27*0.5329*55 + 0.389*27075 - = 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.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q101 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q103 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q107 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q129 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q131 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q145 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q162 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q179 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q182 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q200 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q57 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q61 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q65 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q66 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q71 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q73 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q76 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q77 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q82 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q90 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q92 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t50_q93 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q153 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q161 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q164 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q167 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q171 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q46 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t60_q6 2.svgdeleted 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.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q111 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q112 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q113 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q123 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q129 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q132 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q150 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q151 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q152 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q66 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q75 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q87 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q90 2.pngdeleted file mode 100644Binary files differ
SPL Exam Questions FR/figures/t80_q95 2.pngdeleted file mode 100644Binary files differ
app/glidr
.. .. @@ -0,0 +1 @@ 1 +/Users/i052341/dev/apps/glidrarticles/how-it-works.md
.. .. @@ -0,0 +1,67 @@ 1 +# How Glidr Helps You Learn2 +3 +## The Problem with Cramming4 +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 Repetition10 +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 Algorithm16 +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 Everything22 +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 Effect33 +34 +A card rated "Good" might be scheduled for:35 +36 +- First review: 1 day37 +- Second review: 6 days38 +- Third review: ~15 days39 +- Fourth review: ~38 days40 +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 Mode44 +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 Mode50 +- Night before the exam: Cram Mode51 +- New chapter preview: Cram, then switch to Study52 +53 +## Tips for SPL Exam Prep54 +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 Algorithm2 +3 +*By Gary Wolf, Wired Magazine, April 2008*4 +5 +## The Man Behind SuperMemo6 +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 Everything14 +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 Works22 +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 Curve26 +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 Secret32 +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 Algorithm38 +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 Memorization44 +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 Matters50 +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 Strength54 +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 Study62 +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 Computers68 +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 Life74 +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 Flashcards80 +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 Genius86 +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: /apps12 + - listitem [ref=e26]:13 + - link "Analytics" [ref=e27] [cursor=pointer]:14 + - /url: /analytics15 + - listitem [ref=e28]:16 + - link "Trends" [ref=e29] [cursor=pointer]:17 + - /url: /trends18 + - listitem [ref=e30]:19 + - link "Reports" [ref=e31] [cursor=pointer]:20 + - /url: /itc/payments_and_financial_reports21 + - listitem [ref=e32]:22 + - link "Business" [ref=e33] [cursor=pointer]:23 + - /url: /business24 + - listitem [ref=e34]:25 + - link "Users and Access" [ref=e35] [cursor=pointer]:26 + - /url: /access/users27 + - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:28 + - generic:29 + - generic: Matthias Nott30 + - generic: Matthias Nott31 + - 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]: Agreements38 + - 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 their43 + - link "account" [active] [ref=e59] [cursor=pointer]:44 + - /url: https://developer.apple.com/account45 + - 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 8A51 + - paragraph [ref=e67]: Troistorrents, Valais 187252 + - paragraph [ref=e68]: Switzerland53 + - generic [ref=e69]:54 + - paragraph [ref=e71]: "85427149"55 + - generic [ref=e73]:56 + - paragraph [ref=e74]: 175 Countries or Regions57 + - 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 Regions75 + - 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 Regions87 + - 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 Accounts96 + - 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]: USD117 + - 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 Forms124 + - 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]: Active143 + - 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 Act159 + - cell "27 Countries or Regions View" [ref=e190]:160 + - generic [ref=e192]:161 + - paragraph [ref=e193]: 27 Countries or Regions162 + - 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]: Active167 + - 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: /apps174 + - 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/termsOfService179 + - text: "|"180 + - listitem [ref=e16]:181 + - link "Privacy Policy" [ref=e17] [cursor=pointer]:182 + - /url: https://www.apple.com/legal/privacy183 + - text: "|"184 + - listitem [ref=e18]:185 + - link "Contact Us" [ref=e19] [cursor=pointer]:186 + - /url: /contact-usfix_explanation_formatting.py
.. .. @@ -0,0 +1,254 @@ 1 +#!/usr/bin/env python32 +"""3 +Fix explanation formatting in SPL exam question files.4 +5 +Converts parenthetical option references like "(A)" in prose sentences6 +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 re17 +import os18 +import glob19 +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 sentence39 + starts with an uppercase letter (including accented chars).40 + """41 + parts = re.split(r'(?<=\w)\.\s+(?=[A-ZÀÂÄÈÉÊËÎÏÔÙÛÜÇ])', text)42 + return parts43 +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 + continue52 + 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 alone70 + if stripped.startswith('- ') or stripped.startswith('* '):71 + return text72 +73 + # No option references - leave it alone74 + if not OPTION_REF_PATTERN.search(text):75 + return text76 +77 + # Split into sentences78 + sentences = split_into_sentences(stripped)79 +80 + if len(sentences) <= 1:81 + # Single sentence - just bold the option refs82 + return bold_option_refs(text)83 +84 + # Count how many sentences have option refs85 + 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 inline89 + return bold_option_refs(text)90 +91 + # Multiple sentences have option refs - convert them to bullets92 + 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 text102 + if intro_sentences:103 + output_lines.append(join_sentences(intro_sentences))104 +105 + # Middle sentences (option-containing and any in between) as bullets106 + 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 text112 + 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 = 0125 +126 + while i < len(lines):127 + line = lines[i]128 +129 + # Empty line - keep as is130 + if not line.strip():131 + result.append(line)132 + i += 1133 + continue134 +135 + # Already a bullet line - keep as is136 + if line.strip().startswith('- ') or line.strip().startswith('* '):137 + result.append(line)138 + i += 1139 + continue140 +141 + # Header line - keep as is142 + if line.strip().startswith('#'):143 + result.append(line)144 + i += 1145 + continue146 +147 + # Regular text line - collect into a paragraph148 + para_lines = []149 + while i < len(lines):150 + current = lines[i]151 + # Stop at empty lines, bullets, or headers152 + if not current.strip():153 + break154 + if current.strip().startswith('- ') or current.strip().startswith('* '):155 + break156 + if current.strip().startswith('#'):157 + break158 + para_lines.append(current)159 + i += 1160 +161 + if not para_lines:162 + i += 1163 + continue164 +165 + # Join the paragraph lines and process166 + 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 result173 +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 = 0183 + i = 0184 +185 + while i < len(lines):186 + line = lines[i]187 +188 + # Check if this is an explanation header189 + if re.match(r'^#### (Explanation|Erklärung|Explication)\s*$', line.strip()):190 + result_lines.append(line)191 + i += 1192 +193 + # Collect lines until next #### or ### header194 + explanation_lines = []195 + while i < len(lines):196 + current = lines[i]197 + if re.match(r'^####? ', current) or re.match(r'^### ', current):198 + break199 + explanation_lines.append(current)200 + i += 1201 +202 + # Process the explanation block203 + processed = process_explanation_block(explanation_lines)204 +205 + # Count if there was a change206 + if explanation_lines != processed:207 + changes_made += 1208 +209 + result_lines.extend(processed)210 + else:211 + result_lines.append(line)212 + i += 1213 +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_made221 +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 = 0232 + total_changes = 0233 +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 files239 + if filename.startswith("SPL Exam Questions"):240 + continue241 +242 + changes = process_file(filepath)243 + total_files += 1244 + total_changes += changes245 +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: /apps12 + - listitem [ref=e26]:13 + - link "Analytics" [ref=e27] [cursor=pointer]:14 + - /url: /analytics15 + - listitem [ref=e28]:16 + - link "Trends" [ref=e29] [cursor=pointer]:17 + - /url: /trends18 + - listitem [ref=e30]:19 + - link "Reports" [ref=e31] [cursor=pointer]:20 + - /url: /itc/payments_and_financial_reports21 + - listitem [ref=e32]:22 + - link "Business" [ref=e33] [cursor=pointer]:23 + - /url: /business24 + - listitem [ref=e34]:25 + - link "Users and Access" [ref=e35] [cursor=pointer]:26 + - /url: /access/users27 + - button "Matthias Nott Matthias Nott Account name menu" [ref=e37] [cursor=pointer]:28 + - generic:29 + - generic: Matthias Nott30 + - generic: Matthias Nott31 + - 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 Pilot38 + - 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/distribution44 + - listitem [ref=e61]:45 + - link "TestFlight" [ref=e62] [cursor=pointer]:46 + - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/testflight47 + - listitem [ref=e63]:48 + - link "Xcode Cloud" [ref=e64] [cursor=pointer]:49 + - /url: /teams/69a6de75-2050-47e3-e053-5b8c7c11a4d1/apps/6760631689/ci50 + - 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/inflight61 + - generic [ref=e84]:62 + - img [ref=e85]63 + - text: 1.0 Prepare for Submission64 + - 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/info73 + - generic [ref=e96]: App Information74 + - listitem [ref=e97]:75 + - link "App Review" [ref=e98] [cursor=pointer]:76 + - /url: /apps/6760631689/distribution/reviewsubmissions77 + - generic [ref=e99]: App Review78 + - listitem [ref=e100]:79 + - link "History" [ref=e101] [cursor=pointer]:80 + - /url: /apps/6760631689/distribution/activity/ios/versions81 + - generic [ref=e102]: History82 + - 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/privacy92 + - generic [ref=e114]: App Privacy93 + - listitem [ref=e115]:94 + - link "App Accessibility" [ref=e116] [cursor=pointer]:95 + - /url: /apps/6760631689/distribution/accessibility96 + - generic [ref=e117]: App Accessibility97 + - listitem [ref=e118]:98 + - link "Ratings and Reviews" [ref=e119] [cursor=pointer]:99 + - /url: /apps/6760631689/distribution/ratings/ios100 + - generic [ref=e120]: Ratings and Reviews101 + - 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/events107 + - generic [ref=e127]: In-App Events108 + - listitem [ref=e128]:109 + - link "Custom Product Pages" [ref=e129] [cursor=pointer]:110 + - /url: /apps/6760631689/distribution/productpages111 + - generic [ref=e130]: Custom Product Pages112 + - listitem [ref=e131]:113 + - link "Product Page Optimization" [ref=e132] [cursor=pointer]:114 + - /url: /apps/6760631689/distribution/optimization115 + - generic [ref=e133]: Product Page Optimization116 + - listitem [ref=e134]:117 + - link "Promo Codes" [ref=e135] [cursor=pointer]:118 + - /url: /apps/6760631689/distribution/promo_codes/generate119 + - generic [ref=e136]: Promo Codes120 + - listitem [ref=e137]:121 + - link "Game Center" [ref=e138] [cursor=pointer]:122 + - /url: /apps/6760631689/distribution/gamecenter123 + - generic [ref=e139]: Game Center124 + - 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/pricing130 + - generic [ref=e146]: Pricing and Availability131 + - listitem [ref=e147]:132 + - link "In-App Purchases" [ref=e148] [cursor=pointer]:133 + - /url: /apps/6760631689/distribution/iaps134 + - generic [ref=e149]: In-App Purchases135 + - listitem [ref=e150]:136 + - link "Subscriptions" [ref=e151] [cursor=pointer]:137 + - /url: /apps/6760631689/distribution/subscriptions138 + - generic [ref=e152]: Subscriptions139 + - 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/nominations145 + - generic [ref=e159]: Nominations146 + - 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 URL163 + - 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 URL168 + - 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: /apps184 + - 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/termsOfService189 + - text: "|"190 + - listitem [ref=e16]:191 + - link "Privacy Policy" [ref=e17] [cursor=pointer]:192 + - /url: https://www.apple.com/legal/privacy193 + - text: "|"194 + - listitem [ref=e18]:195 + - link "Contact Us" [ref=e19] [cursor=pointer]:196 + - /url: /contact-usscreenshots/IMG_0737.PNGBinary files differ
screenshots/IMG_0738.PNGBinary files differ
screenshots/IMG_0739.PNGBinary files differ
screenshots/IMG_0740.PNGBinary files differ
screenshots/IMG_0741.PNGBinary files differ
screenshots/IMG_0742.PNGBinary files differ
screenshots/IMG_0743.PNGBinary files differ
screenshots/ipad/ipad_home.pngBinary files differ
tasks/PRD-Glidr.md
.. .. @@ -0,0 +1,1182 @@ 1 +# PRD: Glidr — SPL Exam Preparation App2 +3 +**Version:** 1.04 +**Date:** 2026-03-155 +**Status:** Ready for Implementation6 +**Author:** Atlas (Principal Software Architect)7 +8 +---9 +10 +## 1. Executive Summary11 +12 +### 1.1 Product Overview13 +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 Users19 +20 +- Primary: German/Swiss/European glider pilot students preparing for SPL theoretical exam21 +- Secondary: Existing pilots refreshing knowledge or preparing for recurrency checks22 +- Language: English and French (bilingual, selectable in-app)23 +24 +### 1.3 Success Metrics25 +26 +- App Store rating >= 4.5 stars27 +- >= 80% of purchased users complete at least one full subject review cycle28 +- Content update delivery: corrections visible to users within 24 hours of server update29 +- Exam pass rate correlation: users who complete >= 3 full cycles pass exam at >= 90%30 +31 +### 1.4 Timeline Estimate32 +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 Requirements46 +47 +### 2.1 Functional Requirements48 +49 +#### FR-01: Content Display50 +- 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 questions53 +- Figures must be zoomable via pinch gesture and double-tap54 +- Display correct answer and explanation after user selects an answer55 +- Explanation collapses/expands on tap56 +57 +#### FR-02: Subject Navigation58 +- List all 9 subjects on home screen with progress indicators59 +- Each subject shows: total cards, due today, learned cards, new cards60 +- Tap subject to begin study session61 +62 +#### FR-03: Spaced Repetition Mode63 +- Implement SuperMemo SM-2 algorithm exactly as specified64 +- Present cards due today in review sessions65 +- After revealing answer, user rates recall using 4-button self-assessment: Again / Hard / Good / Easy66 +- SM-2 state (n, EF, interval) updated immediately after each rating67 +- Session ends when all due cards have been reviewed68 +- Show session summary: cards reviewed, correct count, next due date69 +70 +#### FR-04: Cram Mode71 +- Accessible from each subject screen72 +- Shows ALL cards in a subject (ignoring SM-2 schedule)73 +- User answers, then sees correct answer + explanation74 +- No self-assessment rating in cram mode75 +- Cram progress does not modify SM-2 state76 +- Session score shown at end (X/Y correct)77 +78 +#### FR-05: Content Updates79 +- On each app launch: check remote manifest for content version changes80 +- If new version available: download updated subject JSON in background81 +- Update local SQLite database with new/changed questions82 +- Show user notification when update is available and downloaded83 +- Full offline operation after initial download84 +85 +#### FR-06: Purchase / Unlock86 +- Free tier: first 10 questions of each subject are accessible without purchase87 +- Paid tier: one-time purchase unlocks all 9 subjects fully88 +- Restore Purchases button in Settings89 +- Purchase state persisted in flutter_secure_storage90 +91 +#### FR-07: Language Selection92 +- Language toggle (EN / FR) accessible from Settings and from subject screen93 +- Switching language immediately updates all displayed content94 +- Language preference persisted across app sessions95 +96 +#### FR-08: Progress and Statistics97 +- 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: Settings103 +- Language selection (EN / FR)104 +- New cards per day per subject (1-20, default 5)105 +- Review reminder notification (optional, time picker)106 +- Restore purchases107 +- App version + build number108 +109 +### 2.2 Non-Functional Requirements110 +111 +#### NFR-01: Performance112 +- App launch to home screen: < 1.5 seconds (cold start)113 +- Question display including image: < 300ms114 +- SM-2 state save after rating: < 50ms115 +- Content manifest check: non-blocking (background thread)116 +117 +#### NFR-02: Offline Support118 +- Full functionality after initial content download (no network required)119 +- Initial download required only once; graceful offline handling after120 +121 +#### NFR-03: Platform Support122 +- 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: Accessibility127 +- Dynamic Type support (text scales with system font size)128 +- VoiceOver / TalkBack support for all interactive elements129 +- Minimum tap target size: 44x44pt130 +131 +#### NFR-05: Data Privacy132 +- No user accounts, no personal data collected133 +- No analytics in v1 (add opt-in analytics in v2)134 +- Purchase state stored locally only135 +- GDPR-compliant: no EU data transfer136 +137 +---138 +139 +## 3. System Architecture140 +141 +### 3.1 High-Level Architecture142 +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 header168 + ▼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 Architecture181 +182 +```183 +lib/184 +├── main.dart185 +├── app.dart # MaterialApp, routing, theme186 +├── core/187 +│ ├── constants.dart # API base URL, API key (from --dart-define)188 +│ ├── router.dart # go_router route definitions189 +│ └── theme.dart # Color scheme, typography190 +├── data/191 +│ ├── database/192 +│ │ ├── database.dart # Drift database definition193 +│ │ ├── tables/ # Drift table definitions194 +│ │ └── daos/ # Data access objects per domain195 +│ ├── models/ # Freezed immutable data classes196 +│ │ ├── question.dart197 +│ │ ├── card_progress.dart198 +│ │ ├── study_session.dart199 +│ │ └── subject.dart200 +│ ├── repositories/ # Repository interfaces + implementations201 +│ │ ├── question_repository.dart202 +│ │ ├── progress_repository.dart203 +│ │ └── content_sync_repository.dart204 +│ └── api/205 +│ ├── glidr_api_client.dart # Dio client with auth interceptor206 +│ └── models/ # API response models207 +├── domain/208 +│ └── sm2/209 +│ ├── sm2_algorithm.dart # Pure SM-2 calculation functions210 +│ └── review_scheduler.dart # Queue building logic211 +├── presentation/212 +│ ├── home/213 +│ │ ├── home_screen.dart214 +│ │ └── home_provider.dart215 +│ ├── study/216 +│ │ ├── study_screen.dart # SM-2 review session217 +│ │ ├── study_provider.dart218 +│ │ └── widgets/219 +│ │ ├── question_card.dart220 +│ │ ├── answer_options.dart221 +│ │ ├── rating_buttons.dart222 +│ │ └── figure_viewer.dart # photo_view zoomable image223 +│ ├── cram/224 +│ │ ├── cram_screen.dart225 +│ │ └── cram_provider.dart226 +│ ├── subject_detail/227 +│ │ ├── subject_detail_screen.dart228 +│ │ └── subject_detail_provider.dart229 +│ ├── stats/230 +│ │ ├── stats_screen.dart231 +│ │ └── stats_provider.dart232 +│ └── settings/233 +│ ├── settings_screen.dart234 +│ └── settings_provider.dart235 +└── l10n/236 + ├── app_en.arb237 + └── app_fr.arb238 +```239 +240 +---241 +242 +## 4. Data Models243 +244 +### 4.1 Question JSON Format (Remote API)245 +246 +Each subject is served as a single JSON file. Example structure:247 +248 +```json249 +{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 Format318 +319 +```json320 +{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 +```sql341 +-- 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 card357 +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 sessions372 +CREATE TABLE study_sessions (373 + id TEXT NOT NULL PRIMARY KEY, -- UUID374 + started_at TEXT NOT NULL, -- ISO 8601375 + ended_at TEXT,376 + mode TEXT NOT NULL, -- "spaced_repetition" | "cram"377 + subject_id TEXT, -- NULL = mixed378 + cards_reviewed INTEGER NOT NULL DEFAULT 0,379 + cards_correct INTEGER NOT NULL DEFAULT 0380 +);381 +382 +-- Individual review events383 +CREATE TABLE review_log (384 + id TEXT NOT NULL PRIMARY KEY, -- UUID385 + session_id TEXT NOT NULL,386 + question_id TEXT NOT NULL,387 + reviewed_at TEXT NOT NULL, -- ISO 8601388 + quality INTEGER NOT NULL, -- 0-5 (SM-2 grade) or -1 for cram389 + 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 tracking396 +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 NULL402 +);403 +404 +-- App settings (key-value)405 +CREATE TABLE settings (406 + key TEXT NOT NULL PRIMARY KEY,407 + value TEXT NOT NULL408 +);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 Implementation421 +422 +### 5.1 Core Algorithm423 +424 +```dart425 +// lib/domain/sm2/sm2_algorithm.dart426 +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 blackout442 +/// Grade 1: incorrect, remembered on seeing answer443 +/// Grade 2: incorrect, correct seemed easy after444 +/// Grade 3: correct with serious difficulty445 +/// Grade 4: correct after hesitation446 +/// Grade 5: perfect response447 +SM2Result computeNextInterval({448 + required int currentRepetitionNumber, // n449 + required double currentEasinessFactor, // EF450 + required int currentIntervalDays, // I451 + required int quality, // 0-5452 +}) {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 EF461 + n = 0;462 + interval = 1;463 + } else {464 + // Successful review: advance interval465 + 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 Queue503 +504 +```dart505 +// lib/domain/sm2/review_scheduler.dart506 +507 +Future<List<Question>> buildDailyQueue({508 + required String? subjectId, // null = all subjects509 + 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 new527 + dueCards.shuffle();528 + newCards.shuffle();529 +530 + return [...dueCards, ...newCards];531 +}532 +```533 +534 +### 5.4 Cram Mode535 +536 +```dart537 +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 state547 +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 stats558 + wasCram: true,559 + ));560 + // card_progress is intentionally NOT updated561 +}562 +```563 +564 +---565 +566 +## 6. API Design567 +568 +### 6.1 Base Configuration569 +570 +```571 +Base URL: https://tekmidian.com/glidr/api/v1572 +Auth Header: X-Glidr-Key: {compile-time-injected-secret}573 +Content-Type: application/json574 +```575 +576 +### 6.2 Endpoints577 +578 +#### GET /manifest.json579 +Returns the current version hash for all subjects. Used to detect if local content needs updating.580 +581 +Response:582 +```json583 +{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}.json595 +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 Handling608 +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 Flow621 +622 +```623 +App Launch624 + │625 + ├── Is local content available?626 + │ NO → Show "Downloading content..." screen627 + │ Download all 9 subjects + figures sequentially628 + │ Show progress bar629 + │ On complete → proceed to Home630 + │631 + │ YES → Proceed to Home immediately632 + │ Background: check manifest633 + │ │634 + │ ├── Hash differs for any subject?635 + │ │ YES → Download updated subject JSON636 + │ │ Update SQLite questions table637 + │ │ Show subtle "Content updated" banner638 + │ │639 + │ └── No changes → do nothing640 + │641 + └── Home Screen642 +```643 +644 +---645 +646 +## 7. UI Flows and Screens647 +648 +### 7.1 Screen Inventory649 +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 Layout664 +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 Flow691 +692 +```693 +1. Question Screen694 + ┌─────────────────────────────┐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 Screen733 + ┌─────────────────────────────┐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 Flow745 +746 +```747 +1. Subject screen → [Cram Mode] button748 +2. Cram session: question → tap to reveal → correct/incorrect tap749 +3. No self-rating buttons (just "Next" arrow)750 +4. Session end: score summary (X/Y)751 +```752 +753 +### 7.5 Figure Viewer754 +755 +When a question includes a figure:756 +- Figure displayed inline in the question card757 +- `photo_view` widget wraps the image758 +- Pinch-to-zoom with min/max scale (0.5x - 5.0x)759 +- Double-tap resets to fit760 +- Single-tap on figure expands to full-screen overlay761 +- Full-screen overlay: same zoom behavior + close button (X)762 +763 +---764 +765 +## 8. Technology Stack766 +767 +### 8.1 Dependencies (pubspec.yaml)768 +769 +```yaml770 +dependencies:771 + flutter:772 + sdk: flutter773 +774 + # State management775 + flutter_riverpod: ^2.5.0776 + riverpod_annotation: ^2.3.0777 +778 + # Database779 + drift: ^2.18.0780 + sqlite3_flutter_libs: ^0.5.0781 + path_provider: ^2.1.0782 + path: ^1.9.0783 +784 + # HTTP785 + dio: ^5.4.0786 +787 + # Immutable data models788 + freezed_annotation: ^2.4.0789 + json_annotation: ^4.9.0790 +791 + # Image zoom792 + photo_view: ^0.15.0793 +794 + # In-app purchase795 + in_app_purchase: ^3.1.0796 +797 + # Secure storage (API key, purchase state)798 + flutter_secure_storage: ^9.0.0799 +800 + # Navigation801 + go_router: ^13.0.0802 +803 + # UUID generation804 + uuid: ^4.4.0805 +806 + # Crypto (SHA-256 for manifest verification)807 + crypto: ^3.0.0808 +809 + # SVG rendering810 + flutter_svg: ^2.0.0811 +812 + # Internationalization813 + flutter_localizations:814 + sdk: flutter815 + intl: ^0.19.0816 +817 +dev_dependencies:818 + flutter_test:819 + sdk: flutter820 + drift_dev: ^2.18.0821 + riverpod_generator: ^2.3.0822 + build_runner: ^2.4.0823 + freezed: ^2.4.0824 + json_serializable: ^6.7.0825 + flutter_lints: ^4.0.0826 +```827 +828 +### 8.2 Flutter Configuration829 +830 +```831 +flutter:832 + assets:833 + - assets/figures/ # All 58 figure files (PNG + SVG)834 + - assets/fonts/ # Custom fonts if used835 +836 + generate: true # Enable l10n generation837 +```838 +839 +Build-time injection of API key (never in source):840 +```bash841 +flutter build ios --dart-define=GLIDR_API_KEY=your_secret_here842 +flutter build appbundle --dart-define=GLIDR_API_KEY=your_secret_here843 +```844 +845 +---846 +847 +## 9. Backend Setup (tekmidian.com)848 +849 +### 9.1 Directory Structure850 +851 +```852 +/var/www/tekmidian.com/public_html/glidr/853 +├── api/854 +│ └── v1/855 +│ ├── .htaccess # Auth + CORS headers856 +│ ├── manifest.json # Regenerated on each content update857 +│ ├── subjects/858 +│ │ ├── air_law.json859 +│ │ ├── aircraft_general_knowledge.json860 +│ │ ├── communications.json861 +│ │ ├── flight_performance.json862 +│ │ ├── human_performance.json863 +│ │ ├── meteorology.json864 +│ │ ├── navigation.json865 +│ │ ├── operational_procedures.json866 +│ │ └── principles_of_flight.json867 +│ └── figures/868 +│ ├── *.png (50 files)869 +│ └── *.svg (8 files)870 +└── tools/871 + └── generate_manifest.php # CLI tool to regenerate manifest.json872 +```873 +874 +### 9.2 .htaccess Auth Configuration875 +876 +```apache877 +# /glidr/api/v1/.htaccess878 +Options -Indexes879 +880 +# CORS for potential web use881 +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 variable885 +SetEnvIf HTTP_X_GLIDR_KEY "^your_secret_here$" GLIDR_AUTH=1886 +Order Deny,Allow887 +Deny from all888 +Allow from env=GLIDR_AUTH889 +890 +# Better: use a PHP wrapper for auth to avoid key in .htaccess891 +```892 +893 +Better approach — PHP auth wrapper:894 +895 +```php896 +<?php897 +// auth.php — included by all JSON-serving endpoints898 +$provided_key = $_SERVER['HTTP_X_GLIDR_KEY'] ?? '';899 +$expected_key = getenv('GLIDR_API_KEY'); // Set in Apache/server environment900 +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 Workflow910 +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 files915 +3. Upload new JSON files to server via SFTP/rsync916 +4. Run `generate_manifest.php` to update version hashes917 +5. App users receive update on next launch918 +919 +---920 +921 +## 10. Implementation Checklists922 +923 +### 10.1 Phase 1: Foundation (3 weeks)924 +925 +#### Database & Models926 +- [ ] Set up Flutter project with all dependencies listed in Section 8.1927 +- [ ] Create Drift database with all tables from Section 4.3928 +- [ ] Generate Drift DAOs for questions, card_progress, review_log, settings929 +- [ ] Create Freezed data models for Question, CardProgress, StudySession930 +- [ ] Write unit tests for all DAOs931 +932 +#### Content Download933 +- [ ] Implement Dio HTTP client with X-Glidr-Key auth interceptor934 +- [ ] Implement manifest.json fetch and local version comparison935 +- [ ] Implement subject JSON download with SHA-256 verification936 +- [ ] Implement SQLite import from downloaded JSON937 +- [ ] Implement first-run download screen with progress indicator938 +- [ ] Implement background update check on subsequent launches939 +940 +#### Question Display941 +- [ ] Implement question card widget (text + options)942 +- [ ] Implement figure viewer widget using photo_view943 +- [ ] Implement SVG rendering via flutter_svg944 +- [ ] Implement answer selection with color feedback (correct/incorrect)945 +- [ ] Implement explanation collapsible section946 +947 +#### App Shell948 +- [ ] Set up go_router with all routes from Section 7.1949 +- [ ] Implement Riverpod provider structure950 +- [ ] Implement home screen with subject list951 +- [ ] Implement subject detail screen952 +- [ ] Implement bilingual content switching (EN/FR)953 +954 +#### Testing Checklist — Phase 1955 +- [ ] Unit tests: JSON parsing for all 9 subjects956 +- [ ] Unit tests: SHA-256 manifest verification957 +- [ ] Unit tests: SQLite import idempotency (re-importing same data is safe)958 +- [ ] Widget tests: question card displays correctly for question with figure959 +- [ ] Widget tests: question card displays correctly for question without figure960 +- [ ] Integration test: full download flow on fresh install961 +- [ ] Manual test: figure zoom works on real device962 +963 +### 10.2 Phase 2: Learning Engine (2 weeks)964 +965 +#### SM-2 Algorithm966 +- [ ] Implement `computeNextInterval()` function (Section 5.1)967 +- [ ] Unit test all quality grades 0-5 with known expected outputs968 +- [ ] Unit test EF clamping at 1.3 minimum969 +- [ ] 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 days971 +972 +#### Review Queue973 +- [ ] Implement `buildDailyQueue()` — due cards + new cards (Section 5.3)974 +- [ ] Implement "new cards per day" limit from settings975 +- [ ] Implement queue shuffle976 +- [ ] Unit test queue: due cards before new cards977 +- [ ] Unit test queue: respects new card limit978 +979 +#### Study Session UI980 +- [ ] Implement study session screen with question display981 +- [ ] Implement progress bar (X of Y reviewed)982 +- [ ] Implement answer tap → reveal flow983 +- [ ] Implement 4-button rating row (Again / Hard / Good / Easy)984 +- [ ] On rating: call `computeNextInterval()`, persist to card_progress985 +- [ ] Log review to review_log table986 +- [ ] Implement within-session re-show for quality < 3 cards987 +- [ ] Implement session completion detection988 +- [ ] Implement session summary screen989 +990 +#### Testing Checklist — Phase 2991 +- [ ] Unit tests: all SM-2 branches (quality 0-5, all n values, EF edge cases)992 +- [ ] Unit tests: daily queue respects new card limit993 +- [ ] Integration test: complete a study session of 10 cards, verify DB state994 +- [ ] Integration test: rating "Again" re-shows card in same session995 +- [ ] Integration test: session completion triggers summary screen996 +- [ ] Manual test: study 20 cards in one session, check review_log entries997 +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 state1004 +- [ ] Log cram reviews to review_log with was_cram=11005 +- [ ] Implement cram session summary screen1006 +1007 +#### Testing Checklist — Phase 31008 +- [ ] Unit test: cram review does not modify card_progress1009 +- [ ] Integration test: cram session records in review_log with was_cram=11010 +- [ ] Integration test: SM-2 state unchanged after cram session1011 +1012 +### 10.4 Phase 4: Purchase + Polish (2 weeks)1013 +1014 +#### In-App Purchase1015 +- [ ] Create non-consumable IAP product in App Store Connect1016 +- [ ] Create one-time product in Google Play Console1017 +- [ ] Implement `in_app_purchase` purchase flow1018 +- [ ] Implement "Restore Purchases" button1019 +- [ ] Persist unlock state in flutter_secure_storage1020 +- [ ] Implement paywall screen with subject preview (first 10 questions free)1021 +- [ ] Gate subject access: > Q10 requires purchase1022 +1023 +#### Statistics Screen1024 +- [ ] Implement per-subject stats: new / learning / review / mature counts1025 +- [ ] Implement overall retention rate calculation1026 +- [ ] Implement study streak counter1027 +- [ ] Implement "exam readiness" indicator per subject1028 +1029 +#### Onboarding1030 +- [ ] Implement first-run onboarding (3-4 screens: welcome, how it works, language, purchase/trial)1031 +- [ ] Implement first-run content download trigger1032 +1033 +#### UI Polish1034 +- [ ] 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 elements1037 +- [ ] Dynamic Type: test at all font sizes1038 +- [ ] Dark mode support1039 +1040 +#### Testing Checklist — Phase 41041 +- [ ] Manual test: purchase flow on Sandbox environment (iOS)1042 +- [ ] Manual test: restore purchases after app delete and reinstall1043 +- [ ] Manual test: free tier correctly limits to 10 questions per subject1044 +- [ ] Accessibility audit: VoiceOver navigate entire study session1045 +- [ ] Manual test: dark mode on iOS and Android1046 +1047 +### 10.5 Phase 5: Backend (1 week)1048 +1049 +- [ ] Provision `tekmidian.com/glidr/api/v1/` directory1050 +- [ ] Set GLIDR_API_KEY server environment variable1051 +- [ ] Write and deploy `.htaccess` auth rules1052 +- [ ] Write Markdown-to-JSON conversion script (Python or PHP)1053 +- [ ] Convert all 9 subject Markdown files to JSON format1054 +- [ ] Convert French Markdown files and merge into bilingual JSON1055 +- [ ] Copy all figures to server1056 +- [ ] Write and run `generate_manifest.php` to create initial manifest.json1057 +- [ ] Test all endpoints with curl (with and without API key)1058 +- [ ] Test rate limiting (verify 429 on abuse)1059 +- [ ] Document update workflow in a README1060 +1061 +#### Security Checklist — Phase 51062 +- [ ] HTTPS with valid SSL certificate on tekmidian.com1063 +- [ ] 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 traces1066 +- [ ] Test: direct URL access without API key returns 4031067 +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 S211072 +- [ ] TestFlight beta (iOS): 5-10 beta testers1073 +- [ ] Beta feedback incorporated1074 +- [ ] App Store listing: screenshots, description (EN + FR), keywords1075 +- [ ] App Store Review Guidelines compliance check1076 +- [ ] Privacy Policy published (required for App Store)1077 +- [ ] Submit for App Store review1078 +- [ ] Google Play closed testing track1079 +- [ ] Submit for Play Store review1080 +1081 +---1082 +1083 +## 11. Content Conversion Tool1084 +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 Purpose1088 +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.11091 +1092 +### 11.2 Parsing Logic1093 +1094 +The script must handle:1095 +1. Parse `### Q{N}: {text} ^q{N}` as question text and number1096 +2. Parse `- A) ... B) ... C) ... D) ...` as answer options1097 +3. Parse `**Correct: {letter})**` as correct answer1098 +4. Parse `> **Explanation:** {text}` as explanation1099 +5. Parse `![[figures/{filename}]]` and `` as figure references1100 +6. Match each English question to its French counterpart by question number1101 +7. Combine into bilingual JSON structure1102 +1103 +### 11.3 Script Location1104 +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 Assessment1112 +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/server1127 +- 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 tracking1130 +- Opt-in analytics: understand which questions users find hardest1131 +- Push notifications: study reminders1132 +- Certificate pinning: harden API against MITM attacks1133 +- Apple App Attest: prevent automated bulk downloading1134 +- Widget: iOS home screen widget showing "cards due today"1135 +- Apple Watch: quick review on watch1136 +- Web version: study on desktop browser (Flutter Web)1137 +1138 +---1139 +1140 +## Appendix A: Subject IDs Reference1141 +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 Reference1155 +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 Grade1172 +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 Glidr2 +3 +## Threat Model4 +5 +What we are protecting:6 +- A question bank (981 questions with explanations) that represents significant editorial work7 +- Not financial data, medical data, or personal information8 +- Business goal: prevent competitors from bulk-downloading questions for free9 +10 +Realistic threats:11 +1. **Casual scraping** — someone writes a script to download all content12 +2. **Competitor copying** — another app developer bulk-downloads the question bank13 +3. NOT a concern: sophisticated nation-state attackers, reverse engineers with unlimited resources14 +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 Evaluated20 +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 app25 +- Server validates this header on all content endpoints26 +- Key is obfuscated in the binary using compile-time string splitting or encryption27 +28 +**Implementation:**29 +```dart30 +// Store as environment variable injected at build time31 +// Never hardcode in plain text in source32 +const apiKey = String.fromEnvironment('GLIDR_API_KEY');33 +34 +// In HTTP client35 +dio.options.headers['X-Glidr-Key'] = apiKey;36 +```37 +38 +**Server side (PHP example):**39 +```php40 +$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 scrapers48 +**Cons:** Key is extractable from app binary by determined attacker49 +**Verdict:** Sufficient for protecting a $49.99 exam app's question bank50 +51 +---52 +53 +### Option 2: Certificate Pinning54 +55 +**How it works:**56 +- App validates that tekmidian.com presents the exact SSL certificate it expects57 +- Prevents man-in-the-middle attacks even on compromised networks58 +59 +**Implementation in Dio:**60 +```dart61 +(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 key70 +**Cons:** Certificate rotation causes app to break; requires app update when cert expires71 +**Verdict:** Nice-to-have, add in v2 if abuse becomes a problem72 +73 +---74 +75 +### Option 3: Apple App Attest + Google Play Integrity76 +77 +**How it works:**78 +- Apple/Google cryptographically verify the app is genuine and unmodified79 +- Backend receives attestation token, validates with Apple/Google servers80 +- Only genuine, unmodified app instances can download content81 +82 +**Pros:** Strongest protection available; cannot be bypassed without jailbreak83 +**Cons:**84 + - Requires backend infrastructure to validate attestation tokens85 + - Apple App Attest has rate limits (free tier: limited attestations/day)86 + - Adds significant implementation complexity87 + - Overkill for a small hobby/niche app88 +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 Fingerprinting94 +95 +**How it works:**96 +- App registers with backend using device ID + purchase receipt97 +- Backend issues a JWT valid for this device98 +- JWT is used for all content requests99 +- Content is tied to a specific "account"100 +101 +**Pros:** Can revoke access per device; enables future account features102 +**Cons:** Requires user accounts infrastructure; adds backend complexity103 +**Verdict:** Not needed for v1 one-time purchase model with no accounts104 +105 +---106 +107 +## Recommended Security Architecture for Glidr v1108 +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 Header113 +- Obfuscated compile-time constant injected via `--dart-define`114 +- Never committed to source control115 +- Stored in CI/CD environment variables116 +- Rotatable by releasing a new app version117 +118 +### Layer 3: Rate Limiting on Server119 +- Limit to 100 requests/hour per IP120 +- Limit to 10 full-bank downloads per day globally121 +- Simple nginx or PHP-level throttling122 +123 +### Layer 4: Content Structure (Defense in Depth)124 +- Serve individual subject JSON files, not one giant file125 +- App downloads only what it needs (avoids one-request full dump)126 +127 +### Layer 5: Monitoring128 +- Server access logs reviewed periodically129 +- Alert on unusual download patterns130 +131 +---132 +133 +## API Endpoint Design134 +135 +Base URL: `https://tekmidian.com/glidr/api/v1/`136 +137 +All requests require header: `X-Glidr-Key: {secret}`138 +139 +```140 +GET /manifest.json141 + Response: { "version": "1.2.0", "subjects": { "air_law": "abc123hash", ... } }142 +143 +GET /subjects/{subject_id}.json144 + Response: Full subject JSON with all questions145 + Example: /subjects/air_law.json146 +147 +GET /figures/{filename}148 + Response: Image file (PNG or SVG)149 + Example: /figures/bazl_30_q08_ask21_speed_polar.png150 +```151 +152 +### Manifest Response Example153 +```json154 +{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": 4200000165 + }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.com174 +175 +Minimal PHP implementation:176 +177 +```178 +/glidr/179 + api/180 + v1/181 + .htaccess # auth check + routing182 + auth.php # API key validation183 + manifest.json # pre-generated static file184 + subjects/185 + air_law.json186 + meteorology.json187 + ... (9 files)188 + figures/189 + *.png190 + *.svg191 +```192 +193 +`.htaccess`:194 +```apache195 +RewriteEngine On196 +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 Files2 +3 +## Summary4 +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 +```markdown26 +### 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 path40 +- 4 answer options always labeled A, B, C, D41 +- Correct answer on its own line: `**Correct: X)**`42 +- Explanation in a blockquote immediately after43 +- Some questions embed images using Obsidian wiki-link syntax: `![[figures/filename.png]]`44 +- Some use standard markdown: ``45 +46 +## Image/Figure References47 +48 +Two syntaxes in use:49 +1. **Obsidian wiki-link**: `![[figures/bazl_30_q08_ask21_speed_polar.png]]` - used in BAZL mock exam questions50 +2. **Standard markdown**: `` - used in custom-created figures51 +52 +### Figures directory: 58 total files53 +- 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 Version62 +63 +- Identical structure to English64 +- 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 versions67 +- 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 App71 +72 +```json73 +{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-referencing138 +- `version` + `updated_at` at top level for update detection139 +- 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 Research2 +3 +## The SM-2 Algorithm (Complete Technical Specification)4 +5 +Source: https://super-memory.com/english/ol/sm2.htm6 +7 +### Per-Card State Variables8 +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 Schedule15 +16 +```17 +I(1) = 1 # after first successful review: 1 day18 +I(2) = 6 # after second successful review: 6 days19 +I(n) = I(n-1) * EF # for n > 2: multiply previous interval by EF20 +```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 Formula36 +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.1044 +- q=4: EF unchanged (delta = 0)45 +- q=3: EF decreases by -0.1446 +- q=2: EF decreases by -0.3247 +- q=1: EF decreases by -0.5448 +- q=0: EF decreases by -0.8049 +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 >= 458 +59 +### Session Behavior60 +61 +Within a single study session:62 +- All items scoring < 4 are re-shown in the same session63 +- Session continues until all shown items score >= 464 +- New intervals only schedule for the NEXT day's review (not within-session repetitions)65 +66 +---67 +68 +## Practical Implementation Notes69 +70 +### Data Model Per Card71 +72 +```typescript73 +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 string79 + lastReviewDate: string | null;80 + totalReviews: number;81 + correctReviews: number;82 +}83 +```84 +85 +### Scheduling Logic86 +87 +```typescript88 +function scheduleNextReview(card: CardProgress, quality: number): CardProgress {89 + let { n, EF, I } = card;90 +91 + if (quality < 3) {92 + // Failed: reset sequence but keep EF93 + n = 0;94 + I = 1;95 + } else {96 + // Successful: advance sequence97 + 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 Mode114 +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 Cram118 +- Cram sessions use a separate "cram_progress" table119 +- SM-2 state for normal mode is untouched120 +- Good for: pre-exam cramming without corrupting spacing data121 +122 +### Option B: Cram Reads but Doesn't Write SM-2123 +- Cram shows cards from the full bank124 +- Correct/incorrect tracked for session stats only125 +- SM-2 nextReviewDate is not modified126 +- Simpler implementation, recommended approach127 +128 +### Recommended Cram Mode UX129 +- Show cards in random order within a subject130 +- Show question → user answers → reveal correct answer + explanation131 +- 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 Handling137 +138 +Each subject is a separate "deck" but SM-2 state is unified per-card. Recommended approach:139 +140 +### Daily Review Queue141 +- At app open: query all cards where `nextReviewDate <= today`142 +- Group by subject for display143 +- Allow "study all due" or "study due in [subject]"144 +145 +### New Card Introduction146 +- Introduce N new cards per day per subject (configurable, default: 5)147 +- New cards have n=0 and no scheduled date148 +- Introduce after reviewing due cards (or before — user preference)149 +150 +### Statistics151 +- Per-subject: total cards, due today, learned (n>=2), new152 +- 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 App158 +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 Framework2 +3 +## Decision: Flutter4 +5 +**Recommendation: Flutter with Dart**6 +7 +### Justification8 +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/diagrams25 +2. **Official IAP plugin** reduces risk for the one-time purchase model26 +3. **Flutter 46% market share** in 2025, strong momentum27 +4. **No JavaScript bridge** for image rendering means smoother zoom/pan experience28 +5. App is data-heavy but not UI-heavy — Flutter's widget library handles quiz UX cleanly29 +30 +---31 +32 +## Recommended Full Stack33 +34 +### Mobile App35 +- **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 tests38 +- **ORM/Query builder**: `drift` (type-safe SQLite layer, formerly Moor)39 +- **HTTP Client**: `dio` with interceptors for auth headers40 +- **Image zoom**: `photo_view` package41 +- **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 classes45 +46 +### Backend (Content API)47 +- **Hosting**: tekmidian.com — static file server or lightweight Node.js/PHP48 +- **Language**: PHP 8.x or Node.js (whatever tekmidian.com already runs)49 +- **Content delivery**: Pre-built JSON files served with auth header validation50 +- **No database needed** for v1 — question bank is static JSON files per subject51 +52 +### Content Update Mechanism53 +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 JSON55 +3. Questions stored in SQLite after first download56 +4. Images bundled with app for v1 (to avoid complex asset downloading); OR served from CDN with local cache57 +58 +---59 +60 +## Local Database Schema (SQLite via Drift)61 +62 +### Tables63 +64 +```sql65 +-- 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 card81 +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 card87 + 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 NULL92 +);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/all101 + cards_reviewed INTEGER NOT NULL DEFAULT 0,102 + cards_correct INTEGER NOT NULL DEFAULT 0103 +);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-5112 + time_to_answer_ms INTEGER, -- optional: track hesitation time113 + was_cram INTEGER NOT NULL DEFAULT 0 -- boolean114 +);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 NULL122 +);123 +124 +-- App settings125 +CREATE TABLE settings (126 + key TEXT PRIMARY KEY,127 + value TEXT NOT NULL128 +);129 +```130 +131 +---132 +133 +## Image Handling Strategy134 +135 +### Option A: Bundle All Images with App (Recommended for v1)136 +- All 58 figures (50 PNG + 8 SVG) = estimated ~5-10 MB total137 +- Bundle in Flutter assets: `assets/figures/`138 +- Display with `photo_view` for pinch-zoom139 +- Pro: works offline immediately, no download complexity140 +- Con: app binary is larger, updating images requires app update141 +142 +### Option B: Download with Question Pack143 +- Images served from tekmidian.com alongside JSON144 +- Downloaded to local app documents directory145 +- Cached permanently146 +- Pro: images can be updated without app release147 +- Con: more complex implementation, first-run requires download148 +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 Model154 +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 guidelines161 +162 +### Play Store (Android)163 +- IAP type: **One-time product** (equivalent to non-consumable)164 +- Same product unlock logic165 +166 +### Implementation with `in_app_purchase` plugin167 +```dart168 +// Check purchase status at app start169 +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 python32 +"""3 +Restructure explanation text in SPL exam question files to use markdown bullets4 +for option analysis sentences.5 +"""6 +7 +import re8 +import os9 +from pathlib import Path10 +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-D37 + 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 callouts51 + 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 by61 + 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 capital67 + parts = re.split(r'(?<=\.)["\'\u201d\u2019»]?\s+(?=[A-Z\xdc\xc4\xd6L\'"«\u201c\u2018])', text.strip())68 + return parts69 +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 True77 + return False78 +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_s117 +118 + return s119 +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 paragraph125 + - Option-analysis sentences -> bullet points126 + Returns (new_text, was_changed: bool)127 + """128 + text = explanation_text.strip()129 + if not text:130 + return text, False131 +132 + sentences = split_into_sentences(text)133 + if not sentences:134 + return text, False135 +136 + # Find index of first option sentence137 + first_option_idx = None138 + for i, s in enumerate(sentences):139 + if is_option_sentence(s, lang):140 + first_option_idx = i141 + break142 +143 + if first_option_idx is None:144 + # No option sentences found, leave as-is145 + return text, False146 +147 + # Main paragraph: sentences before first option sentence148 + 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 bullets153 + # (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 + continue162 + # Remove trailing period for bullet, then re-add163 + s_clean = s.rstrip(".")164 + bolded = bold_option_reference(s_clean)165 + bullets.append(f"- {bolded}.")166 +167 + if not bullets:168 + return text, False169 +170 + # Build new text171 + parts = []172 + if main_paragraph:173 + parts.append(main_paragraph)174 + parts.append("") # blank line175 + parts.extend(bullets)176 +177 + new_text = "\n".join(parts)178 +179 + # Only report change if something actually changed180 + changed = new_text.strip() != text.strip()181 + return new_text, changed182 +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 = 0192 + result_lines = []193 + i = 0194 +195 + while i < len(lines):196 + line = lines[i]197 +198 + # Check if this line is the explanation header199 + if line.strip() == explanation_header:200 + result_lines.append(line)201 + i += 1202 +203 + # Collect blank lines after header204 + while i < len(lines) and lines[i].strip() == "":205 + result_lines.append(lines[i])206 + i += 1207 +208 + # Collect the explanation body until we hit:209 + # - key terms header210 + # - next question (### Q)211 + # - end of file212 + explanation_body_lines = []213 + while i < len(lines):214 + l = lines[i]215 + if (l.strip() == key_terms_header or216 + l.strip().startswith("### Q") or217 + l.strip().startswith("#### ")):218 + break219 + explanation_body_lines.append(l)220 + i += 1221 +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 += 1229 + result_lines.append(new_body)230 + result_lines.append("") # trailing blank line231 + else:232 + # Restore original lines233 + for bl in explanation_body_lines:234 + result_lines.append(bl)235 +236 + else:237 + result_lines.append(line)238 + i += 1239 +240 + new_content = "\n".join(result_lines)241 + return new_content, count242 +243 +244 +def main():245 + total_restructured = 0246 + total_files = 0247 +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 file255 + if "SPL Exam Questions" in filepath.name:256 + continue257 +258 + new_content, count = process_file(259 + filepath, lang, explanation_header, key_terms_header260 + )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 += count266 + else:267 + print(f" [{lang}] {filepath.name}: no changes")268 +269 + total_files += 1270 +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 - TODO2 +3 +## Session 2026-03-16: Xcode Build + App Store Submission4 +5 +### What Was Done6 +7 +#### Xcode Project Setup8 +- Created Glidr.xcodeproj / Glidr.xcworkspace (renamed from Runner)9 +- All Runner references replaced with Glidr in: pbxproj, Podfile, xcscheme, xcworkspacedata, xcconfig files, storyboards10 +- Team ID: 7KU642K5ZL, Bundle ID: com.tekmidian.glidr11 +- Signing: Automatic with Apple Development: Matthias Nott12 +- Created Runner.xcscheme symlink -> Glidr.xcscheme (needed for flutter CLI)13 +14 +#### Build Issues Fixed15 +- 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 after17 +- Icon alpha channels: App Store rejects transparent icons18 + - Fix: Python PIL script removes alpha, saves as RGB19 +- Overflow UI indicator ("RFLOWED BY" text): leadingWidth too small in home_screen.dart20 + - Fix: Wrapped in ClipRect + OverflowBox, increased leadingWidth to 11021 +22 +#### App Store Connect (Apple ID: 6760631689)23 +- App name: Glider Pilot (Glidr was taken)24 +- Apple account: mn@mnsoft.org25 +- Build 1.0.0 uploaded successfully (with dSYM warning - non-blocking)26 +- Pricing: FREE ($0.00) - changed from initial $49.99 mistake27 +- In-App Purchase created:28 + - Name: Full Access29 + - Product ID: com.tekmidian.glidr.fullaccess30 + - Type: Non-Consumable31 + - Price: $49.99 (all 175 regions)32 + - Localization: "Full Access - All Questions" / "Unlock all 950 SPL exam questions in EN/FR/DE."33 +- Category: Education34 +- Age Rating: 4+ (all "None" for all categories)35 +- Content Rights: No third-party content36 +- 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 filled39 +- Build attached to version, compliance handled40 +- Review contact: Matthias Nott, mn@mnsoft.org, +41 79 000 0000 (placeholder)41 +- Review notes filled42 +43 +#### App Store Submission Blockers (remaining)44 +- [x] Privacy Policy URL - set to https://youdrill.com/glidr/privacy.html45 +- [x] Content Rights - set to "no third-party content"46 +- [ ] iPad 13-inch screenshot - created at screenshots/ipad/ipad_home.png but NOT uploaded yet47 +- [ ] 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 agreement49 +- [ ] Submit for review (click "Add for Review")50 +51 +#### Docker / youdrill.com Server52 +- 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 services54 +- Traefik config at /opt/data/traefik/dynamic/youdrill.yaml - routes /glidr/ to website, rest to API55 +- Static files at /opt/data/youdrill/website/56 + - /glidr/privacy.html - privacy policy (live, verified 200)57 + - /glidr/support.html - support page58 + - /glidr/index.html - landing page59 + - /index.html - root (for healthcheck)60 +- Container is healthy61 +62 +#### Code Changes Made63 +- ios/Glidr/Info.plist: CFBundleDisplayName changed to "Glider Pilot"64 +- lib/screens/home_screen.dart: title changed to "Glider Pilot", leading wrapped in ClipRect/OverflowBox65 +- create_icon.py: OUTPUT_DIR path updated from Runner to Glidr66 +- ios/Glidr.xcodeproj/xcshareddata/xcschemes/Glidr.xcscheme: LaunchAction buildConfiguration changed to Release67 +- Runner.xcscheme symlink created pointing to Glidr.xcscheme68 +69 +#### App Icon70 +- Smiling glider plane icon restored from git history (commit a8a0c56)71 +- Alpha channels removed from all 15 icon PNGs for App Store compliance72 +- Icons in ios/Glidr/Assets.xcassets/AppIcon.appiconset/73 +74 +### Known Issues75 +- 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 debugger77 + - Fix: Use flutter build ipa --release + devicectl install, or TestFlight78 + - flutter build ipa --release --no-pub (then xcrun devicectl device install app)79 +- Homebrew rsync must be temporarily moved aside for Xcode distribution80 +- Scheme rename to "Glider Pilot" broke flutter CLI - reverted to "Glidr" with Runner symlink81 +- Simulators must be shut down before flutter device detection (xcrun simctl shutdown all)82 +- flutter run --release hangs on device detection with Xcode 26 beta83 +84 +### Deploy Commands85 +```bash86 +# Build release IPA87 +cd /Users/i052341/dev/apps/glidr88 +flutter build ipa --release --no-pub89 +90 +# Install on iPhone91 +xcrun devicectl device install app --device 00008150-001609EA3CEA401C build/ios/ipa/glidr.ipa92 +93 +# Archive and distribute (Xcode GUI)94 +# 1. Move homebrew rsync: mv /opt/homebrew/bin/rsync /opt/homebrew/bin/rsync.bak95 +# 2. Open Glidr.xcworkspace, set destination to "Any iOS Device"96 +# 3. Product > Archive97 +# 4. Distribute App > App Store Connect > Distribute98 +# 5. Restore rsync: mv /opt/homebrew/bin/rsync.bak /opt/homebrew/bin/rsync99 +```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/DE105 +- [x] SM-2 spaced repetition, cram mode, browse all, statistics106 +- [x] Smiling glider app icon107 +- [x] Content sync from youdrill.com108 +- [x] Articles system109 +- [x] Converter handles all 3 languages110 +111 +## Next Up112 +- [ ] Upload iPad screenshot to ASC113 +- [ ] Complete App Privacy practices in ASC114 +- [ ] Submit for App Review115 +- [ ] Update phone number in review contact116 +- [ ] Test release build on iPhone (standalone restart)117 +- [ ] TestFlight testingxcode_initial.pngBinary files differ