Abhyas Request demo access

Adaptive learning desk · for coaching centres

Practice that comes back just before you forget.

Abhyas is where a learner at a coaching centre in Vijayawada reads tonight's lesson, takes a quiz that adapts to how she did, reviews the cards that are due, and follows a plan that bends when life gets busy. Her teacher opens one desk and sees who is slipping before the exam does.

Leave your email once and we send one access PIN and one temporary login that open every OC Apps demo, this one included. The demo runs on synthetic data and reseeds itself; nothing you enter is kept.

Streak
5 days
XP
340
Quiz mastery
72%
Cards due
13
Class 10 Mathematics
63%
Spoken English
50%
Tally & GST
17%

The problem

A coaching centre with sixty students and three teachers runs on a notebook, a WhatsApp group and one teacher's memory. Three things go wrong every term.

Nobody knows who is falling behind

Attendance is in the notebook, marks are on loose sheets, and the quiet student who stopped practising three weeks ago is discovered at the exam.

Quiz and forget

A test on Friday is gone by Monday. There is no schedule that brings a fact back the day before it fades, so revision means re-reading the whole chapter.

Study plans die by week two

A plan written on day one never bends. One busy week and the learner is behind on paper, gives up on the plan, and the plan quietly stops meaning anything.

The product

A learner's desk and a teacher's desk, both server-rendered and fast, with a tutor drawer that only uses what is in the lesson.

Courses & lessons

A searchable catalogue by category and level; enrol from the course page; ordered lessons with teaching text, an optional video, a progress bar and the next lesson always one click away. Completing a lesson earns XP and keeps the streak.

Quizzes that adapt

Five questions built from the lesson itself at four levels. Every wrong answer comes with the sentence that explains it. Mastery is your best score; the next level rises when you pass 80% and eases when you fall under 50%.

Flashcards on a schedule

Key terms become cards. One card at a time, flip, rate yourself 0–5, and the SM-2 spaced-repetition rule decides when it returns: tomorrow, in six days, then further out each time.

Study plans that bend

Pick a pace (relaxed, steady, intense) and a finish date; the remaining lessons are spread evenly in order. Change the pace and the plan rebuilds from today without losing anything you have done.

Teaching desk & at-risk radar

Per course: enrolled, mean completion, mean mastery, work waiting to be graded. A radar flags learners inactive for a week, below 50% mastery, or behind their plan, and links to each one.

Assignments & grading

Teachers set a brief with a due date; learners submit a written answer; late is flagged. The teacher grades with a score and one line of feedback, helped by a rubric that suggests a mark from coverage, length, structure and specifics.

Tutor

Ask a question about the open lesson and get an answer quoted from it, or an honest “not covered here”. Ask what to do next and get one step with a reason. Works without AI; with a model key it helps more, and the badge says which.

Progress is computed, never typed.Completion, mastery, streaks and every at-risk flag derive from completions, attempts and reviews at read time. No status column can drift.
The tutor degrades honestly.Every AI feature has a deterministic offline path and a badge naming the provider. No feature exists only when a key is present.
The learner owns the pace.Plans bend to the learner, quiz levels follow the score, and cards return on the learner's own forgetting curve.

Architecture

One FastAPI process, one SQLite file, server-rendered pages with a little JavaScript for the flashcard flip and the tutor drawer. The same code serves the local service and the hosted twin.

Browserserver-rendered pages · drawer · cards PIN gate (hosted twin)signed cookie · betadoc at / JSON API v1feed token · courses · /verify FASTAPI APP — app/main.py domain.pySM-2 · plans · masterystreaks · certificatesat-risk flags · rubric tutor.pyAnthropic → OpenRouter→ heuristicquizzes · cards · ask · next templates + staticJinja2 · design tokensquiz · flashcard · planprintable certificate seed.py5 courses · 30 lessons12 learners · historydeterministic SQLite — db.py12 tables: courses · lessons · quizzes · attempts · flashcards · plans …local: one file on disk · twin: /tmp, reseeds on cold start Model providers (optional)ANTHROPIC_API_KEY → official SDK, structured outputOPENROUTER_API_KEY → JSON mode · none → rules engine

Feature matrix

Every capability we set out to build, with its status and the test that proved it. This table is generated from the internal build tracker, so it cannot claim more than the tracker shows.

36 of 37 tracked capabilities built and verified · 1 partial · 0 on the roadmap. Generated from the build tracker; each row names the test that proved it.

