Smart review: name the source deck on every card, break the counts down

"It contains mistakes I either did more than once or from decks that I've
removed - there's no way there's 34 to repeat with 2 decks." The counts were
honest (dueItems/mistakeItems only resolve live deck+activity pairs; a seeded
repro with 37 SRS rows and 30 live shows "30 due"), but nothing in the UI said
where an item came from: a deck created twice from the same notes shows every
question twice, and a deck that survived a delete or a sync looks like a ghost.
Both read as a broken list.

- SessionFeed gains showDeck (review + mistakes): a deck chip above every
  card, so two identical questions are visibly two decks.
- The Smart review card breaks its numbers down under the buttons:
  "Due by deck: Sky deck - 17 - Sky deck (2) - 17", capped at three decks
  plus "+n more" (groupByDeck in lib/learning).
- New i18n keys (en + cs, plural forms for "+n more").

review-test pins it: a twin-deck seed renders the breakdown and both deck
chips in the feed. Version 0.6.8, SW v90.
This commit is contained in:
Martin Dvořák 2026-10-15 19:27:04 +00:00
commit 1e8606a95b
9 changed files with 129 additions and 8 deletions

View file

@ -167,6 +167,47 @@ async function makeReviewPage(locale = "en") {
await context.close();
}
/* 4b. the review names its source decks ------------------------------ */
{
/* "It contains mistakes I either did more than once or from decks that I've
removed" — the count was honest (only live deck+activity pairs), but the
session never said WHERE an item came from, so a deck created twice from
the same notes read as one question repeating itself. Every card now
names its deck, and the Smart review card breaks the count down. */
const { context, page } = await makePage(browser);
const twin = { ...DECK, id: "seed_twin", title: "Twin deck" };
await seed(page, { decks: [DECK, twin] });
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(800);
await seedLearning(page, {
srs: [dueSrs("seed_deck", "a1"), dueSrs("seed_deck", "a2"), dueSrs("seed_twin", "a1"), dueSrs("seed_twin", "a2")],
});
await page.reload({ waitUntil: "networkidle" });
await page.waitForTimeout(900);
const breakdown = await page.evaluate(
() => document.body.innerText.split("\n").find((line) => line.includes("Due by deck")) ?? "",
);
check(
"review: the card breaks the count down by deck",
breakdown === "Due by deck: Sky deck · 2 · Twin deck · 2",
breakdown,
);
await page.getByText(/Review due · 4 due/).click();
await page.waitForTimeout(1500);
const chips = await page.evaluate(() => {
const feed = document.querySelector("[data-testid='review-feed']");
return [...(feed?.querySelectorAll(".glass-chip") ?? [])].map((el) => el.textContent?.trim());
});
check(
"review: every card names its deck",
chips.includes("Sky deck") && chips.includes("Twin deck"),
JSON.stringify(chips.slice(0, 8)),
);
check("review: the twin's question is distinguishable", (await page.getByText("Twin deck").count()) >= 1);
await context.close();
}
/* 5. health probes are deduplicated ---------------------------------- */
{
const { context, page } = await makePage(browser);