Appendix — The Snag List
05 JUL AT 08:40 PM

Appendix — The Snag List

0 LOVES 1 VIEWS
Open items, a don't-forget tracker.

UTS — Open-items punchlist (don't-forget tracker)

Built 2026-07-02 from a hand read of every web-phase doc (1–21) + a 3-agent cross-check (web-phase docs · Flutter-22.x docs · memory+git), reconciled. The web track (Phases 1–21) is shipped + on prod; the Flutter parity track (22.x) is in flight. None of the OPEN items is a live prod defect — every 🔴 broken-behavior bug (dishonest completion page, multi-user O2M lock/Spartacus, hydration-green loss, double headings, picker XSS, cover/scroll bugs) was fixed and, where prod-relevant, deployed. The items below are deferred features, deferred engineering, a security follow-up, and owed verifications — the loops most at risk of being forgotten now that "web is done."

🔴 OPEN — web-side (deferred, not lost)

# Item Kind Source
~~W1~~ → VERIFIED + BROADENED + FILED as RI issue 2539 (2026-07-05). Not just episodes/series: a guard-trace of every media-edge setter found the same missing media.HasSiteWith(site) check in 9 service files / ~19 sites (series, events, contacts, categories, posts, weblinks, content_blocks, cover_variants, media_collections); only guides/materials/steps were guarded. Real authenticated cross-tenant disclosure IDOR (the /media/ FileServer is unauthenticated, picker is site-scoped → id-enumeration is the only path to foreign media); Medium severity, inert in single-owner dogfood. Merged with W8 (the shared-helper extraction is the fix vehicle). Full scope + exploit chain + fix in issue 2539. 🔒 security → filed phase-19:263-269; RI issue 2539
W2 Guide-lightbox reset modal not g-styled. The post-body block reset modal is a g-card; the guide-lightbox showResetModal is still plain Bootstrap. Flagged to the web team; Flutter deliberately leads (accepted 22.4 divergence). parity phase-22.4-cook-lightbox-parity-audit.md (ACCEPTED-DIVERGENCE)
W3 Step deep-link (b)location.hash → open the lightbox + scroll to the step (then re-add the per-step url, dedupe in-collection slugs, guarantee slug population). Only (c) "drop the url" shipped. feature phase-13:138-160,215-217 (D13.6b)
W4 Material tags → chip discovery (142 tagged materials in prod; chip render exists) — spun OUT of Phase 13 into the American Graffiti program (needs its own mockup; the cook-lightbox chip UX is unresolved). feature (→ AG) phase-13:174-180 (D13.5)
W5 Ratings feature. Guide rating metadata renders as generic Rating: 4 text; stars / aggregate / an authoring surface is a proper later feature (deliberately NOT a metadata special-case). feature phase-9:141
W6 Playthrough guide-scoped media aggregation. A future guide "playthrough" must aggregate guide media as a guide-scoped group, NOT by adding step/material media to the page-wide {{ if .Hidden }} data-spans (would break the isolated-experience intent). Architectural guard. architecture guard overview :112 (Phase 15)
W7 Search/pagination on the 4 other bespoke pickers + the media-library page (event_cover, category_media, contact_photo, weblink_thumbnail + the media_library grid). Endpoint already supports it → front-end-only. Deferred to AFTER the Flutter parity pass. engineering phase-19a:173-184
~~W8~~ → MERGED into RI issue 2539 (2026-07-05). The shared validateMediaOwnership extraction is the fix vehicle for the broadened-W1 IDOR (materials/steps/guide have copies #1–3; 9 more files need the guard). Doing it once as a shared helper closes W1+W8 together — tracked in issue 2539, not here. cleanup → filed phase-19:164-166; RI issue 2539
W9 Studio "Block interaction insights." Guide progress has a Studio Insights view; block interactions have NO analytics view (coverage asymmetry). Out-of-scope / future. feature phase-21:102-109 (D21.3)
W10 ?v= asset version doesn't bump on restart — CSS/JS changes need a hard refresh in dev; flagged for a build fix. Dev-ergonomics, out of scope. dev-tooling phase-9:51
W11 Google modal-content policy watch-item. D13.6(c) still emits recipeInstructions text from lightbox-only (user-openable) steps; defensible today. Re-confirm if Google tightens its "don't mark up content not visible" policy. watch-item phase-13:157-160
W12 Flutter learn-flow leads → propose to web. The 22.5 audit found 3 places where Flutter's learn lightbox is intentionally richer than web (operator-approved to keep on Flutter). Web team to review as candidate web additions: (a) review answers as pill boxes + "YOUR ANSWER"/"CORRECT ANSWER" labels (web = inline text + ✓/✗ icons); (b) explanation box shown in the review section (web review has none); (c) "flashcards not included in score" note on the completion page (web omits). → Proposal written 2026-07-03 (w12-flutter-leads-web-proposal.md); web-side review verified all claims (14/14) + folded in 4 findings. ✅ DONE — IMPLEMENTED ON WEB + dev-smoked 2026-07-04: all three + the F2 companion (correct_option_id in the hydration blob, answered-steps-only = redaction-safe; review pills survive a pure-hydration reload — the previously-impossible case, verified). Operator live-confirmed on dev during the smoke: label measured 11/600/uppercase/#9AA0A6 = Flutter's exact values. Dead Phase-4h fns (GetProgressForGuides/ExportProgressForJS) retired; tests rewritten onto the live BuildProgressJSON path (which had zero coverage). Full go test ./... green; button-driven smoke on guide 3/post 48, console clean. As-built + deviations in the proposal doc. Web and Flutter learn lightboxes reconverged on D22.5.a/b/c. UX proposal (→ web) phase-22.5-learn-parity-audit.md D22.5.a/b/c

⚠️ VERIFY — owed checks (never confirmed resolved)

# Item Source
~~V1~~ RESOLVED (2026-07-03): the web never had the bug, so no fix commit exists — correctly. All 5 web disc/verdict sites are server-verdict-based (data.block_interaction.is_correct / data.step_response.is_correct / hydrated interaction.is_correct): block live-answer (block-interaction-compiler.js:494), block flashcard (:110), block hydration (:62), lightbox live-answer (guide-lightbox.js:532), lightbox hydration (:307/:359). git log -S shows that logic is ORIGINAL to the first implementations (b0f5f3ff block, 5825aeda lightbox) — never an id-compare. The id-compare disc bug was Flutter-rebuild-only (fixed 22.3, step_block.dart:112); the memory claim "operator fixed the same on web" was wrong. verified 2026-07-03
~~V2~~ RESOLVED — server is_correct redaction IS enforced on the Flutter path: api/v1/services/guide_redact.go (RedactInteractiveBlockAnswers + RedactGuideAnswers, called from posts.go:157-163) sets is_correct=false on answer options for unanswered steps (all for anon); test TestPostDetail_RedactsAnswerKeys_Anonymous. So Flutter's answerOptions.isCorrect fallback is spoiler-safe. verified 2026-07-02
~~V3~~ RESOLVED — backend fix IMPLEMENTED 2026-07-04: api/v1 GetGuidesForPost now loads WithCreatedBy(uq→uq.WithAvatar()) = byte-parity with kontent (guide.go:68); the Go-team review verified all 6 request-doc claims + safety (ent .Sensitive() fields excluded regardless; redaction untouched; guides ride PostDetail). Build + full api/v1 suite green. Operator: no Go test — end-to-end verification handed to the Flutter team (set an avatar on a dev user — dev has zero — then confirm the guide_band.dart:96 medallion renders the photo). As-built in v3-go-team-request-guide-author-avatar.md. resolved → Flutter team verify
~~V4~~ RESOLVED (2026-07-03): parity already correct. kontent loads bootstrap.min.css (base.gohtml:9) whose reboot underlines links; kon_guide_v2.css:290 overrides only the COLOR (.g-md-v a{color:var(--g-brand-ink)}) → web metadata links = underlined brand-ink. Flutter matches: guide_metadata_grid.dart:91-92 (TextDecoration.underline + brandInk). verified 2026-07-03
~~V5~~ RESOLVED (2026-07-03): YES on webisStepCompleted counts a revealed flashcard (guide-lightbox.js:832-834) and maybeShowResetButton (:839-850) shows Reset on ANY completed step. Flutter matches for the authed case (authed reveal POSTs a response → hasProgress → Reset shows). Accepted micro-divergence: an ANON local reveal would unhide web's Reset (pure DOM check) but not Flutter's (showReset is auth-gated) — Flutter stricter/saner, since anon reset can't POST anyway. verified 2026-07-03
~~V6~~ RESOLVED (2026-07-03): confirmed closed on BOTH sides. Web: _biMaybeShowTryAgain early-returns when !resettable (block-interaction-compiler.js:136-138) and the collection reset link requires resettable (:208-209). Flutter: step_block.dart:104/111/180 + step_collection_block.dart:91/136 gate identically on block.resettable — and it's regression-tested (guide_organisms_test: "no 'Reset progress' on non-resettable collection"). verified 2026-07-03
~~V7~~ FIXED (2026-07-02) — was an app-wide JWT-lifecycle bug, NOT a guide/hydration bug. Found in the 22.5 step-1 smoke: the lightbox started at "Lesson 0 of 9" despite the DB having 3 done. Root cause (proven with hard evidence): the app's JWT expired (15-min TTL); optional-auth reads return 200-anonymous, not 401, on a rejected/expired token (verified by curl A/B/C), so the interceptor's reactive 401-refresh could never fire for reads, and there was no proactive refresh → after 15 min every user-scoped read (guide progress, reactions, block interactions…) silently degraded to anonymous. The kontent web never hits this because it's session-based + server-rendered (same EnrichGuideEdges→JSON→hydration pipeline, pages.go:456/515). Fix (Option A — lifecycle refresh, isolated from the request pipeline): new SessionRefreshScheduler (lib/core/auth/session_refresh_scheduler.dart) + pure TokenExpiry (token_expiry.dart), wired in main.dart — periodic near-expiry check + app-resume catch-up, single-flight shared with the reactive path, refresh-fail→onSessionExpired. A first attempt to refresh inside the interceptor onRequest deadlocked the QueuedInterceptor (the refresh's own POST queued behind the blocked request) → moved outside the pipeline. 13 new unit tests; full suite 313 green. Live-validated end-to-end: scheduler fires + refreshes live, and the Mixology lightbox now hydrates "Lesson 3 of 9" (was 0). 22.5 step-1 smoke → fixed 2026-07-02

🔎 Web-fix → Flutter parity checklist (did the Flutter rebuild inherit each web bug fix?)

The core worry: Flutter is rebuilt to match web, so a behavioral web fix that never made it into the spec can be silently re-implemented in its pre-fix (buggy) form. Method: for each SHIPPED web behavioral/rendering fix, verify Flutter has the equivalent in code (not assumed). Web-only fixes (LD+JSON, Studio, CSS-coexistence, backend/infra) have no Flutter surface — Flutter shares the backend or lacks the layer. Standing gate for 22.5/22.6: for every web fix touching a surface being built, the done-gate includes "does Flutter have the web-side fix?" — and this table gets a row.

Web fix (phase) Flutter status Verified at
Hydration re-green — wrong-answered MC re-greens the correct option on reload (Phase 16) ✅ present (block); ✅ lightbox correctly stays neutral (data differs by surface) step_block.dart:89-96, step_collection_block.dart:381-385; lightbox 22.4
D9/D10 anti-spoileris_correct withheld for unanswered/anon ✅ present — backend redaction on the Flutter path (guide_redact.go) + _optionState gated on isSubmitted api/v1/services/guide_redact.go; step_block.dart:424
Disc correctness = server verdict (not id-compare) ✅ present step_block.dart:112; lightbox 22.4
Selected-option icon stays bi-circle until submit (Phase 6) ✅ present answer_option_row.dart (selected → BootstrapIcons.circle)
Completed-state greening + M8 title-strike (block strikes / lightbox doesn't) ✅ present, both surfaces correct 22.3 block; 22.4 lightbox
De-rainbow badges / neutral qtype (Phase 5/6/9/10) ✅ present 22.2–22.4
Cover natural-aspect / no crop (Phase 9) ✅ present GuideBand fitWidth, 22.4
Metadata value-syntax dispatch (Phase 9) ✅ present GuideMetadataGrid, 22.4
o_mat_thumb media → viewer, nested above lightbox (Phase 2/10) ✅ present (smoked) MaterialThumb + primaryMediaTap, 22.4
Gallery collapsed-by-default (Phase 6) ✅ present guide_gallery_disclosure.dart:33 _open=false
Double-heading (Phase 17, data cleanup) ✅ parity — Flutter renders block.heading + collection.title exactly like web → relies on the same cleaned prod data; no Flutter-specific doubling content_block_renderer.dart:78, step_collection_block.dart:94
D20.1–D20.6 learn completion/review/CTA/Done (Phase 20) ALL SIX individually verified on Flutter (2026-07-03): D20.1 nonsequential+honest end-page (step 1 + widget tests); D20.2/2a score only-when-complete, flashcards excluded (completion runs: "2/4 (50%)" + "1/1 (100%)"); D20.3 review-return-to-origin (smoked twice); D20.4 adaptive CTA (Continue→lesson-4 smoked; "Review Results"→completion smoked on the all-done course); D20.5 Done closes cleanly (tapped live — lightbox closed to post, no error); D20.6 Submit-Answer centered (smoked repeatedly). 22.5 steps 1-2 + completion runs
Course scroll-to-Start-Course (Phase 9) ✅ N/A-by-construction + smoked — the web bug was a CSS height collapse (height:100% vs max-height) making Start unreachable; Flutter's overview is a SingleChildScrollView (no such failure mode) and scrolling to Start/Continue/Review-Results was exercised in every 22.5 smoke. verified 2026-07-03
Web-only: LD+JSON/step deep-link (13), Studio pickers/IDOR/XSS (19/19a), CSS-coexistence (5/6/10/11), ?v= (9), Spartacus O2M→M2M (backend), reset-modal z-index (10) N/A — no Flutter surface (or shared backend)

✅ FOUND by the sweep + FIXED (2026-07-02, push-pending) — block-side misses

# Item Fix
F1 Anon flashcard reveal was inert on the post-body block (web allows anon-local reveal, D10) ✅ FIXED — anon now reveals locally (mirrors the lightbox 22.4 #3): _flashcardRevealedLocal bool in step_block.dart, a per-step Set<int> in step_collection_block.dart (cleared on reset); onReveal always fires (authed → POST, anon → local setState). Test flipped to "reveals locally."
F2 Post-body block showed a "Sign in to track your progress" prompt (web block has none — D10 reconcile) ✅ FIXED (removed for parity, operator call) — dropped both _buildAnonPrompt() call sites + the method in step_block.dart; retired the 8 dead guideAnonPrompt* tokens. Two tests flipped to assert no sign-in prompt.

Flutter — OPEN (post-22.5 tail; 22.5 itself is COMPLETE 2026-07-03)

Everything that was listed here for 22.5/22.6 SHIPPED (see phase-22.5-review.md: learn surfaces, review page, me-guides, sweep 0-dead, picker parity, logout-deadlock + paging-selection fixes). Remaining Flutter-side tail: - ~~FT1 — per-page scroll position resets on Learn-Mode paging.~~ ACCEPTED DIVERGENCE (operator call, 2026-07-05) — web's scroll persistence is an artifact of its DOM d-none paging, not a designed behavior; native mobile pagers reset scroll, and nothing in the D20.1–D20.6 contract requires scroll persistence. Won't-fix; recorded so it isn't re-raised as a parity gap. - ~~Launcher a11y (found by the 2026-07-05 full-doc sweep)~~ ✅ FIXED 297582b6 — hero icon buttons (gallery/video/audio/series/guide + CEWL trigger) were icon-only with no Tooltip/Semantics (web has title attrs). Now: required tooltip on HeroIconButton with web-parity labels (Strings.hero*), counts threaded from PostHeaderZone's block scan, stable ValueKeys (hero-*-btn) for driver/marionette targeting. 4 widget tests; live-smoked (tap-by-key opened the sourdough guide lightbox). - ~~GAP-T1 — footer/completion widget tests~~ ✅ DONE c0633eb9 (15 widget tests, guide_lightbox_footer_completion_test.dart). - ~~BUG-RISK-A1~~ ✅ FIXED 2026-07-05. Evidence: overnight keychain lock made the scheduler's token read throw PlatformException(-25308) UNHANDLED from SessionRefreshScheduler._run and the app stopped repainting until relaunch; separately, ANY refresh Err (offline/5xx at the token boundary) mapped → onSessionExpired → silent logout. Fix: new sealed RefreshResult (lib/core/auth/refresh_result.dart) — pure classifyRefresh maps AuthFailure/ SessionExpiredFailureDenied (only path that logs out); NetworkFailure/ApiFailure/ ParseFailure/platform throws → Transient (skip tick / fail request, keep session). Scheduler + AuthInterceptor + doRefresh all guarded with try/catch (storage throw = skip tick / send unauthenticated; retry failure ≠ session death). 11 new unit tests (classifier + scheduler transient paths); suite 345 green; happy-path smoked on macOS (session restore + scheduler start clean past a native OSStatus 13 keychain grumble). - ~~FT2 — pre-existing non-guide zero-consumer DesignTokens entries~~ ✅ RETIRED 2026-07-05. Full re-measurement of all 811 members (DesignTokens 795 + ChatConstants 16): true dead = 38, not the recorded 54 (point-in-time count, list not preserved; today's enumeration is authoritative). Retired: authButtonPaddingH, blockH1–4Weight, locationMapImageZoom, cewlSwipeThreshold, ownerNavButtonSize, hiddenBlock{BorderColor,PaddingLeft}, draftBanner{Bg, FontSize}, formRowValueWeight, actionSheet{TitleWeight,SectionHeaderWeight,ActionHoverBg}, infoPanelAnimDuration, 13 chat incl. the whole Chat Emoji Picker section, and 8 ChatConstants protocol limits (maxWs, upload sizes, page sizes — unenforced in code; Go doc remains the spec source). Method lesson: a per-line grep first misjudged 46 — 8 tokens' only refs were formatter line-wrapped (DesignTokens\n .name); the multiline-aware re-measure and the compiler independently agreed on the same 8, restored verbatim. Re-audit after edits: 765 members, 0 dead; analyze clean; suite 349 green. Deletions hand-edited (operator directive), script used for measurement only. - Lower-priority render-Qs from the 22.4 audit — margin-collapse deltas, checkbox px. ~~flashcard disc red/green~~ ✅ FIXED 2026-07-05 (next bullet). - Flashcard disc coloring + disc adoption + CSS cleanup ✅ SHIPPED (web) 2026-07-05 — verify-first per the Flutter handoff (every claim re-checked in source + live, not taken on trust; 3 items changed vs the handoff description). (a) Lightbox review painted a completed flashcard RED — flashcards are step_type=question but never set data-step-correct, so review-disc coloring hit the incorrect→red branch; now routed to green --completed (guide-lightbox.js enterReviewMode). (b) Collection flashcard hydrated RED and live-reveal left the disc neutral until reload — handoff said only the live path; verify found hydration also reddened it (is_correct is null for flashcards → fell to --answered-incorrect); fixed BOTH paths → greens live and on reload via --completed/✓ (block-interaction-compiler.js). (12) Standalone question discs now adopt the Flutter-lead g-step-num--done/✓ + --wrong/✗ on hydrate/submit/reveal, reset→? (new _biMarkStandaloneDisc + _biVisualReset). Retired 25 dead g-* selectors (g-rating/meta/callout/badge/media/lightbox + progress-fill--brand; 0 refs by boundary-grep — a naive substring grep false-positived g-badge on tag-badge-guide). Review nits: answer/explain indent → calc(var(--g-disc)+.6rem)=39.6px, --revealed font-size:13.5px. Live-smoked on dev: review flashcards green (post 49 / guide 4 / lessons 5&7), standalone reveal + hydration green (post 46); collection --completed→green confirmed via CSS (:579-580). node --check clean both JS. Files: guide-lightbox.js, block-interaction-compiler.js, kon_guide_v2.css. - ~~Flashcard-tip render check (22.6 fix #4, was data-impossible)~~ ✅ VERIFIED BOTH PLATFORMS 2026-07-05. Operator-approved one-row dev-DB UPDATE seeded a tip on step 25 ("Joinery Knowledge Check", Intro to Woodworking lesson 5 — tip kept in dev DB so the path stays testable; revert = UPDATE steps SET tip = NULL WHERE id = 25;). A/B: Flutter (marionette, lesson 5) and web (playwright, same step) render the identical amber .g-tip callout above the flashcard card — first live render of this path on either platform.

Program close-out ceremony — DEFERRED TO PROGRAM END (operator call, 2026-07-05)

The governance the main doc prescribed (RI Plans post, docs/INDEX.md entry, Control-post 2511 reconcile) plus the phase-19 "file an issue" for the W1 episode/series cover IDOR run as ONE close-out at the true end: after white-label apps are generated on prod and verified on device. Sequence then = gen covers → create posts → Bell push notify → archive → reconcile INDEX + Control (playbook: docs/playbooks/issue-closeout.md). Until then the W1 IDOR is tracked ONLY as row W1 in this punchlist's web-team table (accepted consciously — the web team is actively working that queue, cf. W12 done 2026-07-04).

Status note (deployment reality)

  • Web Phases 1–21 — merged + deployed to prod (cwcocktails.com / admin.roughindustries.com).
  • Flutter 22.2/22.3/22.4 (ec30c714 / 83d32232 / 11846f2e) — committed to master, push-pending GoLand; NOT yet in a mobile app-store build. So the Flutter fixes are "shipped in-repo," not live on devices.

Post-close hardening (Go-team review finding, 2026-07-05)

  • Flashcard type-guard on the collection disc (c7c3bc0a) — the collection surface leaned solely on the backend's is_correct=null flashcard contract; a reveal records interactionType 'answer', so a contract regression would have painted the disc red (the web bug family, which web now guards via [data-flashcard-back]). Added the explicit !isFlashcardStep guard (matching the standalone + review surfaces) + a contract-break regression test (verified to fail unguarded). Web↔Flutter symmetry restored on all 3 surfaces.

Standing guardrails (confirmed 2026-07-05 — permanent constraints, not work)

  1. Never re-run db/seeds/220_materials.sql — it created the dev-only 500g g unit doubling; prod data is clean.
  2. Guide-progress resets go through the Ent path, never raw SQL DELETE — raw deletes orphan ent edges (Spartacus lesson).
  3. D21.1 — never merge the two tracking systems (UserBlockInteraction vs UserGuideProgress); operator-locked, independence proven at every layer.
  4. Re-verify LD+JSON when a new guide type ships (W11 watch-item; recipeInstructions from lightbox-only steps is defensible today, re-check if Google tightens policy).
  5. D20.1a stays available — the review-mid-course CTA flip is a recorded option, not a bug, if product wants review before completion.
  6. D22.5 light-brand conditional exists on web — any Flutter brand-contrast work must check the web conditional first, not invent its own.
  7. White-on-pale-brand contrast is a LATENT shared risk — web-identical by design; fix (if ever) must land on both platforms together.
  8. Learn-flow is dormant on cook-only prod — prod guides are all recipes; learn-mode features can't be prod-verified until a course exists in prod. Don't mistake dormant for broken.
  9. Never commit: root CLAUDE.md, flutter/gokonversed/.claude/settings.local.json, lib/config/site_config.dart (white-label rebranding target).

Decided-out (recorded so they're not re-litigated as gaps)

  • Material unit doubling (500g g) — dev-seed-only; prod clean; don't re-run db/seeds/220_materials.sql. (Phase 18 non-issue.)
  • Material slug render, D13.4 structured quantity_*/unit_* (dev-seed-only), collection-item description (field doesn't exist), media_collection autoplay/loop/shuffle (standalone block).
  • Doc/PDF guide media (already works, Phase 14) · hidden-block data-spans for guide blocks (excluded by design, Phase 15) · the two-tracking reconcile (already independent, Phase 21) · current_step_index vestigial (keep + documented, D21.2).
  • "Toggle 500-broken" / "make the course sequential" — retracted (contaminated dev data from a raw DELETE with FK off, not a bug; always reset via the Ent path).
  • 6 cosmetic legacy CSS leftovers dropped at retirement (operator: "keep only if used").
← Back to Under the Tuscan Sun