Target: https://litmanintelligence.com/review/
Source-of-truth script: scripts/build_review_site_v2.py (1,702 lines)
Driver: Playwright (Chromium, headless) — script saved at /tmp/qa_review_site.py
Screenshots / artifacts: output/qa_screenshots/
| Metric | Value |
|---|---|
| Tests run | 19 |
| PASS | 15 |
| PARTIAL | 1 |
| FAIL | 3 |
| Distinct user-visible bugs | 2 |
| Distinct root causes | 1 (single broken inline-onclick template) |
Headline finding: the user's bug ("select binder from sidebar and nothing I click over there does anything") is confirmed and reproduced. The root cause is a single line of broken HTML-attribute escaping — scripts/build_review_site_v2.py:761 — and the same line breaks the Archive, Year, Tag, Category, Doc Type, and Custodian sidebar filters too. The user only happened to notice it on binders, but 6 separate filter groups are silently broken.
The browser logs Failed to execute 'click' on 'HTMLElement': Unexpected end of input (4 times during a normal session) — that page-level JS error and the binder bug are the same bug.
| # | Feature | Status | Notes |
|---|---|---|---|
| 01 | Page loads (state global, EMAILS=59,684, DOCS=3,130) |
PASS | full boot in <30s |
| 02 | Tab switching (Emails / Documents / Dashboard / Reference) | PASS | all four render |
| 03 | Search filtering (KFU=20,000, Goldberg=44,046, 11873299=1) | PASS | counts drop & restore |
| 04 | Search scope toggle (All / Subject / Content) | PASS | All=44,046, Subject=229, Content=40,375 |
| 05 | Sidebar filters (Archive / Year / Tag / etc.) | FAIL | Archive + Year + Tag + Category + Doc Type + Custodian clicks do nothing — same root cause as the binder bug |
| 05a | Sidebar From Domain filter | PASS | drops to 58,931 (only filters that hand-build the onclick with single quotes still work) |
| 05b | Sidebar Hot filter | PASS | drops to 0 |
| 06 | My Binders — create / click / filter / rename / delete | FAIL (HEADLINE BUG) | Create works. Clicking the binder row to filter does nothing — state.filters.binder never gets set, item never gains active class, table never re-renders. Add-to-binder, rename, delete all work — only the click-to-filter is dead. |
| 07 | Right-click context menu — Mark Produce | PASS | row decision flips correctly |
| 08 | Quick-produce checkbox (1st column of row) | PASS | toggles Produce on/off |
| 09 | Detail-pane decision dropdown (Withhold - Privileged) | PASS | row pill + footer Privileged count update |
| 10 | Inline document viewer (PDF iframe / image) | PARTIAL | 0 iframes / 0 images on the row I picked. Likely native files are still being lazy-loaded or the deployed build doesn't link to them — not a bug per se, but worth a manual look |
| 11a | Keyboard / focuses search |
PASS | document.activeElement.id === 'searchBox' |
| 11b | Keyboard j (next row) |
PASS | selection moves forward |
| 11c | Keyboard k (prev row) |
PASS | selection moves back |
| 11d | Keyboard p (mark Produce) |
PASS | dec=Produce |
| 12 | Bulk action ("produce" via prompt) | PASS | filtered set marked |
| 13 | Export Production CSV | PASS | download triggered, CSV bytes>0 |
| 14 | Persistence across reload (tab, search, tags) | PASS | tab=docs, search=patent, tags persisted |
| 15 | Footer counts (Marked / Privileged / Hot / Binders) | PASS | live update |
| 16 | Browser console / page errors | FAIL | 4× Failed to execute 'click' on 'HTMLElement': Unexpected end of input — these are the same root cause as #05/#06, surfaced when the broken-onclick handlers run |
JSON.stringify value injected into double-quoted onclick attribute (HEADLINE BUG)Affects: - My Binders (clicking a binder name does nothing) — what the user reported - Archive sidebar group (Emails tab) - Year sidebar group (Emails tab) - Tag sidebar group (Emails tab) - Category sidebar group (Documents tab) - Doc Type sidebar group (Documents tab) - Custodian sidebar group (Documents tab)
Severity: P0 — six entire sidebar filter groups are silently dead. The user can still filter by typing into the search box, but the click-driven UX is gone.
EMAILS.length === 59684.+ button.Test Binder, OK.0.class="item active", state.filters.binder === "Test Binder", and the main table redraws to 0 rows (since the binder is empty).state.filters.binder stays undefined. No active class. No re-render. No JS error in console.log — but DevTools "Issues" panel shows Failed to execute 'click' on 'HTMLElement': Unexpected end of input.ND0002 (45,650).state.filters.archive === "ND0002".scripts/build_review_site_v2.py:761 — the renderItem template:
return `<div class="${cls}" onclick="${onclick}">
The onclick attribute is wrapped in double quotes. The values passed in for the broken filter groups come from these two callsites:
scripts/build_review_site_v2.py:773 (block() helper used by Archive / Year / Tag / Category / Doc Type / Custodian):
h += renderItem(... , `state.filters.${group}=${JSON.stringify(v)}; refresh()`);
scripts/build_review_site_v2.py:792 (binders):
html += renderItem(name, count, active, `state.filters.binder=${JSON.stringify(name)}; refresh()`, actions);
JSON.stringify("Test Binder") returns the literal string "Test Binder" (including the double quotes). The template substitution then produces this onclick attribute value:
state.filters.binder="Test Binder"; refresh()
When the browser parses this inside onclick="...", the first " in "Test Binder" ends the attribute. Everything after — Test Binder";, refresh(), the closing " — is parsed as garbage HTML attributes (test="", binder="";, refresh()="", ="", etc.). The actual onclick ends up as state.filters.binder= — an incomplete JS expression — and when the user clicks, the browser throws Unexpected end of input.
You can see this directly in the DOM. From Playwright's outerHTML:
<div class="item" onclick="state.filters.binder=" test="" binder";="" refresh()"="">
vs. the working filters (Hot, Has Attachments, From Domain, Decision) which hand-build single-quoted JS strings at line 803, 808, 819, 829, e.g.:
"state.filters.hasAttach='yes'; refresh()"
which renders as:
<div class="item" onclick="state.filters.hasAttach='yes'; refresh()">
and works correctly.
Two equally-valid approaches; I'd take Option A because it's a one-line change.
Option A — escape the double quotes (1-line fix in 2 places):
In scripts/build_review_site_v2.py, replace JSON.stringify(...) with a helper that produces a string safe to embed inside onclick="...". The simplest safe form is single-quoted JS using JSON.stringify(...).replace(/"/g, """) so the HTML attribute parser sees " (and the JS parser then sees "):
// add once, near escapeHtml (line 687):
function jsAttr(v) { return JSON.stringify(v).replace(/"/g, """); }
// line 773:
h += renderItem(displayName ? displayName(v) : v, n, f[group] === v, `state.filters.${group}=${jsAttr(v)}; refresh()`);
// line 792:
html += renderItem(name, count, active, `state.filters.binder=${jsAttr(name)}; refresh()`, actions);
The " entity is HTML-decoded back to " before the onclick string is handed to the JS parser, so the browser still sees a valid string literal. This matches how escapeHtml already encodes " → " for ordinary text.
Option B — switch from inline onclick to delegated handler (more refactoring, but eliminates the entire class of bug):
In renderItem() at line 761, render <div class="${cls}" data-onclick='${onclick}'> instead, and add one document-level click listener that does eval(el.dataset.onclick) on .item clicks. Single quotes around data-onclick plus escapeHtml of the onclick body removes the quote-balancing problem entirely, and a follow-up refactor could replace eval with a small action-dispatch table.
After the fix, re-run python3 /tmp/qa_review_site.py against the redeployed site and check that:
after_archive < baseline and after_year < baseline.state.filters.binder === "Test Binder" after the click.0 pageerrors.Affects: browser console / DevTools "Issues" panel.
Severity: P1 (no user-visible breakage beyond Bug #1, but it's noise that masks future real errors).
Root cause: Same as Bug #1. When the user clicks one of the broken inline-onclick rows, the partial JS string (state.filters.binder=) fails to parse and the browser raises a page error. Fixing Bug #1 fixes this.
When I switched to Documents and clicked the first row, I saw 0 iframes and 0 images. I'm flagging this as PARTIAL, not FAIL, because:
- Many docs are PDFs hosted out-of-band; the deployed build may simply not be wiring native files for those rows yet.
- The viewer markup may be rendered only on user gesture (lazy).
- I tested only one row; the user should manually click a known-PDF row and a known-image row to confirm.
Worth a 5-minute manual smoke check once Bug #1 is fixed.
The following tests passed and are good baselines for any future change:
/, j, k, p/tmp/qa_review_site.py/tmp/qa_results.json/Users/awesomefat/Dropbox/LitmanDev/RichieResearch Claude Code/output/qa_screenshots/05_sidebar_broken.png — sidebar with archive click failing06_binders_summary.png, 06b_click_no_state_change.png — binder click doing nothing09_detail.png, 10_viewer.png — non-fatal, retained for reference/Users/awesomefat/Dropbox/LitmanDev/RichieResearch Claude Code/scripts/build_review_site_v2.pyjsAttr helper near 687)