Tutorial: 2026-07-10

2026-07-10

Newest entry on top.

Mobile full-parity campaign — Phases 1a + 2 (tablet layouts + Manage data layer)

Continued the campaign in green, committed slices:

  • Phase 1a — tablet content layouts. Gallery fills the full width on tablet/wide (larger cell target; was capped at 900px; phone sizing byte-identical). New components/ContentColumn.js (RN counterpart to the web's 960px reading column) wraps Generate/Single/Manage on tablet; Gallery opts out. Component test guards the "same content at every size" invariant. test:mobile 46/46.
  • Phase 2 — Manage data layer. lib/storage.js gains the on-device user overlay: user blocks CRUD (.dpl), .js/.json sidecars, nested folders (recursive walk), a readUserTree builder mirroring the web tree model, folder create/delete + entry move/rename (10 tests). lib/listOps.js ports the web listEditorOps.js (sort/dedupe/AI parse+merge) verbatim; Sort + Dedupe wired into the Manage list editor. New parity-gate step checkListOps asserts the port stays behaviorally identical to the web source. test:mobile 60/60, parity + metro gates green.

Data-layer feasibility confirmed (see ../../plans/mobile-parity.md): metroLoader exposes the whole built-in catalog and the metro catalog inlines raw .dpl/list source, so the built-in tree + source-view + override are all buildable on device. Next: the Manage UI (two-root tree reusing readUserTree + built-ins, the block/DPL editor reusing the GenerateScreen editor, folder editor), then Single two-pane on tablet, then wire the overlay into metroLoader + flip the strict gate.

Mobile ⇄ web FULL-parity campaign — Phase 0 (audit + foundation)

Owner set a hard mandate: the mobile (Android/Expo) app must be at COMPLETE feature + appearance parity with the web app — no exceptions, no size-based feature loss, no "simpler mobile" version — the whole app on every device (phone AND tablet) to the extent the platform allows, enforced behind the release quality gate. The only sanctioned non-parity is the SFW/NSFW build variant (the --tier=sfw catalog physically removes NSFW for the all-ages Play listing; an NSFW APK keeps it) — a compile-time split, not a feature drop.

Combed every mobile screen/component against its web counterpart and the existing parity framework (metro-parity-check engine parity, mobile-parity-check ported-catalog + surface markers, jest-expo component tests, mobile-visual-parity). Current state is green but has real gaps vs. the mandate:

  • Manage is the big one — mobile ManageScreen is lists-only (flat user word-lists), while web Manage is a full content manager (Blocks + Lists roots, folder tree, DPL/block editor with Insert/Refine/Modify-Draft + JS sidecar, list editor Entries/Raw/Sort/Dedupe/AI-Expand, folder editor, override/restore). The parity gate didn't even check Manage, so it hid the gap.
  • Tablet/responsive — the app is single-column at every size; web has phone/tablet/wide tiers with two-pane master/detail on tablet.
  • Visual parity was phone-only (390px).

Wrote the authoritative campaign + gap audit + phased plan: plans/mobile-parity.md (and strengthened the standing instruction in systems/mobile.md). Phase-0 deliverables (this commit, all green):

  • Multi-size visual paritymobile-visual-parity.mjs now shoots the full phone→tablet matrix (phone-small 360 / phone 390 / phone-large 430 / tablet-portrait 834 / tablet-landscape 1112) into artifacts/mobile-parity/<size>/, with a --size=<id> filter. Directly serves the "phones and tablets, no size-based feature loss" mandate.
  • Responsive foundationlib/responsive.js (useResponsive() + pure resolveResponsive), breakpoints mirroring the web tiers (phone ≤768 / tablet 769–1024 / wide >1024) with twoPane + capped reading column; unit-tested (lib/__tests__/responsive.test.js). The per-screen tablet layouts build on this.
  • Test hygiene — fixed the jest FlashList mock so ListEmptyComponent is keyed (killed the "unique key" warning ManageScreen emitted under test).

Verified: test:mobile 44/44 (was 40), mobile:parity green, metro:parity green, lint 0 errors. Phases 1–4 (per-screen tablet layouts; the full RN Manage port reusing the existing GenerateScreen DPL editor; override/restore + runtime overlay; flip the strict Manage gate on) follow — each lands green.

Manage tab — fix the phone/mobile editors (right pane)

Comprehensive web-vs-mobile comparison of the Manage tab (both code and live screenshots), then fixed everything that was actually broken on the phone layout. Verification method note: the Claude-in-Chrome automation browser here is fixed at ~1707 CSS px and ignores resize_window, so true narrow viewports were obtained by loading the dev app (localhost:5173) inside a same-origin iframe sized to 360/390/768/1000 px — media queries evaluate against the iframe's width, so the real mobile CSS renders and can be screenshotted + measured (matchMedia('(width<=768px)') confirmed).

What was fine on mobile (left untouched): the master tree view, the list-entries virtualized rows, and the folder editor all render cleanly at 390 px.

Three real bugs found + fixed (all in the right-pane editors):

  1. Block editor — the "Modify / Draft" combo overlapped the code. DplAskCorner is absolutely pinned to the DPL box's top-right corner. Its intended clearance — padding-top: 2.5rem on .cm-content (in dpl-refine-toolbar.css) — is silently defeated by CodeMirror's own base padding (measured computed padding-top = 4px, not 40px; same source-order override the editorChrome.js theme documents for the gutter). On desktop the pane is wide enough that short lines never reach the corner, so it only bit on the narrow phone pane, where wrapping front-matter ran straight under the buttons. See fix-patterns.
  2. Editor heads squeezed the name field to ~21px. .mg-editor-head is a nowrap flex row tuned for a wide desktop pane; at ~360px the .mg-name-input collapsed to an unreadable/uneditable sliver (measured 21px).
  3. List tools squeezed the entries search to ~24px. Same nowrap cause on .mg-list-tools.

Fix (all in targets/web/frontend/styles/components/manage-responsive.css, one new @media (width <= 768px) block, every selector scoped to .workspace.manage so desktop and the Generate composer are untouched):

  • Heads + list-tools flex-wrap: wrap; the name input and the entries search each take a full-width first line (flex: 1 1 100%), the rest wrap beneath.
  • On phone the Modify/Draft combo un-pins: .dpl-editor-wrap becomes a flex column and .dpl-ask-corner goes position: static; order: -1; align-self: flex-end — a right-aligned row above the editor, so it can never overlap the code (no dependence on the overridden CodeMirror gutter).
  • Its popover would be clipped by .mg-editor-body { overflow: hidden }, so on phone it becomes a full-width bottom sheet (position: fixed; inset: auto 0 0; z-index: 91) over a dimming scrim — the same treatment the header submenus already use (mobile-sheets.css).

Verified (measured live in the iframe, before → after): corner absolutestatic, no vertical overlap with .cm-content; name field 21px→295px; entries filter 24px→295px; popover now fixed full-width. Desktop (≥769px) confirmed unchanged (corner absolute, name 14rem). Breakpoints 360/768 mobile, 769+ desktop all correct. Added two Playwright regression tests to tests/e2e/responsive.spec.js (block-editor corner clears the code + head fields usable; list filter usable width) — the manage e2e group is 5/5 green. lint (0 errors) + lint:css clean.

CSS-only + a test; mobile-only media rules, so no visual-baseline churn on desktop. No data-practice change → legal docs untouched. PATCH-level.

With the two hard gates (CodeQL + SonarCloud) green, the only remaining BLOCKED reason on PR #50 was main's require-conversation-resolution rule against ~15 unresolved CodeRabbit threads. Owner chose "address all, then release", so worked through every one on merit (no blanket resolves):

  • Own hardening code: guarded decodeURIComponent in the visual-parity server (a /% request no longer crashes it); derived build-metro-catalog import specifiers from dirname(outPath); tightened metroLoader.test assertions to reject undefined.
  • Mobile stability: shared fetchWithTimeout (AbortController + 120s, merges caller signal) across every imageProviders.js fetch; 60s timeout race on storage.js remote downloadAsync; keys.js setKey returns success + BYOK field warns on failed keystore write.
  • Mobile error handling / correctness: Gallery load-failure retry + deleteSelected guard; SingleScreen doConvert busy-guard; GenerateScreen insert targets the active Prompt/Negative field + monotonic batch-id (fixes fixed-seed id collision) + always-clear rewrite status; ComfyUI batch_size ≥1; ManageScreen list names disallow /; index.js explicit ./App.js.

Verified: Node coverage gate (352 tests, thresholds held, metroLoader ~87%), metro:parity (150 gens identical), mobile jest (40/40), lint 0 errors, prettier clean. Committed as 2 focused commits (+ the earlier gate-fix commits), pushed; then replied to + resolved all threads. Paused for merge go-ahead.

Release-harden 2.52.0 — clear the PR #50 gate blockers on merit

Picked up the stalled 2.52.0 release. PR #50 (release/2.52.0main) was BLOCKED by two real checks (every other check green): CodeQL code-scanning and the SonarCloud quality gate. Pulled the live details and fixed each on merit rather than silencing anything. Work done on the Windows repo (PowerShell + real push access), verified with the real toolchain before committing.

What was actually blocking (ground truth from the PR):

  • CodeQL — 3 high alerts: "uncontrolled data used in a path expression" (path traversal) in scripts/mobile-visual-parity.mjs. The dev file-server fed req.url straight into join(OUT, …)statSync/readFileSync. Fixed with an absolute-root containment guard (resolve under OUT_ROOT, reject escapes → index.html). Verified functionally: raw + URL-encoded (..%2f, %2e%2e) traversal all fall back to index.html; normal asset paths resolve unchanged.
  • SonarCloud — coverage 0% on new lines: the earlier metroLoader.js coverage exclusion (added to go green) was starving Sonar's lcov too. Reverted it and wrote a real test (tests/mobile/metroLoader.test.js) that earns ~87% and proves metro↔node engine parity. Only the generated metroCatalogData.js stays excluded (data) — now also from Sonar analysis/coverage/CPD so the CI-built blob isn't scanned.
  • SonarCloud — Reliability D + Maintainability + CodeQL 8 dead-code notes: aligned the numeric Steps/CFG/Seed assertions in single.test.js with Sonar's string-valued map inference, switched to toBeNull/toHaveLength, and removed 8 unused imports/vars across the mobile target.

Landmine found + fixed (this machine, not the cloud clone): the new metroLoader.test.js passed standalone but flaked in the full parallel suite (88 vs 89 block names). Root cause: it compared a build-time metro snapshot against a live nodeLoader read while, in parallel, manageFs.test.js writes transient zz-manage-test/* fixtures into the shared engine/data/blocks root and 30+ workers stress the same files (a Windows sharing violation can momentarily hide a real block from a scan). Fixed by moving the catalog build + a clean-state snapshot of nodeLoader's names and parity generations into a Vitest globalSetup (tests/setup/metro-catalog.globalSetup.js) that runs before any worker spawns; the test now compares frozen snapshots via inject. Deterministic green across repeated full runs.

Verification (all on merit, local): eslint 0 errors, prettier clean, check:docs (375 links), smoke, test:coverage (352 tests, coverage above every threshold, metroLoader.js measured ~87%, 3× deterministic), mobile jest (test:mobile, 40 tests / 10 suites), mobile:parity PASS, metro:parity PASS (150 seeded generations identical). Committed as 3 focused commits on release/2.52.0 with their changelog entries; pushed to re-trigger CI. No version bump — 2.52.0 hasn't shipped (origin/main still a9eeaf7, no v2.52.0 tag); these harden the pending release. Merge to main awaits owner go-ahead.