{% extends "base.html" %} {% macro queue_head(j) -%} {# One upload's heading, whether it is the open section or a shut one. Written once because the two have to be the same line: a heading that changed shape on opening would make the batch you are working look like a different kind of thing from the batches you are not. #} {{ j.filename }} {# Two uploads can carry the same name — "3 images" is what any loose batch is called — so the heading needs the time to tell them apart. #} · {{ j.cards }} {{ 'card' if j.cards == 1 else 'cards' }} {%- if j.cohort %} · {{ j.cohort }}{% endif %} {%- endmacro %} {% macro chosen_icon() -%} {%- endmacro %} {% block title %}Import scans · foilstack{% endblock %} {% block status %}{% if active %}import running · {{ active.processed }}/{{ active.total }}{% elif waiting_total > counts.all %}{{ waiting_total }} scans to confirm · {{ counts.all }} in this batch{% elif counts.all %}{{ counts.all }} scans to confirm · {{ counts.nomatch }} with no match{% else %}queue clear · drop some scans to begin{% endif %}{% endblock %} {% block screen %}
Import scans jpg, png, tif · or a .zip of them
Drop your card scans here
one image per card, or a .zip of them
up to {{ "{:,}".format(max_images) }} scans · {{ max_mb }} MB per upload
{{ active.filename if active }}
{{ 'matching…' if active }}
{{ 'queued' if active else 'ready' }} 0%
{# Two of these counted something other than what they were called. "matched" was `job.processed`, which increments for every scan the importer finishes — including the ones that matched nothing and the ones that errored — so a batch where half the images failed still reported them as matched. And "waiting" is not part of this import at all: it is the review queue, which outlives the job and holds rows from earlier batches too. Sat in a row of progress numbers it read as "waiting to be identified" and looked stuck, because it does not move while an archive uploads. #}
scans
{{ active.total if active else 0 }}
processed
{{ active.processed if active else 0 }}
to confirm
{{ waiting_total }}
Match settings
Default condition
{% for c in conditions %} {% endfor %}
Default finish
Auto-accept above
{# Off ships selected, and no percentage is offered as the default. Auto-accepting is the one thing on this screen that puts a card into inventory with nobody having looked at it, and inventory is what gets priced and exported — so it is the seller's to turn on, not something they have to notice and turn off. #}
{% for t in thresholds %} {% endfor %}
{# Off until a seller ticks it, and nothing but a tick may turn it on: this is the only setting on the screen that overrules a match the encoder was sure about, and a batch that is not really one set is worse off with it on. The seller is asserting something about the pile in their hand, so it is theirs to tick. It is remembered across imports along with the rest of the panel, which is the one place that claim gets stretched — the assertion was about the last pile, not this one. It stays defensible only because the chip is painted on before the file is chosen and sits directly above the Start button, so carrying it forward is a visible answer the seller can untick, not a hidden one. The label is not "Batch is all one…" — an ellipsis set in the same uppercase mono as every other label on the panel reads as a truncated string, and sends the reader looking for the rest. #}
Batch is all one
Why finish and auto-accept work this way

A foil and a non-foil printing use the same artwork, so a scan cannot tell them apart. That is why you set the finish yourself. It is worth getting right: a foil often sells for several times what the plain version does.

Auto-accept is off unless you pick a percentage, and off means every scan waits for you in the review queue. Pick one and a scan skips the queue only when all three of these are true.

  • It scores above the threshold you picked.
  • It beats the second-best match by a clear margin.
  • If that second-best is the same card in another printing, the two prices are close.

Reprints share artwork, so a high score on its own proves nothing. The threshold is saved with each import, so you can always see later why a scan was accepted.

All one game or set is worth ticking when the batch really is — a booster box, or one binder page. Every card is matched on its own first; then the game or set that most of them agree on is worked out, and any card that landed outside it is moved to its best match inside it. A card moved that way always goes to the queue for you to look at, however well it scored.

Leave it off for a mixed pile. It decides the game or set by majority, so on a batch that is genuinely three games it will pull two of them towards the third.

matcher: dinov3 image embeddings · nearest neighbour
scans kept on disk · never uploaded off-host
{% if jobs %}
Recent imports
{% for job in jobs %} {# Truncated by the column rather than by a slice: the pane is a fixed width, so where the name has to give way is a question the layout can answer and the template cannot. The full name stays reachable on hover. #} {% endfor %}
WHENFILESTATUSSCANS
{{ job.filename }} {{ job.status }} {%- if job.processed == job.total %}{{ job.total }} {%- else %}{{ job.processed }}/{{ job.total }}{% endif -%}
{% endif %}

Matches

{# The batch stays open across a filter, so every one of these carries it. Without that, narrowing to "No match" would also swap which upload the screen was about. #} {% set in_batch = ('&job=' ~ open_job) if open_job else '' %}
{% if queue_value %}{{ queue_value|money }} market value{% endif %}
{% for j in queue_jobs %} {# One heading per upload that still has something waiting, oldest first, each stating its own size — from a count over the whole queue, not from the rows below it, so a batch nobody has opened is described as accurately as the one that is open. Exactly one of them holds cards. Opening another is a link and therefore a page load, which is the whole point: the seller works one batch at a time and the screen builds one batch at a time, so a backlog of nine uploads costs the same page as one. The queue used to render as many as fitted a four-hundred-row budget and then apologise at the bottom for the rest, which is a screen that both hid work and had to explain itself. #} {% if j.job_id == open_job %} {# A `
`, so shutting the open batch costs no script and arrives with the keyboard and the accessibility tree already working — the same idiom the match-settings panel above uses. Shutting it loads nothing and throws nothing away; only opening another does. #}
{{ queue_head(j) }} {% for r in rows %}
scan
{{ r.filename }}
→
{% if r.card_id %}{% endif %} catalogue
{{ r.name }}
{{ r.meta }}
{# No one-click swap on a row someone has already decided: that button commits on the spot, and putting a commit-to-the-guess control beside a deliberate choice is a trap. The panel behind "wrong card?" still lists every candidate, and picking there does not commit. #} {% if r.needs_review and r.alternates and not r.chosen and not r.cohort %} {# Titled because the line is ellipsised at the pane's width, and what falls off the end is the tail — which on a row the batch moved is the match it was moved off, the only part a reader cannot get from anywhere else on the screen. #} {% else %}{{ r.alt }}{% endif %} {% if r.needs_review %} {% endif %}
{% if r.chosen %}{{ chosen_icon() }}{% else %}
{{ r.pct }} match
{% endif %}
{% for c in conditions %} {% endfor %}
{# Rendered in its final state rather than left to the script to correct, so a foil batch does not flash "Non-foil" first — and that now includes the fill, because a freshly rendered row is no longer always showing what its import asked for. A card the catalogue prices one way only overrules the default, and the unfilled chip is what says so. #} {% if r.ambiguous %} ? {% endif %}
{% if r.needs_review and r.card_id %} {% endif %}
{# Full-width, so it wraps onto its own line inside the flex row rather than squeezing the columns beside it. Filled on first open. #}
{% endfor %} {% if not rows %} {# The batch is open and its rows are all filtered out. Said inside the section rather than in place of the list, because the other uploads are still on the screen and a message where they are would read as a claim about them. #}
{% if filter == 'matched' %}nothing in this batch matched {% else %}everything in this batch has a match{% endif %}
{% endif %}
{% else %} {# Shut. A link rather than a closed `
`, because opening it is a page load — the cards do not exist yet. It works with no JavaScript, and the batch is in the URL, so the screen can be reloaded or bookmarked onto the batch being worked. No `#job-N` fragment, though it is the obvious thing to add. Landing on the heading puts it at the top of the viewport, which scrolls the bar above the list — the filter chips and the Commit button — off the screen, and arriving at a batch with its own controls missing is worse than arriving above eleven lines of headings. #} {% endif %} {% endfor %} {% if not queue_jobs %}
queue is clear · drop some scans to start
{% endif %}
{% endblock %} {% block scripts %} {% endblock %}