Review UI: surface the unresolved backlog and a full catalog ledger

The done screen claimed "diff-ready" while 104 extracted titles had
never been resolved (they're invisible to matches.csv until the BGG
token arrives). The state now counts titles.json entries with no
matches row: the header tally shows "awaiting resolve", the done screen
says "Resolved set fully reviewed" with the real extracted total and
what to run on token day, and a read-only Catalog section lists every
extracted title with its status chip (auto/approved/rejected/awaiting
BGG), matched game, version, and source photos.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-01 16:45:57 -04:00
co-authored by Claude Fable 5
parent f574fb2b3c
commit 6da499d31a
6 changed files with 147 additions and 7 deletions
+3 -2
View File
@@ -60,9 +60,10 @@ class ReviewSession:
self.rows = read_matches(cfg.matches_path)
self.decisions = 0
try:
self._titles = {e.title_raw: e for e in load_titles(cfg.titles_path)}
self.titles = load_titles(cfg.titles_path)
except FileNotFoundError:
self._titles = {}
self.titles = []
self._titles = {e.title_raw: e for e in self.titles}
# -- plumbing -------------------------------------------------------
+46 -3
View File
@@ -166,6 +166,23 @@
background: var(--felt); color: var(--paper);
padding: .3rem .8rem; border-radius: 6px;
}
.done .waiting { color: var(--brass-deep); font-weight: 600; }
/* catalog: read-only status ledger, quieter than decision cards */
.catalog { background: var(--paper); border-radius: 8px; padding: .4rem 1rem; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
.catalog table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.catalog td { padding: .3rem .5rem; border-top: 1px solid var(--paper-edge); vertical-align: top; }
.catalog tr:first-child td { border-top: none; }
.catalog .t { font-weight: 600; }
.catalog .meta { color: var(--ink-soft); }
.chip {
font-size: .68rem; text-transform: uppercase; letter-spacing: .06em;
border-radius: 999px; padding: .1rem .55rem; white-space: nowrap;
}
.chip.ok { background: #dfe9df; color: var(--approve); }
.chip.wait { background: #f3e7cd; color: var(--brass-deep); }
.chip.no { background: #f0ddd8; color: var(--reject); }
.chip.open { background: #e4e4ef; color: #4c4c78; }
@media (max-width: 700px) {
.card, .ticket { flex-direction: column; }
.shots { flex-basis: auto; }
@@ -311,20 +328,29 @@ function render() {
`<span><b>${s.pending.length}</b> matches</span>
<span><b>${s.versions.length}</b> editions</span>
<span><b>${s.unidentified.length}</b> reshoot</span>
${s.summary.unresolved ? `<span><b>${s.summary.unresolved}</b> awaiting resolve</span>` : ""}
<span>${s.decisions} decided this sitting</span>`;
let html = "";
if (!s.pending.length && !s.versions.length) {
const waiting = s.summary.unresolved;
html += `
<div class="done">
<h2>All reviewed — this catalog is diff-ready</h2>
<h2>${waiting
? "Resolved set fully reviewed"
: "All reviewed — this catalog is diff-ready"}</h2>
<div class="nums">
<div>${s.summary.extracted}<span>extracted</span></div>
<div>${s.summary.recognized}<span>recognized</span></div>
<div>${s.summary.version_updates}<span>with versions</span></div>
<div>${s.summary.rejected}<span>rejected</span></div>
</div>
<p>Next: <code>uv run bggpipe diff</code></p>
${s.unidentified.length ? `<p class="status">${s.unidentified.length} reshoot ticket(s) below — they don't block the diff.</p>` : ""}
${waiting
? `<p class="waiting">${waiting} title(s) extracted but not yet matched to BGG —
they're waiting on the API token. When it arrives:
<code>uv run bggpipe resolve</code>, then review the new arrivals here.</p>`
: `<p>Next: <code>uv run bggpipe diff</code></p>`}
${s.unidentified.length ? `<p class="status">${s.unidentified.length} reshoot ticket(s) below — they don't block anything.</p>` : ""}
</div>`;
}
if (s.pending.length) {
@@ -339,6 +365,23 @@ function render() {
html += `<h2>Reshoot <span class="count">— boxes seen but not identified</span></h2>`;
html += s.unidentified.map(ticket).join("");
}
if (s.catalog.length) {
const chip = c => {
if (c.status === "awaiting_resolve") return `<span class="chip wait">awaiting BGG</span>`;
if (c.status === "auto" || c.status === "approved") return `<span class="chip ok">${c.status}</span>`;
if (c.status === "rejected") return `<span class="chip no">rejected</span>`;
return `<span class="chip open">${esc(c.status)}</span>`;
};
html += `<h2>Catalog <span class="count">— every title extracted so far (${s.catalog.length})</span></h2>
<div class="catalog"><table>` + s.catalog.map(c => `
<tr>
<td class="t">${esc(c.title_raw)}</td>
<td>${chip(c)}</td>
<td class="meta">${c.bgg_name ? esc(c.bgg_name) + (c.bgg_id ? " · " + esc(c.bgg_id) : "") : ""}
${c.version_name ? " · " + esc(c.version_name) : ""}</td>
<td class="meta">${esc(c.photos.join(", "))}</td>
</tr>`).join("") + `</table></div>`;
}
m.innerHTML = html;
const cards = actionables();
+24
View File
@@ -152,6 +152,27 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI:
counts: dict[str, int] = {}
for row in session.rows:
counts[row["match_status"]] = counts.get(row["match_status"], 0) + 1
resolved_titles = {r["title_raw"] for r in session.rows}
rows_by_title: dict[str, dict] = {}
for r in session.rows:
rows_by_title.setdefault(r["title_raw"], r)
catalog = []
for entry in session.titles:
row = rows_by_title.get(entry.title_raw)
catalog.append(
{
"title_raw": entry.title_raw,
"confidence": entry.confidence,
"photos": list(entry.source_photos),
"status": row["match_status"] if row else "awaiting_resolve",
"bgg_id": row["bgg_id"] if row else "",
"bgg_name": row["bgg_name"] if row else "",
"version_name": row["version_name"] if row else "",
}
)
unresolved_count = sum(
1 for e in session.titles if e.title_raw not in resolved_titles
)
version_updates = sum(
1
for r in session.rows
@@ -172,6 +193,7 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI:
"pending": [row_payload(r) for r in session.pending_rows()],
"versions": [version_payload(r) for r in session.version_rows()],
"unidentified": sightings,
"catalog": catalog,
"decisions": session.decisions,
"summary": {
"recognized": counts.get("auto", 0) + counts.get("approved", 0),
@@ -180,6 +202,8 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI:
"rejected": counts.get("rejected", 0),
"version_updates": version_updates,
"total": len(session.rows),
"extracted": len(session.titles),
"unresolved": unresolved_count,
},
}