Phase 22.5 — The Language Abroad
05 JUL AT 08:39 PM

Phase 22.5 — The Language Abroad

0 LOVES 0 VIEWS
Flutter learn mode + behavior.

UTS Phase 22.5 — Flutter guide lightbox LEARN mode + behavior

Status: Ready (grounded 2026-07-02) · Depends on: 22.1 tokens, 22.2 atoms, 22.3 blocks, 22.4 shell+cook (11846f2e). Scope: the guide lightbox LEARN mode (course guides) — the last piece of the UTS Flutter parity track. Web is the authoritative reference (kon_guide_v2.css / _guide_lightbox.gohtml / guide-lightbox.js); the behavior target is the Phase-20 contract (phase-20-learn-completion-review.md, D20.1–D20.6), which the Flutter track "must mirror."

⚠️ 22.5 has TWO contracts running together: (A) VISUAL — restyle the 4 learn surfaces onto g-* (they currently render on legacy guideLightbox* tokens = the "mixed state" 22.4 left intentional); (B) BEHAVIOR — the Phase-20 nonsequential/honest-completion flip. Do NOT restyle a surface without also landing its behavior slice, or you get a pretty-but-wrong end page.

Method & rigor (lessons applied — read before trusting the surface/behavior sections below)

The surface + behavior descriptions in THIS doc are grep-grounded, not code-grounded — i.e. UNVERIFIED assumptions about the current Flutter learn state. The 22.3 lesson (an assumption-audit that confirms fields exist still missed the render structure) and the CLAUDE.md "don't assume then certify" rule both say misses hide exactly here. So 22.5 does NOT start by building:

  • Step 0 (MANDATORY, before any build) = a measured current-state + delta audit of the 5 learn files against BOTH contracts. Same method that worked in 22.4 (property-level, source-grounded): for each learn surface, read the actual Flutter file AND the web CSS/template/JS, enumerate every property/behavior, and classify MATCH / DIVERGE / DEAD / MISSING — for the VISUAL contract (vs kon_guide_v2.css) AND the BEHAVIOR contract (vs the Phase-20 D20.1–D20.6 points: which are ALREADY satisfied in Flutter vs missing). Write the delta to phase-22.5-learn-parity-audit.md. The build steps then target the MEASURED diverges — not this doc's assumptions. Confidence % must be earned from that measured set.
  • Per-surface done-gate = a property-level re-check vs source, THEN the eyeball smoke — NOT eyeball alone. 22.4 proved eyeballing misses systematic issues (the property audit caught 3 of my own misses the smoke would not have). A surface isn't "done" until its measured diverges are all closed on a clean re-run.
  • Verify load-bearing claims in code. Any claim that drives a decision (e.g. "canAdvance gates Next", "completion is presence-based") gets re-read at its file:line before it's acted on — the way the disc-glyph and canAdvance claims were re-verified this session.
  • 22.5 is FLUTTER-ONLY — NO backend / ent / schema change. The learn behavior (nonsequential, honest completion, resume) is computed CLIENT-SIDE from hydrated per-step state (Phase-20/21); the backend is unchanged and current_step_index stays vestigial (Phase-21 D21.2). If a "fix" seems to need a backend change, that's the signal to STOP (the ent-schema catch from 22.4) — align Flutter to the existing contract instead.
  • Dead tokens retired by hand (no scripts), rule = "0 refs outside design_tokens.dart", only as each surface actually moves to g-*.

What's already done (don't redo)

  • Shell (barrier/panel/close), masthead (GuideBand), metadata (GuideMetadataGrid), materials (flat MaterialRow) — all built in 22.4 as SHARED widgets. Learn's overview must ADOPT GuideBand+GuideMetadataGrid (it still renders the old cover/title/badge/stat + its own metadata grid).
  • Answer-row atomguide_lightbox_lesson_page.dart already uses AnswerOptionRow(lightbox:true) + verdict _optionState + centered brand "Submit Answer" (pulled forward in 22.4, smoked on the Mixology quiz). The rest of the lesson page (header/counter/title/body/Mark-Complete) is still legacy.
  • Bloc — learn events exist (GuideProgressPageChanged/ReviewEntered/ReviewExited); resumePage (first-incomplete, client-computed) exists; the #6 hydration fix + shared reset all apply.

