Dashboard — read-only overview. The coverage table shows how many bank questions exist per topic bucket and difficulty; click any count to drill into those exact questions (jumps to the Questions tab pre-filtered). Use it to judge whether a paper spec is feasible before assembling. Published papers open directly in the test runner.
Bank coverage
Published papers
Browse bank
Browse — filter the question bank. Topic matches blueprint buckets (Arithmetic, Algebra, ModernMath_Geometry, DataInterpretation, Arrangements, Series_Coding, CriticalReasoning, ReadingComprehension, VerbalLogic, Grammar, Vocabulary). Ans shows the correct option letter. Used in lists every published paper containing the question (click to open it in the runner; — means never used) — combine with the Usage filter to find unused questions. Subtopic shows the canonical syllabus tag (every question carries one); filter by it with the Subtopic box. Stage is the question lifecycle: fresh LLM output lands in INBOX (never assembles) — click ▲ to promote INBOX → PRACTICE → MOCK → SIMULATION, ▼ to demote, ✕ to retire; filter by stage with the Stage box. Click the qid / stem / 👁 to open the full inspector (dataset + stem + options + hint + solution + stage + paper usage, full width for DI tables/formulas; ←/→ moves across the filtered view, Esc closes). Colour legend: QA blue · LR amber · VARC green, and Very Easy / Easy / Medium / Hard / Expert — used across the bank, dashboard, blueprints, paper preview and Papers breakdown.
Add question
Add — appends one question to the bank after schema validation. Required: unique qid (e.g. QA-ARITH-014), section (QA/LR/VARC), topic (use a bucket name so mixes can find it), difficulty (Very Easy/Easy/Medium/Hard/Expert), stem, 4 options, answer_index (0–3), solution (the full explanation — required by the schema). hint is optional but recommended: a short nudge shown alongside the question detail. subtopic must be one of the topic bucket's canonical subtopics (see the Topics tab) — anything else is rejected. Saved questions enter at PRACTICE stage (usable in assembly immediately; pass an explicit "stage" like "INBOX" in the JSON to override).
Paste one v2 question JSON.
Admin app user guide — every area, end to end
This one guide covers the whole admin app: Dashboard · Questions · Topics · New paper · Papers · LLM loop · Config · Students · Users · State. Open it anytime via the 📖 Guide button at the right corner of the menu above. Nothing here needs AI unless explicitly stated.
Release notes — what changed
Version history from CHANGELOG.md (current release: …). Newest first; each entry lists what was added, changed or fixed.
loading…
Dashboard — read-only overview
Bank coverage shows question counts per topic bucket × difficulty — click any count to drill into those questions (Questions tab opens pre-filtered) — check it before assembling to judge whether your spec is feasible. Published papers lists everything shipped to the runner, each with an Open ↗ link that loads it directly via ?paper=<id>.
How papers are made — no AI needed
Paper generation is deterministic curation from your validated question bank. The LLM tab is purely an optional authoring aid for writing new questions — assembly itself never calls any AI.
The 5-step pipeline
| # | Step | What happens | Where |
|---|---|---|---|
| ① | Bank (raw material) | 77 curated v2 questions live in JSONL files, each tagged with section, topic bucket, difficulty, options, answer, hint and solution. | bank/*.jsonl · Questions tab |
| ② | Spec (recipe) | You pick a saved blueprint (section counts + topic mix + difficulty split) or build a custom spec (counts, single-topic-or-mix, difficulties, seed). | blueprints/*.yaml · New paper tab → POST /api/assemble |
| ③ | Assemble | Seeded shuffle picks exact per-bucket counts (fails with need/have numbers if the bank is short), then auto-repairs difficulty via same-bucket swaps and balances answer positions ~25% each. | scripts/banklib.py:assemble |
| ④ | Preview (nothing saved) | You see the achieved mix, difficulty counts and warnings — or a plain-English "bank short" error naming the topic that needs more questions. | Preview pane in New paper tab |
| ⑤ | Publish | Re-validates (schema + marking + counts/mix/difficulty) and writes the paper to papers/ + public/papers/, updating the runner manifest. | POST /api/papers/publish |
Worked example — a 5-question Arithmetic drill
New paper tab → Exam kind Practice → Source Custom spec → one section row: QA, count 5, mode single topic, topic Arithmetic → Assemble (preview shows 5 × Arithmetic) → Publish → Open in runner. Takes under a minute.
Field glossary
| Field | Meaning |
|---|---|
| Exam kind | Mock = standard timed paper · Simulation = exam-day strict (no hints/pauses, solutions locked till submit) · Practice = short set, e.g. drills. |
| Seed | Random draw control — same bank + same spec + same seed = the identical paper. The seed shuffles each bucket's candidate pool before picking (scripts/banklib.py:assemble uses random.Random(seed)), and also drives the difficulty auto-repair swaps; answer-position balancing is deterministic and seed-independent. Default 42 (blueprint Check against bank always dry-runs with 42). The used seed is stored as meta.seed inside the paper and shown in the Papers tab breakdown — so to regenerate an exact paper, reuse its blueprint/spec and its seed. Caveats: adding, removing or reordering bank lines reshuffles the draw even with the same seed; different seeds on one blueprint give different-but-equally-valid variants (handy for parallel sets). CLI equivalent: --seed <n>. |
| Paper ID | Unique id per paper — it becomes the filename papers/<Paper ID>.json. Auto-suggested from the title + timestamp; publishing a duplicate Paper ID is blocked unless you confirm the overwrite. |
| Section mix vs single topic | Mix auto-splits across the section's taxonomy buckets · Single topic takes all questions from one bucket (drills). |
| Difficulties | Restricts the pool before picking. Unticking a level can cause "bank short" if too little remains. |
When it fails — recovery
| Message | Fix |
|---|---|
bank short for QA/Arithmetic: need 20, have 13 | Add questions (Questions tab or LLM tab), lower the count, or switch to section mix. |
difficulty Easy: want 20, got 19 (warning) | Bank spread is thin for that bucket — preview still usable; add Easy questions of that topic to silence it. |
| Validation error on publish | Read the listed line: schema breach (fix the question), marking drift, duplicate qid, or count mismatch. |
paper_id '…' already published | That Paper ID exists. Change the Paper ID for a new paper, or confirm the overwrite to replace it. The publish message always shows the exact saved files (papers/… + public/papers/…) so you can verify. |
Adding new questions without AI
New questions are authored manually — no LLM involved. Flow: Questions tab → Insert template → fill in the fields → Validate & append to bank. The server schema-checks, rejects duplicate qids and unknown topics, then appends to bank/<section>.jsonl. Saved questions are usable in assembly immediately (Dashboard coverage updates).
| Field | Rule |
|---|---|
qid | Unique id, e.g. QA-ARITH-014. Duplicates are rejected. |
section / difficulty | QA|LR|VARC and Very Easy|Easy|Medium|Hard|Expert exactly. |
topic | Must be a taxonomy bucket or alias (see Topics tab), otherwise assembly mixes can't find it. |
stem / options / answer_index | Stem ≥ 10 chars; exactly 4 non-empty options; answer_index 0–3. |
marks / time_sec | Locked to +3 / −1 / 0; time 30–600 s. |
solution / hint | Solution (full explanation) is required; hint (short nudge) is recommended. Both show in the bank detail view. |
Questions tab — browsing the bank
Filter by Section, Topic (taxonomy bucket), Difficulty or free-text Search (stem or qid), then Filter. Click + on any row for a quick peek, or 👁 / qid / stem for the full inspector overlay (sticky dataset, full stem/options/hint/solution, DI-set siblings, stage buttons, paper links; ←/→ navigates the filtered view, Esc closes). Ans column shows the correct option letter at a glance. The Used in column shows which published papers each question appears in (paper IDs link straight to the runner; ⚠ marks a paper file missing from the runner manifest); the Usage dropdown filters to used or never used questions — handy for spotting bank gaps before writing new questions. The Review dropdown filters to ⚑ flagged (open reviewer votes) or unreviewed questions — flagged rows carry a ⚑ badge; open the inspector's Review section to see independent votes and add your own (flags + note, never edits the question). The Subtopic column shows each question's canonical syllabus tag with a substring Subtopic filter — every question carries exactly one, enforced against the Topics tab on add. Sections and difficulties are colour-coded everywhere (QA blue, LR amber, VARC green; Very Easy / Easy / Medium / Hard / Expert); the runner mirrors this with section-tinted palette cells and difficulty badges.
Topics tab — managing the taxonomy
Buckets are the mix keys blueprints assemble from — one card per bucket (search + section filter on top). The headline is assemblable (PRACTICE/MOCK/SIMULATION) — what can actually go into a paper — with the all-stage total beside it (switch via Counts). Each card shows per-stage counts, the difficulty split and per-subtopic chips (amber = in bank but not in the taxonomy list, dashed = no subtopic; click any chip to drill into those questions). Edit label / subtopics / aliases inline and Save (aliases let old topic names keep resolving). View Qs drills into the Questions tab pre-filtered; Generate → jumps to the LLM loop with that section + topic prefilled (you still press Generate). Rename migrates bank questions + blueprint mixes automatically and reports counts. Delete is blocked with exact usage while questions or blueprints reference the bucket. An unmapped topics alert flags any bank question matching no bucket — fix by adding an alias or correcting the question. Changes apply to assembly immediately.
Blueprints tab — managing recipes
Saved recipes in blueprints/*.yaml. New starts a blank form; Edit loads a file (filename locks once created — use Duplicate to copy under a new name). Per-section mix numbers must sum to the row count; the difficulty split must sum to the paper total (or all zero for no targeting). Check against bank dry-runs assembly without saving; Use in New paper → jumps to the wizard with that blueprint selected. Deleting a blueprint never touches bank questions or already-published papers.
Papers tab — paper manager
Every published paper as a card: Breakdown shows difficulty bars, section/topic/subtopic splits, answer balance, schema + bank integrity, and expected-vs-actual rows when a blueprint shares the paper's ID. Unpublish drops the runner listing only (files stay — the paper moves to the Unpublished section below and can be re-published). Delete removes the listing and both files after a double confirm. Dashboard rows link here via details →.
LLM loop tab — optional AI authoring
Disabled until set up in the Config tab → LLM setup (keys are entered there, never shown back). Pick a Provider per call: OpenAI-compatible (endpoint + model + key) or Gemini (model + key only). Preview exact prompt shows the byte-identical system + user prompts before any call. Generate makes max 2 API calls with a visible progress bar + elapsed timer (button locks against double-clicks), then parses and schema-validates the reply; tick save valid to bank to append passing questions (duplicates skipped but now listed, saved qids listed). Nothing invalid is ever saved. On 429/503 strain you get the call count, a wait hint (~Xs cooldown) and a Retry button — 1 retry = 1–2 calls. A 404 means the model name is retired for your key (e.g. gemini-2.5-flash) — press Refresh available models and Use a listed one (gemini-3.5-flash-lite primary, gemini-3.8-flash fallback).
Config tab — exam defaults + LLM setup
Edits config/exam.config.json: total time in seconds (8100 = 135 min) and per-section counts used for custom papers. Marking is locked to +3 / −1 / 0 — the server rejects anything else. The LLM setup card below it manages the AI authoring config (enabled flag, default provider, per-provider keys/models): keys are typed into password fields, never shown back (only a last-4 hint), and leaving a key blank on save keeps the existing one. Test a saved key with Refresh available models in the LLM loop tab.
Students tab — registered accounts
Students sign up at public/signup.html (first/last name, unique email-id, 10-digit mobile, 8+ char password, T&C checkbox) and sign in at public/signin.html (verified against the stored hash; wrong credentials get a generic 401; success lands on public/dashboard.html with their assigned papers). Stored in data/users.json alongside reviewer/admin accounts (STU-|REV-|ADM- ids; legacy data/students.json migrates once, file kept) with passwords as one-way hashes — hashes are never shown or sent to the browser. Use Search to filter by name/email/mobile, Reset pw to set a new password, Delete to remove an account (their paper assignments go too). Duplicate email-ids are rejected with a 409. The Assigned column lists each student's papers. Assign papers from the Papers tab (per-paper Assign: tick students, optional due date — re-assigning updates the date). Staff accounts live in the Users tab, not here. Note: signup posts to the Flask backend, so it works when served via the local server (/public/signup.html), not from a static Pages deploy. Account backups: deploy/export-users.sh (see State below).
State & staging — nothing you save is lost
Backups card — push auth + clones without the VM
Config tab → Backups card holds everything off-host in one place. Push auth: paste your GitHub username + a token (classic PAT with repo scope on BOTH private repos, or fine-grained PAT with BOTH repos selected and Contents read+write) and Save token — it is stored server-side in persistent state (backup-creds/.git-credentials, mode 0600, survives rebuilds, never in the image, never exported) and every push uses it automatically; saving also probes both remotes and reports reachability per split. The token is never shown back (only user + last-4); leaving the token blank on save keeps the existing one; Forget deletes it. Saving before any clone exists cannot validate the token against GitHub — the Clone is the first real test, so a 403 "Write access not granted" there means fix the token's repo selection/scope (not the setup) and Clone again. Clones: when a split reports "not a git clone", press Clone both with defaults (or per-split Clone) — the URL boxes come pre-filled from IPMAT_CONTENT_REPO_URL / IPMAT_BACKUP_REPO_URL; leave blank for the default, paste to override. The server clones it in place (private repos use the saved token) and checks out your backup branch. A clone on the container rootfs (bind mounts still commented out) is flagged container-local — a rebuild wipes it: uncomment the mounts in deploy/compose.stage.yaml, rebuild, and Clone again. The header pill shows the deployment environment (IPMAT_ENV: stage amber, prod red, local grey) — backups inherit it for users/<env>/ + content tags unless IPMAT_BACKUP_ENV overrides. Branches: snapshots land on IPMAT_BACKUP_BRANCH (default main; stage uses stage) — same repos, separate histories; content tags are per-env (content-<env>-vDATE, plus -2/-3 for same-day reruns — a second push the same day no longer fails). The status line shows the configured branch and each clone's actual branch. Cron keeps the daily/weekly schedule — the buttons are push-now after authoring sprints. Host-mount auth from older setups still works as a fallback (shown as source "host-mount"); saving here migrates it into state. Oldest fallback remains the VM copy-paste in deploy/README.md.
Topic taxonomy config/topics.json — single source of truth
Buckets are the mix keys blueprints assemble from — one card per bucket (search + section filter on top). The headline is assemblable (PRACTICE/MOCK/SIMULATION) with the all-stage total beside it — flip via Counts. Each card shows per-stage counts, the difficulty split and clickable subtopic chips (amber = in bank but missing from the taxonomy list, dashed = no subtopic). Edit label / subtopics / aliases inline and Save (aliases let old topic names keep resolving). View Qs drills into the Questions tab pre-filtered; Generate → jumps to the LLM loop with that section + topic prefilled (no auto-run). Changes apply to assembly immediately. Demoting/retiring a question that sits in published papers warns first — published content never changes (snapshots); watch the Papers tab health badges instead.
Add bucket
Blueprints blueprints/*.yaml — paper recipes
Blueprints are the saved recipes assembly uses: section counts + per-bucket topic mix + difficulty split. Edit loads one into the form below, Duplicate copies it, Delete removes the file (never touches the bank). Use → jumps to the New paper tab with that blueprint pre-selected. Mix counts for a section must sum to its count; the difficulty split must sum to the paper total.
New paper
How it works: ① choose an exam kind and a source → ② Assemble previews the paper (topic mix, difficulty split, warnings — nothing is saved yet) → ③ Publish validates and adds it to the runner. If the bank can't satisfy a spec you'll get a "bank short" message telling you exactly which topic needs more questions. Publishing an already-used Paper ID is blocked — change the ID or confirm the overwrite.
How is this paper made without AI? (quick recap — full version in the Guide tab)
Your spec picks questions from the bank (bank/*.jsonl) by seeded shuffle: exact per-topic counts, difficulty auto-repair, answers balanced ~25% each. Same bank + same seed = same paper. Preview saves nothing; Publish validates and ships to the runner. The LLM only ever writes new questions — it is never involved in building papers. See the Guide tab for the full pipeline, a worked drill example and failure recovery.
Papers composition, blueprint match, publish state
Breakdown expands a paper: difficulty/section/topic/subtopic splits, answer balance, schema + bank integrity, and expected-vs-actual rows when a blueprint shares the paper's ID. Unpublish removes the runner listing only (files stay — reversible via Re-publish in the Unpublished section below). Delete removes the listing and both files (cannot be undone).
Unpublished files kept, hidden from the runner
—
LLM question loop (opt-in via app/llm.config.json)
Generate sends ONE section batch prompt (from prompts/) to your chosen provider — 1 click = max 2 API calls, then it stops and shows a manual Retry. You'll always see progress (preparing → sending → waiting Ns → done/failed with call count + seconds). Tick save valid to bank to append passing questions (duplicates skipped) — saved qids are listed and visible via Questions → Filter. Only schema-valid questions are ever saved, and fresh saves enter at INBOX stage — promote them to PRACTICE after review. First enable it in the Config tab → LLM setup (paste keys there; per provider: OpenAI-compatible → endpoint + model + API key; Gemini → model + API key only). Use gemini-3.5-flash-lite (current, cheapest) or gemini-3.8-flash — gemini-2.5-flash and older are deprecated and 404. Refresh available models lists exactly what your key supports (free); Use selected model saves it locally.
Exact prompts sent to the LLM (system + user) — click to expand
Exam config config/exam.config.json — marking locked to +3/−1/0
Defaults used for custom papers and the runner: total time in seconds (8100 = 135 min) and per-section question counts. Marking is fixed to the IPMAT pattern and cannot be changed here — the server rejects anything else.
LLM setup optional AI authoring — keys stay server-side
Keys are stored in the local git-ignored config (volume-persisted on stage) and are never shown back — fields display only a last-4 hint. Leaving a key blank on save keeps the existing one. Test with Refresh available models in the LLM loop tab.
Gemini
OpenAI-compatible
Testing reset bulk clear + restore — for test cycles
Wipes test data from persistent state. Always backs up first into .reset-backup/<timestamp>/ (newest 5 kept) — restore any backup below. Blueprints, exam config and taxonomy are never touched.
Students public/signup.html → data/students.json
Search filters by name, email-id or mobile. Passwords are stored hashed and never displayed. Reset pw sets a new password (min 8 chars); Delete removes the account permanently. Signup page: /public/signup.html.
Users staff + students — data/users.json
Staff (reviewer/admin) are never created on public pages — add them here with a temp password; they set their own on first sign-in. Reviewers default to read-bank + vote only; tick extra caps to delegate (users/reset can never be delegated). Reset pw forces a temp password (must change on next sign-in). Pending password requests queue below while email reset is off.