AreaCapabilityStatusProof (acceptance test)
FoundationsSchema: courses, lessons, quizzes, attempts, flashcards, enrolments, plans, assignments, submissions, certificates, learners, eventsBuiltdb.init() on an empty file creates exactly those 12 tables plus schema_version; running it twice is a no-op (row counts unchanged)
FoundationsSynthetic seed: a coaching centre in Vijayawada with courses, lessons, learners and historyBuiltseeded DB holds >=5 courses, >=24 lessons with real teaching text (>=400 chars each), >=10 learners with enrolments, quiz attempts and flashcard reviews spread over the last 30 days; reseed on an empty DB is deterministic (same counts)
FoundationsApp shell: FastAPI boot, /healthz, base layout with design tokensBuiltcurl /healthz returns 200 JSON with ok=true, db path, course and learner counts and the ai provider; GET / signed in returns 200 HTML with the nav and no placeholder text
FoundationsTest harnessBuiltpytest -q runs >=1 test against a temp DB via TestClient and passes
Courses & lessonsCourse catalogue with search, category and level filtersBuiltGET /courses?q=<seeded title fragment> lists only matching courses; ?category=<cat>&level=<lvl> combine; a filter that excludes everything renders the 'no courses match' state; headless: one card per seeded course, 0 console errors
Courses & lessonsCourse page: ordered lessons, enrol, progress bar, next lessonBuiltPOST /courses/{id}/enrol creates one enrolment (a second POST is a no-op); the page shows lessons in order_index order, a completion percentage equal to done/total and links the first incomplete lesson as 'Next'
Courses & lessonsLesson reader: teaching text, video embed, mark complete → progress, XP and streakBuiltGET /lessons/{id} renders the text and an iframe when video_url is set; POST /lessons/{id}/complete adds the lesson to the enrolment, awards 10 XP once (repeat POST does not double), and a completion the day after the last one raises the streak
Courses & lessonsTeacher authoring: create course, add lesson, reorder lessonsBuilta teacher POST /teach/courses creates a course; POST /teach/courses/{id}/lessons appends with the next order_index; POST /teach/lessons/{id}/move up swaps order with the previous lesson; a learner gets 403 on all three
QuizzesQuiz generator: N questions at 4 difficulty levels from the lesson text; LLM path schema-validatedBuiltPOST /lessons/{id}/quiz with level=basic..expert returns a quiz of exactly N (default 5) questions, each with 4 options, one correct index and an explanation, generated by the heuristic in <2 s offline; a malformed model answer is rejected and the heuristic result is used
QuizzesTake a quiz and get graded with per-question feedbackBuiltPOST /quiz/{id}/submit with answers returns the score as correct/total, marks each question right or wrong and shows the explanation for wrong ones; a submission with a missing answer is counted wrong, not rejected
QuizzesAttempt history and mastery per lessonBuiltafter two attempts on one lesson, GET /lessons/{id} shows both attempts newest first and a mastery equal to the best score; the course page shows mastery per lesson
QuizzesAdaptive difficulty: the next suggested level moves with the scoreBuilttests: score >=80% at 'core' suggests 'advanced'; <50% at 'core' suggests 'basic'; between stays; the lesson page's 'Practice again' button carries the suggested level
PracticeFlashcards generated from a lesson (term → meaning pairs)BuiltPOST /lessons/{id}/flashcards creates >=5 cards for the learner from the lesson text with distinct fronts; calling it again does not duplicate cards
PracticeSM-2 review: quality 0–5 updates ease, interval and due dateBuilttests/test_practice.py pins the SM-2 maths: q=5 on a new card → interval 1 then 6 then round(6*ease); q<3 resets the interval to 1; ease never drops below 1.3
PracticeDue queue with a one-card-at-a-time review pageBuiltGET /practice lists only cards whose due date is today or earlier, oldest due first; reviewing the last due card renders the 'all caught up' state; headless: flip + rate with 0 console errors
Study plans & progressStudy plan generator: pace (relaxed / steady / intense) spreads the remaining lessons to a target dateBuiltPOST /courses/{id}/plan with pace=steady and a date 14 days out creates daily tasks that cover every incomplete lesson exactly once, in order, ending on or before the target date, with no day carrying more than ceil(lessons/plan-days) lessons; intense finishes earlier than relaxed
Study plans & progressAdjusting the pace regenerates the plan without losing completed lessonsBuiltafter completing one planned lesson, PATCH pace=relaxed rebuilds the plan from today with the completed lesson absent and every remaining lesson present
Study plans & progressLearner dashboard: enrolled courses, completion, mastery, streak, XP, cards due, today's planBuilta test asserts each dashboard tile equals its SQL definition on the seeded DB; headless: the tiles and today's tasks render with 0 console errors
Study plans & progressCertificate on course completion with a unique number and a public verify pageBuiltcompleting the last lesson issues one certificate ABH-<year>-NNNN (a second completion issues none); GET /verify/<number> is public and names the learner and course; an unknown number returns 404
Teaching deskAssignments per course with due dates; learners submit text answersBuilta teacher POST /teach/courses/{id}/assignments creates one; a learner POST /assignments/{id}/submit stores the text once (a second submit replaces it); a submission after the due date is flagged late
Teaching deskGrading with score and feedback, plus a rubric-based grade suggestionBuiltPOST /teach/submissions/{id}/grade with score > max_score returns 422; a valid grade is visible to the learner; GET /teach/submissions/{id}/suggest returns a suggested score and >=3 feedback points from the heuristic rubric
Teaching deskClass view: per-course enrolments, completion, mean mastery, submissions waitingBuilta test asserts each class-view figure equals its SQL definition on the seeded DB; the page lists every learner in the course with completion and last activity
Teaching deskAt-risk radar: inactive 7+ days, mastery under 50%, behind plan — each flag links to the learnerBuiltthe seeded DB raises >=3 flags of >=2 kinds; each flag on the teacher dashboard links to /teach/learners/{id}; a learner active today with mastery 90% raises none
TutorProvider layer: Anthropic → OpenRouter → heuristic; never raises; badge in /healthzBuiltwith no keys set /healthz ai=heuristic; a test monkeypatches a failing provider and the tutor still answers with provider='heuristic'
TutorAsk the lesson: a question about the open lesson gets an answer grounded in its textBuiltPOST /tutor/ask with a question whose keywords appear in the lesson returns the matching sentences as the answer with provider='heuristic'; a question with no overlap returns an honest 'not covered in this lesson' reply
TutorWhat next: one recommended action (finish lesson / take quiz / review cards / start plan) with a reasonBuiltfor a learner with cards due, the recommendation is 'review cards'; with none due and an incomplete lesson it is 'continue lesson'; the drawer shows the recommendation with the provider badge equal to /healthz ai
TutorTutor drawer UI with an honest provider badgeBuiltheadless: the drawer opens from any page, the badge text equals /healthz ai, and asking the sample question on a lesson shows an answer with 0 console errors
Accounts & accessRoles admin / teacher / learner with signed-cookie sessions; invite-only sign-upBuilta learner gets 403 on /teach/*; an admin can invite a teacher and the invite link creates that role; the first account on an empty DB becomes admin
Accounts & accessEmail through Resend with an on-screen fallback; invites and access mails deliveredBuiltwith no key mail.mode()=='screen' and the invite page shows the link; with a key a test-mode send returns ok
Accounts & accessEmail-first demo access on the twin: /demo/request issues the PIN and a 2-day guest loginBuiltPOST /demo/request with an email returns the 'check your inbox' page (or on-screen credentials when no channel); the guest login signs in with the learner role and expires after 2 days
ShipLocal service under pm2 on :8361 with an ecosystem fileBuiltpm2 list shows abhyas online; curl http://127.0.0.1:8361/healthz returns 200 after pm2 restart abhyas
ShipHosted twin on Vercel: ephemeral /tmp DB that reseeds, stateless PIN gateBuiltcurl https://<twin>/healthz returns 200 with ai provider; GET /app without the PIN cookie redirects to /demo; with the PIN the dashboard renders
ShipBetadoc site served at / of the twin: story, product, architecture, honest feature matrix, roadmapBuiltGET / on the twin returns the site; its feature matrix marks built / partial / roadmap exactly as the task DB reads at publish time; documentation page links resolve
ShipCustom subdomain abhyas.ajaykongala.online with a verified Resend senderPartialcurl -sI https://abhyas.ajaykongala.online/ returns 200 over TLS and /healthz matches the vercel.app twin; Resend lists the sending domain as verified
ShipHeadless walk of every page: 0 console errors, forms submitBuilttools/walk.py visits dashboard, catalogue, a course, a lesson, a quiz, practice, plan, teach desk, class view and the tutor drawer on :8361 and reports 0 console errors and 0 failed requests
ShipRegistry, HOME.md, cockpit :6798 under pm2, memory updatedBuilttools/check_registry.py passes with 8361 and 6798 present; the cockpit's five tabs render with 0 console errors and the architecture boxes reflect task state
ShipCard on OC Apps: apps.json entry, mark, rewrite, measured runs-on, rebuilt and deployedBuiltoc-apps/tools/check_apps.py --probe says schema ok and abhyas live; the hub's walk passes with four cards; https://oc-apps.ajaykongala.online/abhyas/ serves the betadoc

Roadmap

What comes after the demo, in the order a coaching centre would ask for it.

Deployment

Two lanes, one codebase, no code changes between them. The twin is flipped with environment variables alone.

On-premises

Runs on the centre's PC or a small Linux box under a process manager. One SQLite file is the whole database; back it up by copying it. Model keys are optional and stay on that machine.

uv venv .venv && uv pip install -r requirements.txt
pm2 start ecosystem.config.js   # :8361
# optional: ANTHROPIC_API_KEY or OPENROUTER_API_KEY in .env

Cloud twin (this site)

The same app on a serverless host behind a PIN gate, with an ephemeral database that reseeds itself on every cold start. Nothing to install, up around the clock, and safe to hand to a prospect because nothing persists.

ABHYAS_DB=/tmp/abhyas.db  ABHYAS_SEED=1
BETADOC_PIN=…  FEED_TOKEN=…
vercel --prod   # betadoc at /, app behind /demo

Open the desk.

Synthetic learners, real workflows. Read a lesson, take the quiz, rate a card, and watch the teacher's radar move.