Surfaces (current Flutter file → web target)

  1. Overview (guide_lightbox_overview.dart, 522 lines) — web .o-guide-lightbox-page[data-guide-page=0]: GuideBand masthead + GuideMetadataGrid (ADOPT) · About (.o-guide-lightbox-overview-about) · objective callout (.o-guide-lightbox-overview-objective, "What you'll learn") · materials (already shared) · adaptive CTA (.o-guide-lightbox-overview-start: Start/Continue/Review + {N} lessons count) · scroll hint. Currently blue COURSE/BEGINNER badges + blue objective box + old cover/title = all legacy.
  2. Lesson pages (guide_lightbox_lesson_page.dart, 502) — web .o-guide-lightbox-lesson-*: counter (Lesson N of M) · title · body (15/1.6/g-body) · qtype label · question (answer-row DONE) · Mark Complete button (.o-guide-lightbox-lesson-action, data-guide-lesson-complete). Restyle counter/title/body/qtype/mark-complete onto g-*.
  3. Completion (guide_lightbox_completion.dart, 132) — web .o-guide-lightbox-completion (data-guide-completion): trophy icon · title · score · Continue + Review All Lessons + Done. This is where the Phase-20 D20.1/D20.2/D20.5 behavior lives.
  4. Review (guide_lightbox_review.dart, 355) — web .o-guide-lightbox-review (separate DOM): per-lesson review rows (number disc, correct/incorrect, answer label, explanation). Restyle onto g-* (discs → StepDisc family colours).
  5. Footer nav (guide_lightbox_footer.dart learn branch _buildLearnFooter/_navButton/_buildReviewFooter) — prev/next nav buttons + learn progress fill (currently blue guideLightboxProgressFillLearng-ok green, web :387-388) + review "Exit Review" footer.

BEHAVIOR — the Phase-20 contract (mirror D20.1–D20.6, computed client-side from hydrated per-step state)

  • 🔴 Nonsequential flip (D20.1 "keep nav free"). Current Flutter GATES Next: guide_progress_state.dart:82-90 canAdvance returns isItemCompleted(current) on lesson pages → sequential. Web/Phase-20 is FREE nav; completion is computed honestly, not gated. Remove the canAdvance Next-gate (nav free); keep the count honest.
  • D20.1 honest adaptive end-page. The completion page must branch on VALUE-based completion (all-done → "Course Complete!" + trophy + score + Continue/Review/Done; partial → honest "keep going" header, no false trophy/score). NOT presence-based (the web bug was checkAllCompleted always 9/9). Flutter must compute lessonCompletion()/firstIncompleteLessonIndex() from hydrated per-step state.
  • D20.2 score-on-show. Populate the score when the completion page is SHOWN (not on a completion event); omit on the partial end-page (denominator = done, not total).
  • D20.3 review-return-to-origin. Exit-review returns to the page you ENTERED review from (reviewReturnPage), not hardcoded totalPages-1.
  • D20.4 adaptive resume CTA. Overview CTA = Start (0 done) / Continue→resumePage (partial) / Review Results (all done). Overview stays the landing page on reopen.
  • D20.5 Done button. Completion page gets a Done that reuses the close action (widen the close binding).
  • D20.6 Submit-Answer centered. Already done (22.4 answer-row).

Decisions (carry from Phase-20 + 22.4)

  • D22.5.1 = nonsequential, full parity (the master D22.1) — remove canAdvance gating; free nav; honest completion.
  • D22.5.2 = overview adopts the shared masthead/metadata (GuideBand+GuideMetadataGrid), not a learn-specific reimplementation.
  • D22.5.3 = learn progress fill = g-ok green (web makes BOTH cook + learn fills --g-ok; flip the blue guideLightboxProgressFillLearn).
  • D22.5.4 = review discs reuse the StepDisc state colours (green done/correct, red incorrect), matching the cook disc family.
  • D22.5.5 = the completion/partial branch is computed from hydrated per-step state, NEVER the vestigial current_step_index (always 0 — Phase 21 D21.2).

Questions Step 0's measured audit MUST answer (these are UNVERIFIED — do not act on them until read in code)

  1. Current overview CTA — read guide_lightbox_overview.dart: adaptive CTA already (Start/Continue/Review) or static "Start Course"? (The "Guides Phase 5" impl may already have D20.4.)
  2. Current completion behaviorguide_lightbox_completion.dart + guide_lightbox_learn_mode.dart dispatch: end-page value-based (D20.1) or presence-based? score populated on-show (D20.2)?
  3. Review-return page — does the bloc track reviewReturnPage (D20.3) or does exit-review hardcode the completion page?
  4. Done button / close binding — does completion already have a Done affordance (D20.5)?
  5. Which learn tokens are truly replaced — grep each guideLightbox* zero-ref before retiring (the 22.4 sweep kept ~130 for learn).
  6. Nonsequential removal blast radiuscanAdvance (guide_progress_state.dart:82-90, verified: gates Next) is read by the footer; confirm nothing else depends on it before freeing nav.
  7. What's already g-* — which of the 5 learn surfaces (or parts) is already on g-* vs legacy tokens? (Don't restyle what's already done.)

