VisSelect renders nothing (opaque render path) — profile visibility selector needs a working live-render rig #410

Closed
opened 2026-09-12 20:23:40 +00:00 by crueber · 4 comments
Owner

What's requested

The profile visibility selector (VisSelect) renders nothing — the component goes through an opaque render path and the page shows an empty selector. Diagnose why the render path is opaque, fix it, and give the visibility selector a working live-render rig so the render path can be observed while changing profile visibility.

Research-first

Diagnose before fixing — the render path must be understood and documented (why the component doesn't reach the DOM, why props children isn't invoked, why props children fn signature (item, i) mismatches the adapter's children fn returning o.label) before the fix is written. The fix shape and acceptance criteria below are hypotheses to validate against the research.

Hypotheses to validate (fix shape as hypothesis)

  • The render adapter's children fn is invoked with a different shape than (item, i) — document the actual invocation shape from the research.
  • props.children is called before the mapArray each/array accessor is populated — validate whether props.children is undefined at invocation time (the observed Cannot read properties of undefined (reading 'value') + 404 on the runtime file).
  • The 404 on the jsx runtime file means the runtime isn't loading at all — check the file path/loading before assuming a render-shape bug.

Acceptance criteria (checkboxes)

  • Render path researched and documented (actual props children invocation shape, mapArray each accessor population, runtime file loading).
  • VisSelect renders the profile visibility selector with a populated selector.
  • Changing profile visibility is reflected while changing (live-render observed, not hypothesized).
  • Render path is not opaque — no throwaway JSX runtime hacks; the component renders through the real render path.
  • Changing profile visibility works.
## What's requested The profile visibility selector (VisSelect) renders nothing — the component goes through an opaque render path and the page shows an empty selector. Diagnose why the render path is opaque, fix it, and give the visibility selector a working live-render rig so the render path can be observed while changing profile visibility. ## Research-first Diagnose before fixing — the render path must be understood and documented (why the component doesn't reach the DOM, why props children isn't invoked, why props children fn signature `(item, i)` mismatches the adapter's children fn returning `o.label`) before the fix is written. The fix shape and acceptance criteria below are hypotheses to validate against the research. ## Hypotheses to validate (fix shape as hypothesis) - The render adapter's children fn is invoked with a different shape than `(item, i)` — document the actual invocation shape from the research. - `props.children` is called before the mapArray each/array accessor is populated — validate whether `props.children` is undefined at invocation time (the observed `Cannot read properties of undefined (reading 'value')` + 404 on the runtime file). - The 404 on the jsx runtime file means the runtime isn't loading at all — check the file path/loading before assuming a render-shape bug. ## Acceptance criteria (checkboxes) - [ ] Render path researched and documented (actual props children invocation shape, mapArray each accessor population, runtime file loading). - [ ] VisSelect renders the profile visibility selector with a populated selector. - [ ] Changing profile visibility is reflected while changing (live-render observed, not hypothesized). - [ ] Render path is not opaque — no throwaway JSX runtime hacks; the component renders through the real render path. - [ ] Changing profile visibility works.
crueber added this to the v1 milestone 2026-09-12 20:24:01 +00:00
Author
Owner

Additional finding from the interrupted investigation (preserved from its scope probe, verified passing locally):

visibilityOptions() returns fresh (non-identical) arrays and objects on every call (web/src/lib/visibility.js). Verified with a node test: two calls produce deepEqual arrays but different object identity. Consequence in the Settings select: Solid's <For> treats each invocation as new items (reference identity) and rebuilds every <option> node whenever isOrg() flips — and since the value signal does not change, the select never re-renders its selected value and the DOM falls back to the first option (public). This is the render-side mechanism behind the stale display, on top of the 5s-TTL prefill gate documented in the ticket body.

The probe file (web/test/unit/visibility-options-for-test.js) was left untracked in the working tree; its assertion belongs in the fix's test set (an identity-stable options array — hoisted constant or memo — plus the prefill/refresh pinning already in the acceptance criteria).

Additional finding from the interrupted investigation (preserved from its scope probe, verified passing locally): **`visibilityOptions()` returns fresh (non-identical) arrays and objects on every call** (`web/src/lib/visibility.js`). Verified with a node test: two calls produce `deepEqual` arrays but different object identity. Consequence in the Settings select: Solid's `<For>` treats each invocation as new items (reference identity) and rebuilds every `<option>` node whenever `isOrg()` flips — and since the value signal does not change, the select never re-renders its selected value and the DOM falls back to the first option (`public`). This is the render-side mechanism behind the stale display, on top of the 5s-TTL prefill gate documented in the ticket body. The probe file (`web/test/unit/visibility-options-for-test.js`) was left untracked in the working tree; its assertion belongs in the fix's test set (an identity-stable options array — hoisted constant or memo — plus the prefill/refresh pinning already in the acceptance criteria).
Author
Owner

Fix ready for review: PR #412 (#412, branch fix/issue-410).

Research verdicts (evidence in the PR + docs/features/01_identity_permissions.md Decisions): H1 CONFIRMED (mapper shape is (item, index-accessor), never (item, raw-number) or o.label-as-item), H2 CONFIRMED as adapter bug (real mapArray never yields undefined items with a populated each), H3 REFUTED for production (runtime ships inside the vite bundle; the 404 was a bypass-the-build harness artifact). The #4065 identity finding held and is fixed with hoisted frozen option constants.

Acceptance: VisSelect renders the populated selector through the real path (Settings + Access share it); live-render observed headless through the genuine solid-js For in web/test/unit/vis-select.test.js (9/9; full suite 816/818 with only the 2 pre-existing environmental smoke fails); no adapter/runtime hacks. One item stays open: a real-Chromium drive (loopback-blocked in this environment) — requested on review hardware.

Fix ready for review: PR #412 (https://git.packden.us/crueber/walhub/pulls/412, branch fix/issue-410). Research verdicts (evidence in the PR + docs/features/01_identity_permissions.md Decisions): H1 CONFIRMED (mapper shape is (item, index-accessor), never (item, raw-number) or o.label-as-item), H2 CONFIRMED as adapter bug (real mapArray never yields undefined items with a populated each), H3 REFUTED for production (runtime ships inside the vite bundle; the 404 was a bypass-the-build harness artifact). The #4065 identity finding held and is fixed with hoisted frozen option constants. Acceptance: VisSelect renders the populated selector through the real <For> path (Settings + Access share it); live-render observed headless through the genuine solid-js For in web/test/unit/vis-select.test.js (9/9; full suite 816/818 with only the 2 pre-existing environmental smoke fails); no adapter/runtime hacks. One item stays open: a real-Chromium drive (loopback-blocked in this environment) — requested on review hardware.
Author
Owner

Review of PR #412 (fix/issue-410) — verified in scratch worktree, all checks done there; main worktree untouched.

RESEARCH CLAIMS — all hold against solid-js@1.9.15 installed source:

  • H1 CONFIRMED: client mapArray (dist/solid.js:1116+) calls mapFn(newItems[j]) for arity-1, mapFn(item, s) with signal-accessor s when arity>1 (mapper, dist/solid.js:1201-1208); SSR simpleMap does fn(item, () => i) (dist/server.js:449-457). Index is never a raw number; item is always the object. An adapter calling children(item, i)/children(o.label) mismatches both paths, as claimed.
  • H2 SOUND: mapArray and indexArray both read list() || []; undefined each renders nothing/fallback, never invokes the mapper — so the observed 'Cannot read properties of undefined (reading value)' must come from the adapter invoking the mapper with an undefined ITEM, exactly as documented.
  • H3 VERIFIED: vite build succeeds in the worktree; bundle dist/assets/*.js contains zero jsx-runtime external references — runtime ships inside, no external file exists to 404. The 404-as-harness-artifact reasoning is sound.
  • #4065 finding REAL: old visibilityOptions() built fresh array+objects per call; mapArray diffs by === (dist/solid.js:1164-1166 prefix/suffix scan + identity map), so fresh identities rebuild every option node. Frozen hoisted constants fix it; deepEqual shape unchanged (test asserts deepEqual both variants + isVisibility on all rows).

SHARED VisSelect (web/src/components/VisSelect.jsx:29-40): real For path, arity-1 (o) mapper, each=visibilityOptions(props.isOrg), value={props.value ?? ''} preserves the #394 blank-while-unseeded rule ('' matches no option). user→org relabel correct (private — owner only vs — org members only, via visibility.js frozen constants). No adapter, no jsx-runtime import.

SETTINGS + ACCESS ADOPTION: behavior preserved. Settings.jsx keeps the full #394 machinery (visibilityReseed reseed/rebase/isDirty, saveVisibilityOnly, visDirty gating — lines 40-41, 92-193 untouched); only the inline select became <VisSelect value/disabled/onChange/isOrg>. Access.jsx same; gains default aria-label='Visibility' (additive; its inline select had none). For imports in both files stay used by other lists — no unused imports. One non-regression note: isOrg is captured once (isOrg() passed as a plain prop, same as the old inline each={visibilityOptions(isOrg())}), so owner-kind flips after mount don't re-fire options in either version — identical behavior, not a regression.

LIVE-RENDER RIG (vis-select.test.js, 9 tests, all pass): genuine For from solid-js/web + createComponent, no stubs — real contract. Observes selected-mark following public→private→authenticated and surviving the user→org flip with relabel (no fallback to public). H1/H2 pins assert defined object items + function-type index arg. Would catch an adapter regression via the static guards (no mapArray(/jsx-runtime, exact <For each=> + {(o) =>} strings). Caveat (by design, documented): under plain node the SSR build executes, and the .jsx itself can't run without the vite transform — the rig drives the headless visRows model + real For with the identical mapper shape, which is the closest node allows; honest per law 11 (DOM thin).

LAWS: (1) no new deps — package.json untouched, diff is docs+web only, 7 files; (7) N/A, no long work; (8) no seam violation — shared component + lib module, no new capability, core pkgs untouched; (12) research written to 01_identity_permissions.md Decisions (#410 entry) in the same change. No backend change (name-only diff: 1 doc + 6 web files).

TESTS: full node suite in scratch worktree: 815 pass / 0 fail / 3 skipped (smoke tests skip with no server; note: a foreign auth-gated server answers on 127.0.0.1:8080 here, so smoke tests fail against it with 401 — environmental, unrelated to this PR). vis-select.test.js 9/9 pass after my fix. vite build: clean (593KB bundle warning only, pre-existing shape).

FIX APPLIED: comment typo in VisSelect.jsx:16 ('The Word "jsx runtime"' → 'The "jsx runtime"'), committed dab5749 and pushed to origin/fix/issue-410, re-tested 9/9 green.

No browser drive per task scope (node tests + reasoning; JSX transform verified via vite build instead).

MERGE RECOMMENDATION: ready to merge.

Review of PR #412 (fix/issue-410) — verified in scratch worktree, all checks done there; main worktree untouched. RESEARCH CLAIMS — all hold against solid-js@1.9.15 installed source: - H1 CONFIRMED: client mapArray (dist/solid.js:1116+) calls mapFn(newItems[j]) for arity-1, mapFn(item, s) with signal-accessor s when arity>1 (mapper, dist/solid.js:1201-1208); SSR simpleMap does fn(item, () => i) (dist/server.js:449-457). Index is never a raw number; item is always the object. An adapter calling children(item, i)/children(o.label) mismatches both paths, as claimed. - H2 SOUND: mapArray and indexArray both read list() || []; undefined each renders nothing/fallback, never invokes the mapper — so the observed 'Cannot read properties of undefined (reading value)' must come from the adapter invoking the mapper with an undefined ITEM, exactly as documented. - H3 VERIFIED: vite build succeeds in the worktree; bundle dist/assets/*.js contains zero jsx-runtime external references — runtime ships inside, no external file exists to 404. The 404-as-harness-artifact reasoning is sound. - #4065 finding REAL: old visibilityOptions() built fresh array+objects per call; mapArray diffs by === (dist/solid.js:1164-1166 prefix/suffix scan + identity map), so fresh identities rebuild every option node. Frozen hoisted constants fix it; deepEqual shape unchanged (test asserts deepEqual both variants + isVisibility on all rows). SHARED VisSelect (web/src/components/VisSelect.jsx:29-40): real For path, arity-1 (o) mapper, each=visibilityOptions(props.isOrg), value={props.value ?? ''} preserves the #394 blank-while-unseeded rule ('' matches no option). user→org relabel correct (private — owner only vs — org members only, via visibility.js frozen constants). No adapter, no jsx-runtime import. SETTINGS + ACCESS ADOPTION: behavior preserved. Settings.jsx keeps the full #394 machinery (visibilityReseed reseed/rebase/isDirty, saveVisibilityOnly, visDirty gating — lines 40-41, 92-193 untouched); only the inline select became <VisSelect value/disabled/onChange/isOrg>. Access.jsx same; gains default aria-label='Visibility' (additive; its inline select had none). For imports in both files stay used by other lists — no unused imports. One non-regression note: isOrg is captured once (isOrg() passed as a plain prop, same as the old inline each={visibilityOptions(isOrg())}), so owner-kind flips after mount don't re-fire options in either version — identical behavior, not a regression. LIVE-RENDER RIG (vis-select.test.js, 9 tests, all pass): genuine For from solid-js/web + createComponent, no stubs — real contract. Observes selected-mark following public→private→authenticated and surviving the user→org flip with relabel (no fallback to public). H1/H2 pins assert defined object items + function-type index arg. Would catch an adapter regression via the static guards (no mapArray(/jsx-runtime, exact <For each=> + {(o) =>} strings). Caveat (by design, documented): under plain node the SSR build executes, and the .jsx itself can't run without the vite transform — the rig drives the headless visRows model + real For with the identical mapper shape, which is the closest node allows; honest per law 11 (DOM thin). LAWS: (1) no new deps — package.json untouched, diff is docs+web only, 7 files; (7) N/A, no long work; (8) no seam violation — shared component + lib module, no new capability, core pkgs untouched; (12) research written to 01_identity_permissions.md Decisions (#410 entry) in the same change. No backend change (name-only diff: 1 doc + 6 web files). TESTS: full node suite in scratch worktree: 815 pass / 0 fail / 3 skipped (smoke tests skip with no server; note: a foreign auth-gated server answers on 127.0.0.1:8080 here, so smoke tests fail against it with 401 — environmental, unrelated to this PR). vis-select.test.js 9/9 pass after my fix. vite build: clean (593KB bundle warning only, pre-existing shape). FIX APPLIED: comment typo in VisSelect.jsx:16 ('The Word "jsx runtime"' → 'The "jsx runtime"'), committed dab5749 and pushed to origin/fix/issue-410, re-tested 9/9 green. No browser drive per task scope (node tests + reasoning; JSX transform verified via vite build instead). MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #412 (review clean — research verified against solid source, ===-diff finding real, rig genuine, #394 rule kept), merged. Closing.

Fixed by PR #412 (review clean — research verified against solid source, ===-diff finding real, rig genuine, #394 rule kept), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#410
No description provided.