Skip to main content

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_training flags a sandbox claim. One is auto-created per user by POST /api/training/start (idempotent), numbered TRAINING-<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:

HookFileBehavior for training claims
Upload finalization (video/audio)app/api/routes/upload.py, finalize_single_media_uploadSkips 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.
Submissionapp/api/routes/claims.py, _submit_training_dry_runRuns 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 seed AUDIO_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, and sample-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}/checks grades 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}/check is 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 /training and /verify pages, or once everything is complete.

Data model

  • training_progress: one row per user per mission (status, best quiz_score, checks_passed, banked points, 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). Snapshots holder_name/team_name, stores score_pct (overall quiz accuracy), points, and an unguessable code like CV-3F9A-C24D-71BE. revoked invalidates 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 targeting data-tour="<id>" attributes; the overlay portals to document.body (via ModalPortal), 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 with startTrainingTour(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.