← litmanintelligence.com  |  ← Counsel PDFs index

Review Site Qa 2026-04-28

Review Site QA — 2026-04-28

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/

Executive summary

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.

Results table

# 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 nothingstate.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 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

Bug #1 — 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.

Reproduction (binder, exactly as user described)

  1. Open https://litmanintelligence.com/review/ — wait for EMAILS.length === 59684.
  2. In the left sidebar under "My Binders", click the + button.
  3. Prompt appears, type Test Binder, OK.
  4. The new binder appears in the sidebar with badge 0.
  5. Click the row "Test Binder".
  6. Expected: the row gains class="item active", state.filters.binder === "Test Binder", and the main table redraws to 0 rows (since the binder is empty).
  7. Actual: nothing happens. 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.

Reproduction (sidebar Archive, same bug, different surface)

  1. Same load.
  2. Sidebar → Archive → click ND0002 (45,650).
  3. Expected: count in toolbar drops to 45,650 and state.filters.archive === "ND0002".
  4. Actual: count stays at 59,684. State unchanged.

Root cause

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, "&quot;") so the HTML attribute parser sees &quot; (and the JS parser then sees "):

// add once, near escapeHtml (line 687):
function jsAttr(v) { return JSON.stringify(v).replace(/"/g, "&quot;"); }

// 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 &quot; 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 "&quot; 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.

Verification

After the fix, re-run python3 /tmp/qa_review_site.py against the redeployed site and check that:


Bug #2 — Page-level "Unexpected end of input" errors

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.


PARTIAL: Inline document viewer (Test #10)

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.


Things that work (do not regress)

The following tests passed and are good baselines for any future change:


Files & artifacts