Skip to content

Fix Library and History QA issues (T7: LB-01, LB-03–07, HI-01, MD-05, VS-07) - #203

Merged
Programcode merged 14 commits into
mainfrom
fix/t7-library-history
Oct 22, 2026
Merged

Programcode merged 14 commits into
mainfrom
fix/t7-library-history

Conversation

@Programcode

@Programcode Programcode commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Batch T7 from the 22 Oct 2026 QA report on prod: nine Library and History issues. Nothing here needs a migration, an Edge Function deploy or a production change. Merging ships only the frontend.

Issues

LB-01 (High): the Library switch reverted silently offline

  • Offline, the switch no longer moves. A toast says "Change not saved. You are offline. Reconnect and try again."
  • Online, the switch moves at once. If the write fails, it goes back to the last value the server accepted and a toast says why.
  • Every write that changes whether a source is on (the switch, a folder's Activate/Deactivate, the editor's Save) joins one per-source queue. The server applies them in click order, the newest decides what shows, and a server reload that started earlier can't undo them.
  • A failed folder switch or editor save now shows a toast, and a failed save keeps the editor's text.
  • The source editor sends only the fields edited in the sheet. Fields not edited there follow the stored source while the sheet is open, so a switch that rolls back shows as rolled back, and text typed during a save is kept for the next Save.
  • Verified: store tests with mocked rejected and refused updates, overlapping failures, and reloads landing before, during and after the write. Recipes lib-offline (switch and editor save offline, then a retry online) and lib-edit-while-switching on desktop and mobile Chromium.
  • The "Offline mode" pill: not reproduced. With Playwright offline it stayed visible for all 15 one-second samples. Worth one look in real Chrome DevTools.

LB-03 (Medium): research saved the same page again and again

  • src/lib/source-url.ts holds the URL normalizer: host lower-cased without www., no scheme, fragment, tracking parameters (utm_*, fbclid, gclid and similar) or trailing slash.
  • addSource refuses a page the debate already has (DuplicateSourceError). It checks the in-memory Library first, then the debate's rows on the server, so another tab or a list that failed to load can't add a second copy. Research auto-save refreshes the existing row's metadata instead of adding one. Undo is exempt.
  • No DB guard. Prod already has duplicates (the QA accounts show 17 rows with repeats), so a unique index would fail to build. App-side dedupe only.
  • Verified: store and auto-save unit tests, plus recipe lib-dedupe.

LB-04 (Medium): titles shrank to a few letters on phones

  • Below sm, the row's controls (open link, delete, switch) take their own line, so the title gets the row's width and two lines. The date/URL line wraps to two lines, folder chips wrap, and the open-in-new link is a 44px target on touch. The list card has less side padding on phones, and the search placeholder is shorter. Only LibraryPage.tsx changed; no shared ui/ primitives.
  • Verified: recipe lib-layout at 360px with a long Spanish title. The title is wider than 200px and the page doesn't scroll sideways.

HI-01 (Medium): Crossfire sessions couldn't be found again

  • History has a Crossfire tab. Each row shows the resolution, turn count and date. Open shows the debrief and the transcript; the debrief's turn references jump to the turns. Delete asks first and stays open while it runs.
  • Rows are parsed with the Crossfire zod schemas. A row in an older shape says so instead of rendering broken data.
  • CrossfirePage.tsx is untouched because T5 is changing it. The read-only view in src/components/features/crossfire/SavedCrossfireView.tsx repeats the debrief markup; once T5 lands, CrossfirePage can reuse it.
  • Strings in en, es and hu.
  • Verified: recipe cross-history (finish a round, leave, reopen from History, delete) on desktop and mobile Chromium.

LB-05 (Low): a duplicate URL import was caught only after the fetch

  • The import checks the normalized URL against the debate's Library before calling import-source.
  • Verified: recipe lib-dedupe asserts no import-source request is made.

LB-06 (Low): deleting a source has no confirmation

  • No multi-select or bulk delete exists. "Clear All" and folder delete already confirm. Single delete keeps its immediate delete with Undo, unchanged.

MD-05 (Low): Attack added sources silently, and counts disagreed

  • Research says "Added N sources to your Library", counting only new rows, not refreshed ones.
  • /app, the Current Debate page and the header menu show "N sources (M active)" from one hook (useDebateCounts). M counts the same switched-on sources Ask's "Using M sources" counts.
  • Verified: a unit test for the toast count, and recipe lib-counts (7 seeded sources, 5 on: /app shows "7 sources (5 active)", Ask shows "Using 5 sources") on desktop and mobile. Fake mode has no Exa, so the toast after a real Attack run is not verified end to end; that needs a live run.
  • I couldn't reproduce the original 0 vs 5 by reading the code. Both pages already read the same list. The fix makes the numbers comparable; it doesn't explain the original report.

