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:
co-authored by
Claude Fable 5
parent
f574fb2b3c
commit
6da499d31a
@@ -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 -------------------------------------------------------
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user