Baby-step order (Step 0 audit FIRST, then build → property-recheck/smoke each, hold commits)

  1. MEASURED AUDIT (Step 0) — the current-state + delta audit of all 5 learn files vs both contracts (see Method & rigor). Output phase-22.5-learn-parity-audit.md with a per-surface MATCH/DIVERGE/DEAD/MISSING table + which D20.1–6 points are already satisfied + a measured confidence %. The build steps below are provisional until Step 0 replaces this doc's assumptions with measured facts — reorder/rescope them from the audit.
  2. Footer learn nav — progress fill blue→g-ok; nav buttons on g-*; the nonsequential flip (remove the canAdvance gate). Smoke: free nav on Mixology.
  3. Overview — adopt GuideBand+GuideMetadataGrid; restyle About/objective on g-*; adaptive CTA (Start/Continue/Review from resumePage).
  4. Lesson chrome — counter/title/body/qtype/Mark-Complete on g-* (answer-row done).
  5. Completion — honest adaptive end-page (D20.1) + score-on-show (D20.2) + Done (D20.5) on g-*.
  6. Review — restyle rows + StepDisc-colour discs; review-return-to-origin (D20.3).
  7. Dead-token sweep — retire the now-replaced learn guideLightbox* by hand, grep-verified zero-ref.

Gates (property-check THEN eyeball — not eyeball alone; 22.4 lesson)

  • Per surface: flutter analyze clean + guide tests green + a property-level re-check vs source (its Step-0 diverges all closed on a clean re-run) + THEN operator visual smoke side-by-side vs web (marionette + playwright uts-smoke) on the Mixology / Woodworking courses.
  • Behavior smoke (the risky half — verify each against the Phase-20 contract, not by feel): free nav (nonsequential); partial end-page shows an honest "keep going" NOT a false trophy at 2/9; all-done shows Course Complete + score; exit-review returns to origin; overview CTA adapts Start→Continue→Review; Done closes.
  • Definition of Done for 22.5 = the Step-0 measured audit re-runs clean (all in-scope diverges closed) AND the retired learn-token set is identified/removed — not eyeball sign-off (CLAUDE.md DoD).

Then → 22.6 (final)

me-guides surface (if any) + a full dead-const sweep of any remaining pre-UTS guide tokens + a final end-to-end smoke. After 22.6 the whole UTS Flutter parity track (22.1–22.6) is complete.


UTS Phase 22.5 — Learn-mode parity audit (Step 0, measured)

Method: 6 read-only agents, one per learn surface + one for the behavior contract, each reading the ACTUAL Flutter code AND the web source (kon_guide_v2.css / _guide_lightbox.gohtml / guide-lightbox.js) + the Phase-20 doc, classifying MATCH / DIVERGE / DEAD / MISSING and (for behavior) SATISFIED / PARTIAL / MISSING. This REPLACES the 22.5 plan's grep-based assumptions with code-grounded facts.

Measured confidence: ~93%. ~100+ facets across 6 surfaces each traced to a file:line on both sides. Residual ~7% = runtime-pixel confirmations (not run) + one backend-payload question (D20.2 on a fresh reopen — see COULD-NOT-VERIFY).

Headline: the learn surfaces are the least-migrated part of the lightbox. The cook side (22.4) + the shared atoms (22.2) + the answer-row (pulled into the lesson page in 22.4) are done; everything else in learn is still pre-UTS legacy (blue tokens, rainbow badges, home-grown builders). The cook step guide_lightbox_step.dart is the exact, already-shipped template for most of the lesson-page + overview work.


BEHAVIOR — the Phase-20 contract (measured, end-to-end)

