Target: https://litmanintelligence.com/review/ (cache-busted with ?v=<timestamp>)
Trigger: Re-run after deployment of the jsArg(s) = escapeHtml(JSON.stringify(s)) HTML-attribute escaping fix
Source-of-truth script: scripts/build_review_site_v2.py (helper at line 694, all 29 inline-onclick interpolations now use it)
Driver: Playwright (Chromium, headless) — script saved at /tmp/qa_review_site_verify.py
Raw results: /tmp/qa_results_verify.json
Screenshots / artifacts: output/qa_screenshots_verify/
BINDER CLICK FIX CONFIRMED.
All previously-broken click-driven sidebar features now work end-to-end. State is correctly set, the active class is applied, the table re-renders, and the browser logs 0 console errors / 0 page errors for a full session including binder create/click/add/rename/delete and every facet group.
| Metric | Value |
|---|---|
| Tests run | 30 |
| PASS | 28 |
| PARTIAL | 1 |
| Apparent FAIL (after analysis: non-bugs) | 1 |
| Real FAIL | 0 |
| Console errors | 0 |
| Page errors | 0 |
jsArg( references in served HTML |
34 |
Broken state.filters.binder="..." interpolations in served HTML |
0 |
_lastFiltered, _lastRefresh literal in served HTML |
yes (required marker present) |
Both apparent FAILs were investigated and confirmed to be expected/correct behavior, not regressions:
{custodian: "Richard C. Litman"}, proving the click handler fires. Reclassified to PASS (no-op single-bucket facet).<td colspan="9"><div class="empty"><h4>No results</h4>...), not a data row. The actual filtered result set is empty. Reclassified to PASS.$ curl -s "https://litmanintelligence.com/review/?v=$(date +%s)" | grep -c 'jsArg('
34
$ curl -s "https://litmanintelligence.com/review/?v=$(date +%s)" | grep -c 'state.filters.binder="'
0
$ curl -s "https://litmanintelligence.com/review/?v=$(date +%s)" | grep -c '_lastFiltered, _lastRefresh'
1
All three checks PASS. The fix has been deployed to the live build.
| # | Feature | Prev | Now | Notes |
|---|---|---|---|---|
| 01 | Page loads (cache-busted, networkidle) | PASS | PASS | EMAILS=59,684, DOCS=3,130 |
| 01b | Patch markers in DOM (jsArg( ≥1, no broken interpolations, persistence literal present) |
n/a | PASS | jsArg=34, broken=0, _lastFiltered, _lastRefresh literal present |
| 02 | Tab switching (Emails / Documents / Dashboard / Reference) | PASS | PASS | all four tabs render |
| 03 | Search filtering (KFU / Goldberg / 11873299) | PASS | PASS | baseline=59,684 → 20,000 / 44,046 / 1; restores cleanly |
| 04 | Search scope toggle (All / Subject / Content) | PASS | PASS | All=44,046, Subject=229, Content=40,375 |
| 05a | Sidebar facet 'Archive' (Emails) | FAIL | PASS | clicked ND0002: 59,684 → 45,650; state.filters.archive='ND0002' |
| 05b | Sidebar facet 'Year' (Emails) | FAIL | PASS | clicked 2024: 59,684 → 18,282; state.filters.year='2024' |
| 05c | Sidebar facet 'Tag' (Emails) | FAIL | PASS | clicked NGM: 59,684 → 58,931; state.filters.tag='NGM' |
| 05d | Sidebar facet 'From Domain' (Emails) | PASS | PASS | clicked NGM (@nathlaw.com): 59,684 → 58,931 |
| 05e | Sidebar facet 'Hot' (Emails) | PASS | PASS | clicked Hot only: 59,684 → 0 |
| 05f | Sidebar facet 'Category' (Documents) | FAIL | PASS | clicked DOCUMENT_PHOTOS: 3,130 → 1,536; state.filters.category='DOCUMENT_PHOTOS' |
| 05g | Sidebar facet 'Doc Type' (Documents) | FAIL | PASS | clicked Image: 3,130 → 2,021; state.filters.docType='Image' |
| 05h | Sidebar facet 'Custodian' (Documents) | FAIL | PASS* | clicked Richard C. Litman: state correctly updated to {custodian: 'Richard C. Litman'}. Count stays at 3,130 because Litman IS the only custodian (100% of 3,130 docs). Click handler IS firing — this is a single-bucket no-op facet, not a regression. |
| 06 | My Binders — create / click / filter / rename / delete (HEADLINE BUG) | FAIL | PASS** | Create works. Click-binder applies filter (state.filters.binder = 'Test Binder'). Active class applied. Empty binder shows the empty-state placeholder row (correct). After adding 1 row, re-clicking shows that 1 row. Rename + delete via action buttons both work. |
| 06s | Binder w/ apostrophe Joe's docs |
n/a | PASS | created, clicked, state.filters.binder == "Joe's docs" |
| 06s | Binder w/ ampersand A&B |
n/a | PASS | created, clicked, state.filters.binder == "A&B" |
| 06s | Binder w/ double-quote Quote"Test |
n/a | PASS | created, clicked, state.filters.binder == 'Quote"Test' (round-trips through jsArg = escapeHtml(JSON.stringify(...)) cleanly) |
| 07 | Right-click context menu — Mark Produce | PASS | PASS | row decision flips correctly |
| 08 | Quick-produce checkbox | PASS | PASS | toggles Produce on/off |
| 09 | Detail-pane decision dropdown (Withhold - Privileged) | PASS | PASS | row pill + footer Privileged count update |
| 10 | Inline document viewer (PDF iframe / image) | PARTIAL | PARTIAL | 0 iframes / 0 images on first row of Documents tab — same as prior run; not blocked by this fix; worth a manual smoke-check on a known-PDF row. |
| 11a | Keyboard / focuses search |
PASS | PASS | document.activeElement.id === 'searchBox' |
| 11b | Keyboard j (next row) |
PASS | PASS | selection moves forward |
| 11c | Keyboard k (prev row) |
PASS | PASS | selection moves back |
| 11d | Keyboard p (mark Produce) |
PASS | PASS | dec=Produce |
| 12 | Bulk action ("produce" via prompt) | PASS | PASS | filtered set marked |
| 13 | Export Production CSV | PASS | PASS | download triggered, 282 bytes |
| 14 | Persistence across reload (tab, search, tags) | PASS | PASS | tab=docs, search=patent, tags=3 persisted |
| 15 | Footer counts (Marked / Privileged / Hot / Binders) | PASS | PASS | all live-update; ftBinders=1 reflects the leftover Test Binder before delete |
| 16 | Browser console / page errors | FAIL (4 errors) | PASS | 0 errors, 0 warnings, 0 page errors for a full session including 9 facet clicks + 4 binder creates + add/rename/delete |
* Custodian facet is a single-bucket no-op on the current dataset; the click handler IS firing. State change confirms the fix.
** Test driver's row-count check needed adjustment; the "1 row" was the empty-state <td colspan="9"> placeholder, not a data row. State + active-class + sidebar-update all confirm the fix.
| Check | Pre-fix (2026-04-28 run #1) | Post-fix (this run) |
|---|---|---|
state.filters.binder after click |
undefined (NEVER set) |
"Test Binder" correctly set |
.item gains active class |
NO | YES |
| Table re-renders | NO | YES (shows empty-state placeholder for 0 results) |
| Browser console | Failed to execute 'click' on 'HTMLElement': Unexpected end of input |
silent |
| Special-character names work? | not testable (couldn't even click plain names) | yes — apostrophe, ampersand, AND double-quote all round-trip cleanly through escapeHtml(JSON.stringify(name)) |
| Group | Tab | Pre-fix | Post-fix |
|---|---|---|---|
| Archive | Emails | click did nothing | 59,684 → 45,650 |
| Year | Emails | click did nothing | 59,684 → 18,282 |
| Tag | Emails | click did nothing | 59,684 → 58,931 |
| Category | Documents | click did nothing | 3,130 → 1,536 |
| Doc Type | Documents | click did nothing | 3,130 → 2,021 |
| Custodian | Documents | click did nothing | state correctly updates (single-bucket dataset means count doesn't drop) |
| From Domain | Emails | already working (hand-built single-quoted strings) | still working |
| Hot | Emails | already working | still working |
All six previously-broken groups now respond to clicks; the four that previously worked continue to work.
The new jsArg = escapeHtml(JSON.stringify(...)) helper survives:
- Apostrophe Joe's docs — would have broken the previous state.filters.binder='${name}' if anyone had switched to single-quoted JS strings
- Ampersand A&B — would have HTML-decoded incorrectly in many naive escape schemes
- Double-quote Quote"Test — was the exact failure mode of the original bug
All three created cleanly, clicked cleanly, and produced an exact-match state.filters.binder round-trip — including the embedded " character.
None. The test driver registered listeners for both console events (filtered to error/warning types) and pageerror events. Across the full session — 30 tests, 9 facet clicks, 4 binder creates with create/click/add/rename/delete cycles, full keyboard nav, bulk action, and a reload — the browser produced:
Compare to the pre-fix run, which logged Failed to execute 'click' on 'HTMLElement': Unexpected end of input four separate times during equivalent interactions.
When I switched to Documents and clicked the first row, 0 iframes and 0 images rendered in the right pane. This is the same result as the prior run — it is not caused by, and not affected by, the jsArg fix. Possible explanations carried over from the prior report: - Many docs are PDFs hosted out-of-band; the deployed build may not be wiring native files for all rows yet. - The viewer markup may be rendered only on user gesture (lazy). - Only the first row was tested; a known-PDF row might behave differently.
Recommendation: unchanged from prior report — 5-minute manual smoke check on a known-PDF row and a known-image row.
DOCS currently contains 3,130 documents, all with cu = "Richard C. Litman". Clicking the custodian filter is therefore a visible no-op (count stays at 3,130). The fix works — state.filters.custodian is correctly set — but if you want to verify the visible count drops, you'll need a multi-custodian dataset, or this facet group can be hidden/collapsed when only one bucket exists. Low priority; not a regression.
Every test that passed in the pre-fix run still passes. No regressions.
/tmp/qa_review_site_verify.py/tmp/qa_results_verify.json/tmp/qa_verify_run.log/Users/awesomefat/Dropbox/LitmanDev/RichieResearch Claude Code/output/qa_screenshots_verify/05_custodian_no_change.png — single-bucket facet (expected)06_binders_summary.png — final binder state (delete confirmed)99_final_state.png — clean post-run stateexport_<ts>.csv — Export Production download artifact/Users/awesomefat/Dropbox/LitmanDev/RichieResearch Claude Code/scripts/build_review_site_v2.pyjsArg helper at line 694renderItem(...) template uses ${jsArg(v)} and ${jsArg(name)} at lines 780 and 799onclick=/onchange= attributes that interpolate user-derived strings now route through jsArg