Job runs, decisions and new accounts, newest first.
Run history
Every recorded run, newest first. Click one for its log and suggested fixes.
Commands from this console
Publish, Look for new sets and Rerun presses, as the Mac reported them back.
Find an account
Search by email, username, user number (#42) or UID.
?
—
—
UID
User number
Username
Email verified
Account status
Sign-in methods
Created
Last sign-in
Subscription
Email opt-out
Account actions
Everything here runs server-side and takes effect immediately.
Change email
Updates Auth and the profile document, resets verified status and emails the new address.
Subscription
For refunds, goodwill and comped accounts. A later RevenueCat event for this user will overwrite whatever you set here.
Delete account
Deletes the Auth user, which cascades into their collection, armies, recipes, username reservation and uploaded images. There is no undo. Type the account's email to confirm.
Newest accounts
The 20 newest accounts. Click a row to open it.
Loading submissions…
Nothing in this queue
JK moveEnter openA approveR rejectZ zoom imageU undoEsc close/ search
Find a set in the catalog
Search by name, barcode or set id. An edit queues a correction — reviewed, published and undoable exactly like one from an issue report.
—
Sets published
—
Approved, not published
—
Rejected
—
Requesters emailed
What the pipeline has done
Every proposal that reached a decision, newest first. A set counts as published once it is in the workbook, Google Sheets and Firestore — the same moment the requester is emailed.
New paints waiting for a colour
Found on a maker's shop; the pipeline refuses to invent the hex.
Colour reports 0
"This swatch is wrong", sent from the app. Worst first.
—
Accounts
—
Sets per account
—
Armies per account
—
Recipes per account
What the average account has
The median and the share of accounts using a feature say more about a typical user than the mean.
Accounts
From the same snapshot as the table.
Signups per month
The last 12 months with at least one signup.
Send an email to users
Every message gets an unsubscribe footer, and opted-out users are excluded automatically.
Marketing email to EU recipients needs a lawful basis and a working opt-out. The unsubscribe footer is added for you, but account notices (password resets, verification) are the only messages safe to send without consent. Count recipients and send yourself a test before every real send.
What the recipient sees
Exactly what is sent — your HTML plus the footer. Unstyled here means unstyled there.
Hobby Armory(no subject yet)
Previous broadcasts
—
Image comparison
Current catalog image
No image yet
This would be the first one
New submission
Published photos
Submission details
KindSet photo
Catalog ID
Photographed in
Faction
Submitted by
User ID
Submitted
File size
Paint recipe
Image
100%
Scroll to zoom · drag to pan · double-click to toggle
Published to catalog
One page that answers: is the app OK, is the machinery OK, and what is waiting for me?
Needs your attention
Problems only, worst first. Each one says what happened, why it matters, and a proposed fix — a button where the console can do it, a command to paste where only the Mac can. A job failure's fix comes from the pipeline's catalogue of failures it has actually seen, matched against that run's traceback.
Where we stand
Every queue that waits on a person — inbox, catalog proposals, image submissions, paints, colour reports — with what is in it right now.
Background jobs and Coming up
What runs on the Mac, when it last ran, how it went, and when it runs next. Schedules are read from launchd itself, so this page cannot claim a time launchd does not keep.
How the app is doing
Accounts, active users and signups from the cached stats snapshot (refreshed at most every 6 hours; Recompute on App stats forces it).
Where these come from
The Mac's heartbeat — the console bridge checks in every 5 minutes. No check-in for 15 minutes means it is asleep, offline or the process died, and nothing scheduled or pressed here will run.
Job runs — the latest run of every job. A failure or warning shows with the suggested fix the pipeline matched from its log.
Missed runs — a scheduled slot that passed with no run recorded, usually because the Mac was asleep.
launchd — a job that is not loaded will never run, and says nothing.
Queues — work that is decided but not finished: approved sets nobody published, paints approved but not yet pushed, console commands the Mac has not picked up.
Routine work (a new request, a pending photo) is not an attention item — it is in Where we stand.
Everything that runs unattended on the Mac, and the bridge that carries this console's buttons to it.
How the data gets here
Every run appends a line to run/.state/runs.jsonl on the Mac: status, a headline, issues, the log tail and suggested fixes. The always-on console bridge (catalog/watcher.py) publishes those to ops_runs, the job registry (run/jobs.json) to ops_status/jobs, and every 5 minutes a heartbeat with launchd's own view of each job to ops_status/mac.
The console only ever reads these. Rules let it create a command and nothing else; the Mac writes results with a service account, so a console session cannot fake a successful run.
Statuses
OK ran and everything checked out.
Warning finished, but something needs a look — a box not linked to a datasheet, a rule upstream changed.
Failed a step failed; nothing past it ran.
Skipped nothing to do — the daily 11e and Old World checks skip when upstream has no new commit, which costs nothing.
Rerun
Runs the job exactly as launchd does, through run_update.sh, with the same log, report email and journal line. A per-job lock means a Rerun pressed while the scheduled run is going is refused rather than doubled.
Suggested fixes
Matched from KNOWN_FAILURES in run/ops_record.py — one entry per failure the Mac has actually had. When a new kind of failure happens twice, add it there and it will arrive with its fix next time.
Search by email, username, user number (#42) or UID, then act on the account.
Action
Callable
Send password reset
adminSendPasswordReset
Resend verification
adminResendVerification
Mark email verified
adminVerifyEmail
Change email
adminChangeEmail
Disable / re-enable
adminSetDisabled
Grant or clear subscription
adminSetSubscription
Download data (GDPR)
adminExportUser
Delete account
adminDeleteUser
Firestore rules give admins read access to users and never write, so every change goes through a callable in europe-west1.
An account with no users/ document is flagged: it signed up before the profile existed, or a deletion half-completed.
Issue reports and catalog requests submitted from the app. Every submission also emails support@hobbyarmory.de.
What drops out of the open list
A submission the pipeline has already picked up — a request or an issue report — leaves the open list and shows its state (in the catalog queue, approved, publishing next, published) under Include resolved. That is derived by cross-referencing catalog_staging, so there is one source of truth. A rejected proposal releases it back here, because rejecting means the pipeline will not handle it.
A report marked not staged: … is one the pipeline could not turn into a proposal — usually a set the workbook does not carry. Answer those by hand.
Answer…
One dialog does both halves: the email the person gets, and the instruction the pipeline reads back. Picking an outcome rewrites the draft; editing the draft never changes the outcome, and once you type in it it stops being rewritten.
Outcome
Pipeline does
Added
Publishes it
Already in the app
Closes it against the setId, adds nothing
Not covered
Never re-proposes it
Need more detail
Waits for a reply
Corrected / Logged / App bug / Nothing to change
Issue-report outcomes; corrections normally arrive as proposals in the Catalog queue instead
"Already in the app" replies the pipeline matched itself are sent by the next Publish approved, which is why that button can run with no sets approved.
catalog_staging is the handover between the ingestion pipeline and a person.
The pipeline writes a proposal plus the questions it could not answer; you answer them and decide; the pipeline reads the decision back and writes the workbook.
Sets and corrections
A set is a box that is missing (a request or the Sunday Preview). A correction is a box that is wrong: it names the setId it edits and shows exactly which cells move. Research may disagree with the reporter — catalog already right refuses Approve and points you to Reject, which tells the reporter it was checked.
Rules that keep this safe
A proposal carries no setId — ids are allocated when the row is written, so a proposal that sits for days cannot collide.
An unanswered blocking question blocks approval. FYI questions do not.
Answers like "Drop this one" or "Same box" mean do not publish — Approve refuses and bulk approval skips them.
A correction with no change cannot be approved.
Replies
Approve and Reject both open a prefilled, editable reply. An approval's mail is sent by notify.py once the set is live; a rejection is sent immediately.
Closing keeps your edits
Closing the dialog saves a draft on the proposal (a your edits saved pill). Discard my edits restores what the pipeline proposed.
The buttons
Publish approved — the Mac runs apply.py then notify.py. The workbook must not be open in Excel. The log streams here.
Approve all high confidence — only proposals with everything resolved and no blocking question, in one batch.
Look for new sets — what the 07:30 daily job does, now.
Correct a published set without opening the workbook.
It does not write catalog_sets. Both sync scripts rebuild Firestore from the workbooks, so a direct write would be erased on the next publish. What it queues is a correction — the same shape an issue report produces — already approved, which goes out on the next Publish approved.
apply.py checks the live workbook row against the snapshot this form was built from and refuses any column that moved.
The system is not editable: a correction cannot move a set between workbooks.
If another correction is already queued for the row, the form says so — only the first would be written.
Name search uses the same keywords array as the app. A barcode or set id is looked up exactly. Nobody is emailed.
paint_staging: paints the discovery pipeline found on a maker's own shop and could not colour.
It knows the name, range, code, barcode and price. It refuses to guess the hex, because a wrong colour is a confident answer nobody can check by looking at the row — which is how 1,482 fabricated hexes got into the catalog once.
Most of these need nothing
Makers publish swatch charts eventually and the pipeline measures them itself; the row then moves to Resolved by itself. This page is for ranges you want sooner and for saying not a paint.
Working a row
The swatch sits on a checkerboard so "no colour" and white never look alike.
Text field and picker are one value. 7a4b2a and #abc are accepted.
Pick from photo (Chrome/Edge) enlarges the maker's photo and opens the eyedropper.
Approving without a colour is refused — here and in the pipeline.
This console never writes the paint catalog: it writes status and decision, and rules enforce it. Decisions are read by promote.py on the Monday 05:00 run, or now with Publish approved paints.
paint_color_reports: the only route by which a user can correct a colour.
Each report is drawn as the comparison it is — what the app shows, what the reporter suggests, and the applied colour — sorted worst first by ΔE. 19 units out is a different colour; 3 units is a matter of screens.
The reporter's suggestion is only a starting point: it came off their screen.
Applying the colour the app already shows is refused.
An applied correction is pinned (manual_admin_correction) so the weekly resolve cannot put the old value back.
Status goes pending → applied | dismissed → published, and the reporter can see it in the app.
User-submitted catalog photos — formerly the separate Image Submission admin.
Sets and units
A Set photo shows a whole box and is published to catalog_images. A Unit photo shows one kind of miniature and is published to catalog_unit_images/{miniatureId} — so it appears on that unit's tile in every set containing it.
A pool, not one image
The app shows up to three photos per target. Approving appends to the pool; the review window's Published photos strip is where you make one the cover, move one, or remove one. Remove is not Revoke: no email, no status change.
The undo window
Every decision is reversible for 12 seconds. Nothing irreversible — deleting the upload, deleting a published file, sending the email — happens until it closes. A decision stranded by a closed tab is finished on the next load.
Recipes
A submission can carry a frozen paint recipe. Publish with photo (ticked by default) decides whether it goes live with the photo.
Keyboard
J/K move, Enter opens, A approves, R rejects, Z zooms, U undoes, / searches, Esc closes. Revoke has no shortcut on purpose.
Images go through the Cloudflare worker image-upload-worker (preview, transform, R2 delete), authenticated by your Firebase ID token.
Every proposal that reached a decision, newest first, with the setId it was published as and whether the requester was emailed.
A set counts as published only once it is in the workbook, Google Sheets and Firestore — also the point the email goes out. This reads the same cache as the Catalog queue, so it costs nothing to open.
adminGetStats: what the average account has, plus account totals and signups per month.
Totals come from count() aggregations and cost almost nothing; medians and "accounts using it" need the documents, so the snapshot is cached in meta/stats for 6 hours. Opening the page reads the cache — Recompute pays for a fresh scan.
Cohorts
Active means the app refreshed the account's sign-in token inside the window (lastRefreshTime), because people stay signed in for months and the last sign-in date would call a daily user dormant. Collections not scanned per user show not per account in a cohort view.
Email a segment of users through mail_queue.
The preview is the bytes, not an impression of them. It renders your HTML plus the unsubscribe footer and nothing else, because that is exactly what adminSendBroadcast sends. UNSUBSCRIBE_FOOTER in app.js is a copy of the function's — change one, change both.
The warning under the message flags markup mail clients throw away: <style> blocks, class attributes, scripts, http: images and HTML5 layout tags.
Each user gets an unsubscribe token on first broadcast; opted-out users are filtered from every segment. There is no recall — count and send yourself a test first.