Point Flutter status Evidence Work
Nonsequential nav 🔴 MISSING (violation) guide_progress_state.dart:82-90 canAdvance returns isItemCompleted on lesson pages → Next gated; consumed guide_lightbox_footer.dart:207 1-line: footer.dart:207enabled: !isLastPage. Only 2 refs (getter + call site); retire the getter + 6 canAdvance tests
D20.1 honest end-page 🔴 MISSING guide_lightbox_completion.dart:32-92 renders trophy + "Course Complete!" + score UNCONDITIONALLY branch on state.isGuideComplete (bloc:94): complete → trophy/title/score; partial → no trophy, "You've completed N of M", Continue → resumePage
D20.2 score PARTIAL on-show ✅ (_computeScore in build(), completion.dart:101-131, flashcards excluded); omit-on-partial ❌ omit the score block in the D20.1 partial branch
D20.3 review-return ✅ SATISFIED review is an overlay flag; _onReviewEntered/Exited only toggle isReviewMode, currentPage preserved (bloc:339-355) — structurally better than web's pre-fix none
D20.4 adaptive CTA PARTIAL destination adapts (overview.dart:406resumePage), overview stays landing ✅; label static "Start Course" ❌ adaptive label: none→Start / partial→Continue / done→Review Results (from completion state)
D20.5 Done button 🔴 MISSING completion.dart:83-92 has only "Review All"; close is X/ESC only add a Done button that pops the dialog (reuse the shell close)
D20.6 Submit centered ✅ SATISFIED (22.4) lesson_page.dart:303-331 centered brand "Submit Answer" none

🔴 The load-bearing coupling (measured, not assumed): the completion page's unconditional trophy is only masked by the sequential canAdvance gate — it's the literal Phase-20 pre-fix D20.1 bug, latent. Free nav (footer) + honest end-page (completion) MUST ship in the same step; freeing Next without the honest branch reproduces "Course Complete! at 2 of 9."


VISUAL — per-surface deltas (the actionable synthesis)

