Add a structured authenticity signal with a distinct banner, instead of burying it in card_note

The Charizard grade that prompted this had the model writing 'possible
non-standard/proxy print' directly into card_note -- the only free-text
field available -- since there was nowhere else for that observation to go.
That's why it got no visual treatment: the UI renders card_note as a plain
grey subtitle, identical to any ordinary card name.

Added authenticity as its own top-level field (flag: none/worth_checking/
likely_not_genuine, plus an observation) alongside the four grading
categories but explicitly separate from them -- this is about whether the
card is a genuine product at all, not its condition, and PSA authenticates
before it grades, so a flagged card's estimated_grade goes to null rather
than a number.

Prompted with concrete tells for both classes of concern: fan-made/proxy
prints (fictional sets, non-existent number combos, home-printer texture,
'proxy' watermarks) and counterfeits of real cards (colour/font/holo
mismatches against genuine copies). Trimming (already covered under CORNERS
from an earlier fix) now also sets this field when flagged, so the UI has
one place to check regardless of which specific issue triggered it.

UI: a full banner (not a hint line) above the slab -- critical red for
likely_not_genuine, warning amber for worth_checking -- plus a matching dot
next to the card name in History so it's visible without opening the card.
Both colours reuse --critical/--warning rather than the brand red, since
this is exactly the 'distrust this' signal those are already reserved for.

Migration tested against the live schema shape; existing rows get
authenticity=None and render with no banner, as before.
This commit is contained in:
Barely Removable 2026-08-23 08:19:58 -07:00
parent d026cce91a
commit 2b3bbaf40f
5 changed files with 148 additions and 12 deletions

View file

@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS grades (
grade_high INTEGER,
confidence TEXT,
categories_json TEXT,
authenticity_json TEXT,
edge_measurements_json TEXT,
centering_measurement_json TEXT,
aspect_measurement_json TEXT,
@ -126,6 +127,7 @@ def init():
for statement in (
"ALTER TABLE grades ADD COLUMN aspect_measurement_json TEXT",
"ALTER TABLE grades ADD COLUMN source_images_json TEXT",
"ALTER TABLE grades ADD COLUMN authenticity_json TEXT",
):
try:
conn.execute(statement)
@ -219,11 +221,12 @@ def save_grade(grade, thumbnail=None, label=None, source_images=None):
"(created_at, label, card_type, card_note, image_count, thumbnail, "
" model, estimated_grade, "
" grade_low, grade_high, confidence, categories_json, "
" authenticity_json, "
" edge_measurements_json, centering_measurement_json, "
" aspect_measurement_json, "
" limitations_json, note, estimated_cost, usage_json, "
" source_images_json) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
(
now(), label, grade.get("card_type"), grade.get("card_note"),
grade.get("image_count"), thumbnail,
@ -231,6 +234,7 @@ def save_grade(grade, thumbnail=None, label=None, source_images=None):
grade.get("estimated_grade"), grade.get("grade_low"),
grade.get("grade_high"), grade.get("confidence"),
json.dumps(grade.get("categories")),
json.dumps(grade.get("authenticity")),
json.dumps(grade.get("edge_measurements")),
json.dumps(grade.get("centering_measurement")),
json.dumps(grade.get("aspect_measurement")),
@ -264,6 +268,7 @@ def update_grade_result(grade_id, grade, thumbnail=None, source_images=None):
grade.get("estimated_grade"), grade.get("grade_low"),
grade.get("grade_high"), grade.get("confidence"),
json.dumps(grade.get("categories")),
json.dumps(grade.get("authenticity")),
json.dumps(grade.get("edge_measurements")),
json.dumps(grade.get("centering_measurement")),
json.dumps(grade.get("aspect_measurement")),
@ -274,7 +279,7 @@ def update_grade_result(grade_id, grade, thumbnail=None, source_images=None):
sql = (
"UPDATE grades SET created_at=?, card_type=?, card_note=?, "
"image_count=?, model=?, estimated_grade=?, grade_low=?, "
"grade_high=?, confidence=?, categories_json=?, "
"grade_high=?, confidence=?, categories_json=?, authenticity_json=?, "
"edge_measurements_json=?, centering_measurement_json=?, "
"aspect_measurement_json=?, limitations_json=?, note=?, "
"estimated_cost=?, usage_json=?"
@ -404,7 +409,7 @@ def get_grade_images(grade_id):
def _row_to_grade(row):
d = dict(row)
for key in ("categories_json", "edge_measurements_json",
for key in ("categories_json", "authenticity_json", "edge_measurements_json",
"centering_measurement_json", "aspect_measurement_json",
"limitations_json", "usage_json"):
out_key = key[:-len("_json")]
@ -429,9 +434,10 @@ def _row_to_grade(row):
_LIST_COLUMNS = (
"id, created_at, label, card_type, card_note, image_count, thumbnail, "
"model, estimated_grade, grade_low, grade_high, confidence, "
"categories_json, edge_measurements_json, centering_measurement_json, "
"aspect_measurement_json, limitations_json, note, estimated_cost, "
"usage_json, (source_images_json IS NOT NULL) AS source_images_json"
"categories_json, authenticity_json, edge_measurements_json, "
"centering_measurement_json, aspect_measurement_json, limitations_json, "
"note, estimated_cost, usage_json, "
"(source_images_json IS NOT NULL) AS source_images_json"
)