Training and certification
ContentsVision Academy is the in-product training program: users learn the system by doing the real job inside a sandboxed training claim, earn points and badges per mission, and finish with a publicly verifiable certificate. Everything is deterministic and offline: training never calls the AI providers, never spends credits, and never contacts Adjust Square.
The design in one paragraph
The curriculum lives in code
(backend/app/services/training.py, the MISSIONS list) as ordered
missions, each with instruction steps, a quiz (answers stay server-side),
and "doing" checks. A mission's checks are graded against the actual
database state of the user's training claim: the user really creates the
room, really uploads media through the real uploader, really fixes the
planted inventory defects, and really runs Send for Estimation. The training
claim makes that safe: uploads into it are intercepted and completed
instantly with fixture items instead of running the pipeline, and
submission short-circuits into a dry run.
The training claim sandbox
claims.is_trainingflags a sandbox claim. One is auto-created per user byPOST /api/training/start(idempotent), numberedTRAINING-<FIRSTNAME>.- It appears in the normal dashboard with a blue Training chip, and the claim page shows a banner linking back to the Training hub.
- Everything else about it is a real claim: rooms, uploads, items, review, activity logging.
Three interception points make it free and safe:
| Hook | File | Behavior for training claims |
|---|---|---|
| Upload finalization (video/audio) | app/api/routes/upload.py, finalize_single_media_upload | Skips billing parking and the pipeline; training.complete_training_job seeds fixture items and marks the job complete synchronously. |
| Process (photos after grouping, parked confirms) | app/api/routes/jobs.py, process_job (and quote_job, which quotes 0) | Skips the credit charge and pipeline; photo jobs get one item per photo group, honoring the user's grouping, with the actually-uploaded files as item photos. |
| Submission | app/api/routes/claims.py, _submit_training_dry_run | Runs the identical room/item selection and locking (items get submitted=True), issues a fake claim key TRAINING-DRY-RUN-<id>, never touches the Adjust Square client and needs no team token. |
Deterministic fixtures and planted defects
Fixture inventories are constants in app/services/training.py:
- The first video uploaded into a training claim seeds
MESSY_VIDEO_ITEMS: ten items with six deliberate problems (a microwave in the wrong room, a duplicated TV, a mirror-reflection non-item, quantity 12 coffee tables, a sofa with no condition, and a vague "Small Kitchen Appliance" whose audio evidence names a blender). Mission 3 grades the user on finding and fixing all six, then approving everything. - Later videos seed
EXTRA_VIDEO_ITEMS(small, clean) so re-uploads cannot corrupt the graded state. Audio uploads seedAUDIO_ITEMS(clean, with transcript and per-item audio ranges). Photo jobs create one item per group at Process time. - The downloadable sample pack lives in
frontend/public/training/(four generated JPEGs, a WAV voice memo, an MP4 test video, andsample-pack.zip). The files are real playable media, but their content is irrelevant: classification happens by extension and the pipeline never runs.
Grading
training.evaluate_checks re-derives every check from the database, for example "exactly one active
item named like tv", "the item named like microwave has room like
kitchen", "claim submitted with 5+ items". A mission completes when all
checks pass and its quiz was passed (70%+); completion banks the points
exactly once (mission base + 10 per correct quiz answer). Missions unlock
sequentially. Quiz answers are never sent to the client; grading returns
per-question correctness and explanations.
Two endpoints share that evaluator:
GET /api/training/missions/{key}/checksgrades with no side effects (no attempt counter, no stored results, no completion). The on-screen task tracker polls it every few seconds so tasks tick themselves off live.POST /api/training/missions/{key}/checkis the explicit grade: it stores the results, counts the attempt, and completes the mission (banking points exactly once) when all checks and the quiz are passed.
The on-screen mission tracker
components/TrainingTracker.tsx is a floating quest log mounted app-wide in
AppShell, so the active mission's task list stays on screen while the user
works inside their training claim instead of forcing round trips to the
Training section. It follows the first unlocked, incomplete mission and:
- polls the GET checks endpoint (5s, only while expanded and the tab is visible; also on focus and route changes), so completed tasks tick off automatically;
- shows the hint for the first open task only, plus a knowledge-check row
with a deep link to the mission's quiz step (
?step=quiz); - auto-completes the mission via the POST endpoint the moment every task and the quiz are green, celebrates with the badge and points, then offers the next mission;
- minimizes to a compact progress pill (
Mission 2 · 1/4); the expanded/minimized choice persists in localStorage, and dismissing the tracker hides it for that mission only; - renders nothing for users who never engaged training (no training claim),
on
/trainingand/verifypages, or once everything is complete.
Data model
training_progress: one row per user per mission (status, bestquiz_score,checks_passed, bankedpoints,completed_at). Badges are derived: mission complete = badge earned. No separate badge table.training_certificates: issued once every mission is complete (POST /api/training/certificate). Snapshotsholder_name/team_name, storesscore_pct(overall quiz accuracy),points, and an unguessablecodelikeCV-3F9A-C24D-71BE.revokedinvalidates without deleting.
Public verification
GET /api/training/certificates/{code} requires no auth by design: it
returns {valid, holder_name, team_name, level, score_pct, points, issued_at} (or {valid: false}) so anyone holding a shared link can confirm
the credential. The frontend page is /verify/[code], whitelisted as a
public path in AppShell (no sign-in). The hub offers Copy verification
link and a LinkedIn share for exactly this URL.
Leaderboard
GET /api/training/leaderboard aggregates points, badge counts, and
certification per user, strictly scoped to the caller's team (a tenant
never sees another tenant's names or scores).
Frontend
/training(hub): points and badge stats, path-to-certification progress, the certificate card, the mission trail, and the team leaderboard./training/missions/[key](runner): a step wizard rendering the curriculum's steps: instruction cards, the guided tour launcher, the sample pack download, live "your tasks" checks with a Check my work button, the quiz with instant feedback, and the badge celebration on completion.components/TrainingTour.tsx: the spotlight tour engine. Tours are step lists targetingdata-tour="<id>"attributes; the overlay portals todocument.body(viaModalPortal), dims the screen with a box-shadow spotlight hole, glides between targets with CSS transitions, and is advanced with Next/Back (Esc exits). Start one from anywhere withstartTrainingTour(key, returnTo). Tagged targets today:sidebar-nav,new-claim,claims-list(dashboard),create-room,send-for-estimation(claim page),room-uploader(room page).
Testing
backend/tests/test_training_api.py covers the sandbox lifecycle, the
fixture seeding for all three media types (including the billing bypass and
the second-video guard), sequential unlocking, quiz and check grading against
real fixes, the submission dry run (asserting the Adjust Square client is
never called), certificate issue plus no-auth verification, and leaderboard
tenant scoping. Like the rest of the suite it is fully offline.