Overview (guide_lightbox_overview.dart) — least migrated (6 MATCH / 14 DIVERGE / 2 DEAD / 2 MISSING)

  • Adopt GuideBand (22.4) → replaces its home-grown _buildCoverImage + title + _buildBadges + _buildStats in one move; adds the MISSING eyebrow + author medallion; retires the 2 DEAD builders. Render full-panel-width (outside body padding).
  • Adopt GuideMetadataGrid (22.4) → replaces _buildMetadataGrid; gains http-link + ISO-duration dispatch.
  • De-blue the "What you'll learn" objective callout: gTint bg, 3px gBrand left border + 0 6 6 0 radius, gBrandInk icon, gInk title, gBody body (currently #E3F2FD/#1565C0 blue).
  • De-blue the Start buttongBrand; + adaptive label (D20.4).
  • Restyle the "About This Course" section title to faint-11px-uppercase (like GuideLightboxMaterials); desc 13px/gMuted.
  • Already correct: Required Materials (shared widget), CTA click target (resumePage), anon handling, scroll-hint/count structure.

Lesson page (guide_lightbox_lesson_page.dart) = "port the cook-step treatment" (6 MATCH / 19 DIVERGE / 1 MISSING)

  • De-rainbow the header → a single neutral qtype label (reuse the cook _QtypeLabel / step_badge.dart idiom); drop the instruction/question/duration rainbow pills (_buildBadges/_badge).
  • Counter/title/body onto g-*: counter 11/w700/gFaint/ls 1.32 (currently 12/w400/#999/0.5); title w500/gInk (currently w600/#333); body 15/1.6/gBody (currently 15.2/1.7/#555); + question-prompt bottom gap gStepPromptMarginBottom.
  • Mark Complete → brand-filled button (mirror Submit's ElevatedButton; currently OutlinedButton); done bg #E9F5EE/no border/check2-circle; border-top gLine.
  • Instruction media → MaterialThumb + primaryMediaTap (currently inline full-width MediaBlock).
  • 🔴 Flashcard anon reveal is inert (lesson_page.dart:393 onReveal: !isAuthenticated ? null) — the SAME F1 bug just fixed on the post-body blocks (8bd1ecec), still present here. Apply the same anon-local reveal.
  • Drop the 4 double-Padding wrappers around tip/gallery/explanation/flashcard (let each atom own its top margin — mirror cook).
  • Already at parity: answer options + Submit (byte-for-byte the cook step, 22.4); the Tip/Explanation/Flashcard/Gallery widgets (22.2).

Completion (guide_lightbox_completion.dart) — legacy tokens, un-migrated

  • Icon → g- circle badge: 72px gBrand circle + white 32px trophy_fill (currently a bare green 48px icon).
  • Title → ultralight w200 / 25.6 / gInk (currently w600/24/#333).
  • Score block: muted caption + green-bold 24px number (gOk) (currently one flat 16px #555 line).
  • Add the Continue button (partial branch) + Done button (D20.5); style all 3 actions as g- .btn (13/w600/radius6/gLine2 border/gBrandInk) in a centered flex row.
  • Decision: the "Flashcards not included in score" line — web omits it (see Decisions).
  • 🔴 Learn progress fill blue→gOk green (guideLightboxProgressFillLearn #1565C0 → gOk; web makes cook + learn both green).
  • Nav buttons: borderless, gBrandInk, w600, no forced min-width, H-pad 4, disabled = recolor to gFaint (opacity 1) — currently bordered flat #333 w400 min-width 100 opacity-0.3.
  • Drop the "— P% complete" suffix from the learn label (web = "Lesson N of M" only).
  • Review footer: drop the progress bar (web shows only Exit Review), add the back-arrow icon, neutral-gray outline.
  • Overview/completion nav: web HIDES prev/next (overview) + Next (completion); Flutter shows-but-dims → hide instead.
  • Nonsequential fix lives here (footer.dart:207) — see Behavior.

Review (guide_lightbox_review.dart) — legacy tokens, not StepDisc; RICHER than web

  • Default number disc grey→gBrand; size 30; font 13/600; remove the disc rings (BoxShadow — web dropped these).
  • Reuse StepDisc (currently bespoke _buildNumberCircle).
  • Simplify the answer block to web: web = plain inline text + colored ✓/✗ icons ("You answered: X" / "Correct: Y"); Flutter = pill boxes + "YOUR ANSWER"/"CORRECT ANSWER" labels. (Decision — richer-than-web.)
  • Remove the explanation box — web's review has none (Decision — richer-than-web).
  • Typography → g- (title 14/gInk, body 13.5/gBody), row border gLine, first/last-child padding.
  • D20.3 already satisfied (no behavior work).

Rescoped build order (from the measured audit — replaces the plan's provisional order)

  1. Footer + nonsequential + honest end-page (COUPLED behavior slice). Fill blue→green + nav restyle + drop the canAdvance gate + branch the completion widget on isGuideComplete (honest partial vs complete) + omit score on partial + add Continue + Done + the completion g- restyle. Smoke: free nav + honest partial end-page (no false trophy at 2-of-9) + all-done Course Complete + Done closes.
  2. Overview — adopt GuideBand + GuideMetadataGrid; de-blue objective + Start; adaptive CTA label (D20.4).
  3. Lesson chrome — port the cook-step treatment (de-rainbow header, g- counter/title/body, brand-fill Mark-Complete, MaterialThumb media, anon flashcard reveal [F1]*, drop the 4 double-paddings).
  4. ReviewStepDisc + brand disc + g- typography; simplify-to-web (per Decisions).
  5. Dead-token sweep — retire the orphaned learn tokens (per surface, grep-verified): guideBadge* (12), guideLightboxLesson* (~13), guideLightboxCompletion*, guideLightboxReview*, guideLightboxNav* residue, guideLightboxProgressFillLearn (once green), the overview _buildBadges/_buildStats token families. KEEP any still-shared until their surface lands.
  6. Analyze + test + smoke on the Mixology / Woodworking courses.

Per surface: flutter analyze clean + tests green + a property-level re-check vs source (its diverges closed) + operator smoke. DoD: this audit re-runs clean + the dead set retired.

Decisions (RESOLVED — operator, 2026-07-02)

  • D22.5.a — Review answer block:KEEP Flutter's richer pills + "YOUR/CORRECT ANSWER" labels (intentional Flutter-lead → propose to web team).
  • D22.5.b — Review explanation box:KEEP (intentional Flutter-lead → propose to web team).
  • D22.5.c — Review disc rings:REMOVE — match web (web dropped these at kon_guide retirement).
  • D22.5.c — Completion "Flashcards not included in score" note:KEEP (intentional Flutter-lead → propose to web team).
  • D22.5.c — Review-footer progress bar (parity-default, not asked): ❌ DROP in review mode — match web (Exit Review only). Revisit if operator wants it kept.
  • D22.5.d — Overview/completion nav (parity-default, not asked): HIDE prev/next (overview) + Next (completion) — match web.

→ 3 Flutter-leads to propose to the web team (pills+labels, review explanation box, completion flashcard-note): logged in uts-open-items-punchlist.md as W12 for the web team to review as candidate web additions.

COULD-NOT-VERIFY (residual risk)

  1. D20.2 score on a FRESH reopen (not same-session)_computeScore counts correct only when resp.selectedOptionId != null && isCorrect. On a pure revisit this depends on the backend guide_progress hydration payload carrying selected_option_id per response (guide_progress_data.dart:45). If the server omits it, hydrated MC/TF answers score 0. Runtime/API check owed in 22.5.
  2. Runtime pixels / centering (review row maxWidth:650 without Center) / gBrandInk color-mix approximation — deferred to the implement-then-smoke pass.
  3. Page-inner max-width/padding of the lesson page not measured (outside the element list).

UTS Phase 22.5 — Flutter learn-lightbox parity: REVIEW

Full review of the 22.5 work (learn-mode guide lightbox → web parity on the g- design system). Web (kontent) is the reference. All work is committed to master, pending a GoLand push (nothing pushed yet). Dev-smoked on the Mixology* course (guide 3 / post 48, 9 steps / 4 questions), macOS Flutter + marionette, A/B against the live web via playwright (uts-smoke session).

Status at a glance

Step Surface State Commit
0 Measured audit (6 agents) ✅ done docs 8254e562
1 Footer + nonsequential + honest end-page ✅ shipped + smoked ffaa0503
Auth V7 (surfaced by the step-1 smoke) ✅ shipped + live-validated fbb26007
2 Overview ✅ shipped + smoked 836e1706
3 Lesson chrome ✅ shipped + smoked (A/B pixel-verified) f45c596a + 677d3d51
test-lint chore d8cd3e66
picker parity + logout-deadlock fix (2026-07-03 detour) ✅ shipped + smoked 89f519db + 0c54c628
4 Review page ✅ shipped + smoked (A/B measured-verified) c28e4b6b
5 Final dead-token sweep + me-guides ✅ shipped + smoked (measured sweep, 0 dead) 8f98dc64
fake_async dev-dep chore 0745decb

Phase 22.5 COMPLETE — the whole learn lightbox + picker + me-guides are on g-*; the measured token sweep re-runs clean (134 guide-family tokens, 0 dead).

Step-0 audit (measured, ~93%)

6 read-only agents (one per learn surface + the Phase-20 behavior contract) → measured deltas in phase-22.5-learn-parity-audit.md. Replaced the plan's grep-assumptions with code-grounded facts. Rescoped build order = footer/behavior → overview → lesson → review → sweep. Learn was the LEAST-migrated part; the cook step is the template.

The load-bearing coupled slice (freeing Next exposes the completion page — must ship together). - Nonsequential nav (D20.1): retired the dead canAdvance gate + getter + 6 tests; Next never gated. - Honest completion: complete → 72px brand trophy badge + "Course Complete!" + score; partial → no trophy, "You've completed N of M lessons", no score (D20.2a); Continue(→resumePage) + Done buttons. - Footer: nav borderless brand-ink w600; progress fill blue→g-ok green; "Lesson N of M" (no %); review footer = centered "← Exit Review", no bar; overview hides both nav, completion hides Next. - Smoked: free nav, honest 0-of-9 end-page (no false trophy), review enter/exit returns-to-origin (D20.3 already-satisfied). 197 guide tests green.

Auth V7 — app-wide JWT-lifecycle bug (fbb26007) — the big detour

The step-1 smoke showed "Lesson 0 of 9" despite the DB having progress. Root cause (proven with hard evidence, incl. curl A/B/C): the JWT expired (15-min TTL) and optional-auth READS return 200-anonymous (not 401), so the interceptor's reactive 401-refresh can NEVER cover reads → after 15 min every user-scoped read silently degrades to anonymous (guide progress, reactions, block interactions). Web is immune (session-cookie + server-rendered, same EnrichGuideEdges→JSON pipeline). Fix = SessionRefreshScheduler (periodic near-expiry check + app-resume catch-up, single-flight) refreshing OUTSIDE the request pipeline — refreshing INSIDE the QueuedInterceptor onRequest DEADLOCKS (the refresh's own POST queues behind the blocked request). 13 unit tests; live-validated (lightbox hydrated "Lesson 3 of 9"). Detail: uts-open-items-punchlist.md V7 + memory gotcha gotcha_jwt_expiry_reads_anonymous_and_interceptor_deadlock.

Step 2 — overview (836e1706)

  • Adopted GuideBand (full-bleed masthead — folds cover/title/rainbow badges/stats into the author medallion + "COURSE" eyebrow + ultralight title + "difficulty · humanized-time · outcome" stat line) + GuideMetadataGrid (.g-md).
  • De-blued the "What you'll learn" callout (gTint + 3px gBrand left border + right-radius, gBrandInk /gInk/gBody) and the Start button (gBrand) with the adaptive D20.4 label (Start / Continue / Review Results / Log in to start; target=resumePage). About = faint gLbSectionTitle + gMuted desc; count → gFaint. Dead-token sweep of the overview-only families.
  • Smoked: masthead (no rainbow badges), de-blued callout+Start, .g-md metadata, "Continue" CTA → jumped to first-incomplete lesson 4, V7 hydration.

Step 3 — lesson chrome (f45c596a + 677d3d51)

Ported the cook-step treatment to the lesson page: - De-rainbow header = faint-w700 counter + neutral GuideQtypeLabel (questions only, exposed public from _QtypeLabel); g- counter/title(w500/gInk)/body; primary media → MaterialThumb+ primaryMediaTap; F1 anon flashcard reveal (local); dropped double-Padding wrappers + Reset BlocListener. Dead-token sweep (rainbow guideBadge(12), StepBadge(6), etc.). - The smoke (A/B vs web computed styles) caught 4 things my build got wrong — a good example of "match the RENDER, not the source CSS": 1. qtype label ran under the close button → +36px header clearance. 2. done-button color: I used the pale-green --done CSS — but that class is never applied; web renders the brand button. → brand for both. 3. done-button fade: completed = the brand button DISABLED → opacity 0.65 (Bootstrap), not clickable. My first "red" fix was fully-opaque + clickable — wrong. 4. icon: web renders bi-check2-circle for BOTH* states (JS normalizes the bi-check2 default); I used plain check2 on incomplete. → check2-circle both. Plus 2 spacing fixes (Flutter stacks margins CSS collapses): counter→title 12 (title top 44=web), and drop the media-top on questions (media top 115=web). Final positions within ~3px of web. - Verified every lesson variant on Mixology: instruction inc(full red)/done(faded), question answered(wrong=red, correct stays neutral on hydration)/unanswered, +media(MaterialThumb), +tip, MC qtype. 313 tests green.

Step 4 — review page (c28e4b6b)

  • Number disc → shared StepDisc with showStateMark:false (30dp brand default, green done / red wrong fill, ordinal kept, halo rings removed per D22.5.c — matches .o-guide-lightbox-review-number). Typography to g-*: title 14/w600/gInk, body 13.5/1.6/gBody at the 30+9.6 disc indent, divider gLine, media radius gRadius.
  • KEPT the W12 Flutter leads re-expressed on g- tokens: answer pills + YOUR/CORRECT ANSWER labels (gOpt/gCallout*, D22.5.a) and the explanation box (now the shared ExplanationBox atom, D22.5.b).
  • Retired guideLightboxReview* (36 tokens) + guideLightboxStepMediaRadius (last consumer).
  • Smoked on Mixology (11/11 measured properties match web: disc 13/600/white, title 14/600/#222, body 13.5/1.6/#3a3a3a @63.6 indent, labels 11/gFaint/ls.4, answer text #0f5132/#842029, media gRadius clip on item 9); D20.3 review-return re-verified; paging did NOT mutate progress (honest "3 of 9" throughout). 314 tests green.
  • Flashcard revealed-answer branch: web --revealed is #555 italic — used gMuted italic as the nearest g- token (flag if gBody preferred). Exercised later same session via the Woodworking completion run (below): measured 13.5/italic/#6B7280 gMuted.*

Operator decisions (locked)

  • D22.5.a review answers = KEEP Flutter's richer pills + "YOUR/CORRECT ANSWER" labels → propose to web (W12).
  • D22.5.b review explanation box = KEEP → propose to web (W12).
  • D22.5.c review disc rings = REMOVE (match web); completion "flashcards not in score" note = KEEP → W12.
  • D22.5.d overview/completion nav = HIDE (done in step 1).
  • W12 (3 Flutter-leads to propose to web team) logged in uts-open-items-punchlist.md.

Step 5 — me-guides on g-* + final dead-token sweep (8f98dc64)

  • Measured sweep (script, not eyeball): extracted all 76 declared guideLightbox* tokens and mapped every consumer across lib/+test/. DEAD = 2 (guideLightboxBodyPaddingTop+Mobile) + guideLightboxSectionTitle* (3; me-guides was the last consumer, moved off). All other families are live and match web's surface-specific .o-guide-lightbox-* selectors — legit, kept. Re-run after the edits: 134 guide-family tokens, 0 dead.
  • Bidirectional bonus: a full-file zero-consumer sweep found 54 pre-existing dead tokens outside the guide scope (chat, ws, mediaLightbox*, block-weight families, etc.) — NOT bundled here (out of UTS scope; several look like behavioral config, not design tokens). Flagged for a future hygiene pass.
  • me-guides (Flutter-only page, no web reference — styled by the g-* vocabulary): gLbSectionTitle eyebrow headers; picker-row idiom (name 14/w600/gInk, meta 12/gFaint, gLine divider); gThumb 48/ gRadius thumbs (gTint+gFaint fallback); progress de-blued to gOk fill on gLine2 for ALL guide types (footer precedent); score → flat gMuted (completion precedent, retired the green/orange/grey trichotomy). meGuides* family cut 21 → 7 (geometry/sizes only). Live-smoked (3 in-progress rows, measured values all match).
  • Incidental: the accidental drawer logout re-confirmed the 0c54c628 fix live (instant → /auth).

Completion runs (operator-blessed) — closed every remaining unexercised branch

Deliberately completed BOTH dev courses as admin (real progress written to the dev DB, by design): - Mixology 9/9 (answered lesson-5 Jigger ✓, lesson-7 T/F False ✓, lesson-9 wrong on purpose → 2/4 score): full trophy completion page live-verified for the first time (brand trophy badge, "Course Complete!", flat gMuted "2/4 correct (50%)", Review All Lessons + Done, no Next); review re-verified with full data (disc 4 flipped brand→green, CORRECT label, wrong-pill under media item 9). Gotcha: answer options need select → Submit Answer (select alone ≠ submitted). - Woodworking 7/7 (2 flashcards): flashcard reveal flow (front → Tap to reveal → back + explanation, read-only revealed on hydrate); "1/1 correct (100%)" proves flashcards excluded from score (the completion note is meaningful); review flashcard revealed-answer measured 13.5/italic/gMuted (the step-4 branch). - The run ALSO surfaced (and 10bffab3 fixed) a real parity bug: the select→"Submit Answer" two-step IS web parity, but an unsubmitted selection (+ anon flashcard reveal) died on page nav — web keeps pages in the DOM (d-none toggling) so a checked radio survives until lightbox close; Flutter disposed the page State per turn. Fix = pendingSelections/localReveals in GuideProgressBloc (fresh per open = reset-on-close for free); lesson page now Stateless; 5 regression bloc tests; live-smoked select→Prev→Next→still-selected→submit (319 green). - me-guides Completed/score row measured: "COMPLETED (1)" eyebrow, full gOk bar, "Complete" = #198754 gOk, trophy + "2/4 correct" = #6B7280 gMuted flat (old code = orange for partial — de-rainbow live-confirmed); Mixology moved In Progress → Completed correctly. → Zero disclosed-unexercised branches remain in 22.5.

What's LEFT (post-22.5 runtime notes)

  • V7 CNV#1 CLOSED (2026-07-03): fresh-reopen hydration DOES carry selected_option_id. Code chain: PostDetail (posts.go:138-139) → EnrichGuideEdges eager-loads WithResponses(rq.WithSelectedOption()) (guide_progress.go:419-420) → BuildProgressMap copies r.Edges.SelectedOption.ID into StepResp.SelectedOptionID → Flutter StepResp parses selected_option_id (guide_progress_data.dart:45) → _onHydrated builds the responses map. Live proof: after a full close + fresh re-fetch, review rendered "YOUR ANSWER ✗ Wheat stalk" and lesson 9 re-hydrated the wrong pick red — only possible if the payload identifies WHICH option was chosen.

Key lessons (this session)

  1. Match the RENDER, not the source CSS. The done-button (--done green rule dead), the icon (bi-check2 overridden to check2-circle), and even the button opacity were all cases where the rendered web differed from the template/CSS source. Always A/B the computed style / live DOM.
  2. Flutter stacks margins; CSS collapses adjacent ones. Any marginBottom+marginTop pair over-gaps in Flutter — check with measured element tops.
  3. Auth is high-risk: the operator was right to demand 100%-proof + tests + live validation before committing the V7 fix; a first in-interceptor attempt deadlocked (caught by testing).
  4. Smoke BEFORE committing; A/B the exact computed values rather than eyeballing.

Resume procedure (after MacBook restart)

  1. Dev Go backend: start via GoLand (kon.test:8443). Commits survive the restart; nothing to recover.
  2. Flutter app: cd flutter/gokonversed && flutter run -d macos --debug (note the VM URI).
  3. marionette: connect ws://127.0.0.1:<port>/<token>/ws; app auto-logs-in as admin (SSO/refresh).
  4. Web A/B: playwright-cli -s=uts-smoke open https://kon.test:8443/... (session dies on restart).
  5. GoLand push owed — 13 commits: ffaa0503 / fbb26007 / 836e1706 / f45c596a / d8cd3e66 / 677d3d51 / 0c54c628 / 89f519db / c28e4b6b / 96f9ce38 / 8f98dc64 / 0745decb + this doc commit.
  6. Local-only (do NOT commit): site_config.dart brand flip, .claude/settings.local.json, repo-root CLAUDE.md.
← Back to Under the Tuscan Sun