VS-07 (Low): History's tab bar scrolled sideways with ~30px tabs

  • Tabs wrap onto more lines on phones, each at least 44px tall. Done with classes on HistoryPage only.

LB-07 (Nit): no feedback after saving a source

  • A new source shows at once with a spinner and disabled controls, then becomes the saved row. A toast says "Saved <title> to the Library". Before the first server load, the Library shows "Loading sources…" instead of "0 sources".
  • Verified: store test "shows the row while it saves, then swaps in the saved row". The lib-offline recipe checks the "Saved <title> to the Library" toast.

Also in this PR

  • vite.config.ts: the dev server ignores .verify/ and .ci-failures/. Playwright writes trace pages under .verify/ during recipes, and Vite reloaded the app mid-test ("Unable to load DebateLab"). This caused three mobile recipe failures before the change and none after.
  • verify-debatelab: new sub-features lib-offline, lib-edit-while-switching, lib-dedupe, lib-layout, lib-counts and cross-history, with recipes and feature docs. docs/architecture.md describes the shared dedupe and the switch write queue.

Not fixed here: two recipe failures that come from main

lib-import (#195)
lib-import fails on both projects since main's #195 (batch C2, LB-02). The new stripHtml returns 0 characters for https://en.wikipedia.org/wiki/Free_public_transport; the old one returned 92,543. The cause is in supabase/functions/import-source/html-text.ts: WIKI_CHROME treats - as a word boundary, and Wikipedia's <html> element has the class vector-feature-toc-pinned-clientpref-1, which matches toc. shouldStrip applies that check to every tag, <html> included, so the whole document is stripped. Out of scope for T7; C2 owns it.

lib-add (on main since before #196; #196 uncovered it)

The failing read is the recipe's response.text() on the streamed ai response. The trace shows that request ending in net::ERR_ABORTED. There is no navigation, no reload and no service worker; the page aborted it.

  • src/lib/ai/live.ts stops reading at the result SSE event and calls reader.cancel() in finally.
  • supabase/functions/ai/stream.ts writes result and closes the stream right after. The client's cancel races that close; when the cancel wins, Chrome drops the body.
  • Before Delay Jam and align cache, CSP, and robots #196, Jam loaded with the page and its fetch wrapper read the whole response, which hid this. Delay Jam and align cache, CSP, and robots #196 loads Jam on the first click, and Keep Jam out of the full-stack e2e harness #198 stubs it in the harness, so the race shows. On this stack: pure main b79f858 fails 2/2, c5f3ad1 passes 2/2, and c5f3ad1 with Jam stubbed fails 2/2.
  • Smallest fix: in live.ts, after result, keep reading to the end of the stream (bounded by the abort signal and a ~2 s cap) and cancel only if it hasn't ended. With that patch applied temporarily, lib-add passes 2/2. For users, it also stops every live request from ending as aborted and from firing the Edge Function's cancel() after the answer. Not changed in this PR.

Review rounds

r1 and r2 ran on Codex. From r3 on, Codex returned 429 (plan limit), so each round ran as a fresh-context Claude Opus review with the same scope and the repo's CODING_STANDARDS.md.

  • r1 on 9ea2a51: four medium race findings and one low. All fixed in f2a1ad6 with tests that fail on the old code:
    1. The saving row was matched by object identity, so a folder switch left a duplicate.
    2. Overlapping failed toggles restored an unconfirmed value.
    3. A server reload overwrote saved toggles.
    4. Undo rows could be deleted mid-restore.
    5. (Low) The Crossfire delete dialog closed before the delete finished.
  • r2 on 6e1b7ad: two medium findings and one low. All fixed in f57bc80, with tests for the two races:
    1. A research metadata refresh could save a switch value that was still being written.
    2. A folder activation could be overwritten by an older toggle after a reload.
    3. (Low) The Open buttons didn't name their round.
  • r3 on 4a5caea: Codex returned 429, so a fresh-context Claude Opus review ran with the same scope instead. It found three medium findings and four low ones. All are fixed in c62d1f3, with tests that fail on 4a5caea:
    1. A switch write that started before a reload and landed during it was overwritten.
    2. A folder's Activate/Deactivate also wrote rows that were still saving.
    3. The lib-add recipe left the page before the optimistic pause had reached the server.
    4. (Low) A replaced toggle failure still showed "It is back to how it was".
    5. (Low) The identity test didn't reach the copy it was named for.
    6. (Low) docs/architecture.md was stale.
    7. (Low) The debate counts included rows still saving.
      The same review agreed that the lib-add network-body failure comes from main and that nothing in this branch touches that path.
  • r4 on c62d1f3 (Claude): confirmed the r3 fixes and found one low. A toggle failure replaced by a folder write or an editor save went unreported, because those handlers never caught errors. Fixed in 49ad557: both handlers now show a toast.
  • r5 on 49ad557 (Claude): confirmed the r4 fix and found one medium that was already on main but that the new toast makes visible. A failed editor save reset the sheet to the old values, so the student lost their edits. Fixed in 74334e7: the draft loads once per opened source. lib-offline now edits offline and checks the typed text stays; it fails without the fix ("Fares and ridership.").
  • r6 on 74334e7 (Claude): one low. The editor's Save sent every field, so a value loaded when the sheet opened could overwrite a change that arrived while it was open. A first fix in fc5cd22 compared against the loaded values. r7 showed that snapshot could go stale, so dc7c626 replaced it: the sheet now tracks which fields the student edited, sends only those, and lets the rest follow the stored source.
  • r7 on fc5cd22 (Claude): three lows, all fixed in dc7c626, including the stale snapshot above and a recipe gap. The new lib-edit-while-switching recipe fails a switch write while the editor is open.
  • r8 on dc7c626 (Claude): one medium. Text typed while a save ran lost its edit mark, as did text typed after reopening the same source. Fixed in 70e5dd7 with an edit count per field and a counter per sheet opening. The recipe types during a slow save and checks that the next Save sends the new text; it fails on dc7c626.
  • r9 on 70e5dd7 (Claude): no findings. Checked the per-field edit counts and the opening counter (close and reopen), the merge of untouched fields, and that the recipe is sound.

Verification

Local only; GitHub Actions minutes are used up, so there's no CI run. Local passes at the final head:

npm run verify at 70e5dd7 (the full ladder) exits 0:

  • docs:check clean (check-site: 48 HTML files, no problems)
  • lint and typecheck clean
  • unit: 66 files, 461 tests passed
  • collaboration: 30 passed
  • edge: 406 passed, 0 failed
  • build OK
  • test:sw all checks true
  • e2e smoke (desktop Chromium): 43 passed

verify-debatelab recipes (fake provider, local Supabase) at 70e5dd7:

  • practice (includes cross-history): 5/5 on desktop-chromium and 5/5 on mobile-chromium.
  • library: lib-offline, lib-edit-while-switching, lib-dedupe/lib-layout and lib-counts pass on both projects. lib-add and lib-import fail on both, for the two main issues described under "Not fixed here". Neither comes from this branch: both fail the same way on main's own app code.
  • Evidence (screenshots, ARIA snapshots, rows) is in .verify/artifacts/ on the VPS worktree; not attached.

Not verified: real Chrome DevTools offline (the pill), a live Attack run's toast, staging and production.

Proposed cleanup for existing duplicates (not run)

Prod still has the duplicate rows from before this fix. This is a proposal for Adam to review; deleting them is a mass delete and needs a separate OK. The preview only reads. The delete keeps one row per user, debate and page: the switched-on one if any, otherwise the oldest. Its normalization approximates the app's (host lower-cased without www., path without trailing slash, utm_*/fbclid/gclid dropped). Run the preview first and spot-check the groups. I checked the preview on a local database with seeded rows inside a rolled-back transaction: it grouped https://www.Motherly.com/homework/?utm_source=x#top with a row whose metadata URL is http://motherly.com/homework, kept the switched-on copy, and left ?id=2 alone.

-- Preview: duplicate groups per user, debate and normalized URL.
with keyed as (
  select s.id, s.user_id, s.debate_id, s.title, s.is_active, s.created_at,
    lower(regexp_replace(substring(u from '^https?://([^/?#]+)'), '^www\.', '', 'i'))
      || regexp_replace(split_part(substring(u from '^https?://[^/?#]+([^#]*)'), '?', 1), '/+$', '')
      || coalesce('?' || nullif(regexp_replace(regexp_replace(split_part(substring(u from '^https?://[^/?#]+([^#]*)'), '?', 2),
           '(^|&)(utm_[^=&]*|fbclid|gclid)=[^&]*', '', 'gi'), '^&+', ''), ''), '') as url_key
  from public.sources s
  cross join lateral (select coalesce(s.metadata->>'url', s.origin) as u) src
  where s.debate_id is not null and u ~* '^https?://'
),
ranked as (
  select *, row_number() over (partition by user_id, debate_id, url_key order by is_active desc, created_at asc) as rn,
         count(*) over (partition by user_id, debate_id, url_key) as copies
  from keyed
)
select user_id, debate_id, url_key, copies, id, title, is_active, created_at, rn
from ranked where copies > 1 order by user_id, debate_id, url_key, rn;

-- Delete (proposal only, do not run without Adam's OK): keep rn = 1.
-- Put the same `keyed` and `ranked` CTEs in front of the delete.
-- begin;
-- delete from public.sources where id in (select id from ranked where rn > 1);
-- commit;

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Crossfire rounds now appear in History, where you can reopen the debrief and transcript or delete a round.
    • Debate summaries show both total and active source counts.
  • Bug Fixes

    • The Library shows sources while they save, prevents duplicate web pages within a debate, and reports save or toggle failures.
    • Source edits and activation changes are more reliable during concurrent saves or connection issues.
    • Library controls and source rows are easier to use on narrow screens.

Programcode and others added 13 commits October 22, 2026 21:01
LB-01: the source switch moves at once and goes back with a toast when
the write fails; offline, it does not move and says the change was not
saved.
LB-03/LB-05: one row per normalized URL per debate, checked in memory and
against the server before insert; URL import warns before fetching.
LB-04: on phones the row controls get their own line, the meta line and
folder chips wrap, and the open-in-new link is a 44px target.
LB-07: a new source shows at once while it saves, with a toast after, and
the Library shows a loading line instead of 0 sources before first load.
HI-01/VS-07: History gets a Crossfire tab with open (transcript and
debrief) and delete; tabs wrap with 44px targets on phones.
MD-05: research says how many sources it added, and the debate summary
shows the active count Ask and the other tools use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… layout in verify recipes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A row still saving is found again by its id, so a folder switch that
  copies it no longer leaves a second copy behind.
- Switch writes for one source run in click order; only the newest decides
  what shows, and a failure falls back to the last value the server took.
- A server reload that read before a switch write keeps the new value.
- An Undo row is pending until its insert lands, so it cannot be deleted
  or changed mid-restore.
- The Crossfire delete dialog stays open while the delete runs and shows
  a failure in place.
- Offline toast wording no longer repeats itself.
- Vite ignores .verify/ and .ci-failures/, where Playwright's trace pages
  were reloading the app in the middle of recipes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ounts (MD-05)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The switch, a folder's Activate/Deactivate and the editor's Save all
  join each source's write queue, and a server reload keeps the newest
  of them, so a folder activation is no longer overwritten by an older
  toggle.
- updateSource writes and undoes only the fields an edit changes, so a
  research metadata refresh cannot save a switch value that is still
  being written or about to fail.
- Each Crossfire Open button names its round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A reload keeps the value of any switch write already running when it
  started, not only writes that began after it.
- A folder's Activate/Deactivate updates only the rows the Library shows,
  so a row still saving cannot take the folder's value on the server alone.
- A failed switch write that a newer click already replaced is not
  reported; the newer write's outcome is what shows.
- Debate counts leave out rows still saving, matching what tools send.
- The identity test now copies a saving row through a folder rename.
- docs/architecture.md describes the shared dedupe and the write queue.
- lib-add waits for the pause to reach the server before leaving the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folder Activate/Deactivate and the source editor's Save did not catch a
failed write, so a refused change rolled back with no message. Both now
show a toast, offline-aware like the switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The draft reloaded whenever the source object changed, so a rolled-back
save or a background refresh replaced what the student had typed. It now
loads once per opened source. lib-offline covers a failed offline save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The editor sent every field on Save, so values loaded when the sheet
opened could overwrite a switch or research change that landed while it
was open. It now sends only what differs from what it loaded, and Save
is disabled while a save runs. lib-offline retries the kept text online.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Save sent the fields that differed from what the sheet loaded, which went
stale when the stored source rolled back and could be overwritten by a
save that finished after the sheet moved to another source. The sheet now
records which fields the student edited: those keep what was typed and
are all Save sends; the rest follow the stored source while it is open.
A finished save clears its marks only if the same source is still open.
New recipe lib-edit-while-switching covers a switch that fails while the
editor is open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A finished save cleared the edit mark of every field it sent, even one
the student changed again while it ran, so that text was never sent and
could be replaced by the stored copy. Each field now carries an edit count
and each sheet opening a number; a save clears a mark only when neither
changed. lib-edit-while-switching types on during a slow save and checks
the next Save sends it, and first checks the editor opened mid-write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Warning

Review limit reached

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Next included review available in 37 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 70e5dd7 and 6745bda.

📒 Files selected for processing: 3
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between b79f858 and 70e5dd7.

📒 Files selected for processing: 21
  • .claude/skills/verify-debatelab/features/library-sources.md
  • .claude/skills/verify-debatelab/features/practice.md
  • .claude/skills/verify-debatelab/recipes/library.spec.ts
  • .claude/skills/verify-debatelab/recipes/practice.spec.ts
  • docs/architecture.md
  • src/components/features/crossfire/SavedCrossfireView.tsx
  • src/components/features/debate/CurrentDebateMenu.tsx
  • src/components/features/debate/CurrentDebateStrip.tsx
  • src/lib/ai/sources.test.ts
  • src/lib/ai/sources.ts
  • src/lib/source-url.ts
  • src/lib/stores/session-store.ts
  • src/locales/en.json
  • src/locales/es.json
  • src/locales/hu.json
  • src/pages/CurrentDebatePage.tsx
  • src/pages/HistoryPage.tsx
  • src/pages/HomePage.tsx
  • src/pages/LibraryPage.tsx
  • src/test/stores/library-sources.test.ts
  • vite.config.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The PR changes source-library saving, URL deduplication, activation, and source counts. It adds Crossfire history viewing and deletion, with localized labels and tests. The Vite watcher now ignores changes under .verify and .ci-failures.

Changes: Source Library

Layer / File(s) Summary
Source deduplication and saving
src/lib/source-url.ts, src/lib/stores/session-store.ts, src/lib/ai/sources.ts, src/pages/LibraryPage.tsx, src/test/stores/library-sources.test.ts, src/lib/ai/sources.test.ts, docs/architecture.md, .claude/skills/verify-debatelab/features/library-sources.md, src/locales/*
Shared URL helpers compare saved pages by debate. Adds show a pending row while saving, then reconcile it with the saved row. Research autosave reports successful additions and handles duplicate saves.
Source edits and activation writes
src/lib/stores/session-store.ts, src/pages/LibraryPage.tsx, src/test/stores/library-sources.test.ts, .claude/skills/verify-debatelab/recipes/library.spec.ts, .claude/skills/verify-debatelab/features/library-sources.md, docs/architecture.md
Source edits save changed fields. Activation writes are serialized, roll back on failure, and preserve newer values during hydration. Library controls handle offline and failed writes.
Source counts and Library display
src/components/features/debate/*, src/pages/CurrentDebatePage.tsx, src/pages/HomePage.tsx, src/pages/LibraryPage.tsx, src/locales/*, .claude/skills/verify-debatelab/recipes/library.spec.ts, .claude/skills/verify-debatelab/features/library-sources.md
Debate counts exclude pending sources and show total and active counts. The Library adds loading states and narrow-screen layout changes.

Crossfire History

Layer / File(s) Summary
Saved-round parsing and display
src/components/features/crossfire/SavedCrossfireView.tsx
The view validates saved rounds and displays debrief findings, cited transcript turns, and answer signals.
History access and deletion
src/pages/HistoryPage.tsx, src/lib/stores/session-store.ts, src/locales/*, .claude/skills/verify-debatelab/recipes/practice.spec.ts, .claude/skills/verify-debatelab/features/practice.md
History lists Crossfire rounds within the selected debate scope. Users can open a round or delete it after confirmation. Deletion removes it from saved results.

Vite Watch Configuration

Layer / File(s) Summary
Watcher exclusions
vite.config.ts
The Vite server watcher ignores .verify and .ci-failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HistoryPage
  participant SessionStore
  participant SavedCrossfireView
  HistoryPage->>SavedCrossfireView: Pass saved round output for display
  HistoryPage->>SessionStore: Delete confirmed recorded result
  SessionStore-->>HistoryPage: Return after successful deletion
  HistoryPage->>HistoryPage: Close the deleted round and show success
Loading
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants