A comprehensive rubric creation and grading tool built with React and TypeScript — self-hostable with full functionality, and offline-capable (with reduced capabilities) when no backend is configured. Designed for educators who need to design complex rubrics, grade students efficiently, and analyse performance — including language proficiency tracking aligned to the Common European Framework of Reference (CEFR).
🚀 Try it now — the live demo runs entirely in your browser. No account, no install, no backend.
- Quick start
- How to read this README
- Features
- Screenshots
- Documentation
- Development 🧑💻
- Tech stack 🧑💻
- Deployment
- Developer reference 🧑💻
- Contributing & security 🧑💻
Pick the path that fits:
| You want to… | Do this |
|---|---|
| Try it immediately | Open the live demo — data stays in your browser |
| Run it locally (no backend) | The three commands below; everything works in localStorage |
| Run the full stack (Docker + Supabase sync) | Jump to the Docker quick start |
Run locally (3 commands):
git clone https://github.com/NesiciCoding/RubricMaker.git
cd RubricMaker
npm install
npm run devOpen http://localhost:5173. No backend or account is required — without one, all data lives in the browser's local storage.
This document is the landing page, not the manual. Everything below is organised by audience — start where you belong and follow the links for depth.
| Reader | Start with |
|---|---|
| Teachers & educators — you want to use the app | Features, then the user documentation wiki |
| Students — you use the student portal | Student Portal wiki page |
| School IT / admins — you deploy and operate it | Deployment → Installation wiki |
| Developers — you build or contribute to the code | Development → Tech stack → Developer reference → Contributing |
Sections marked 🧑💻 in the contents are written for developers — if you're here to use Rubric Maker, you can skip them entirely.
The full, illustrated feature documentation lives in the Features wiki page (with screenshots). Here is the one-glance overview:
| Area | What you can do |
|---|---|
| Classes & cohorts | Classes carry one or more school years and Dutch VO tracks, so a mixed group (groep 7 & 8, or VMBO-TL/HAVO together) is a single class; cohort chips filter the combined roster, and each student can be pinned to one of their class's own years and tracks — CEFR targets, mastery targets and cohort calibration then follow that tag. |
| Rubric builder | Flexible criteria and performance levels; three scoring modes (total points, weighted percentage, single-point); tiered rubrics that put several tiers (school year + optional VO track, e.g. Mavo/Havo/VWO) side by side with shared levels, a statement checklist per level × tier, requirements and a points-based verdict, each student graded in their own tier, comparative grading paired within a tier or across tiers, import of tiered sheets from Word or PDF, and PDF/DOCX exports and previews in the layout of the paper sheet; sub-items as scored components or as an evidence checklist you tick while grading (with a best-fit level suggestion), point ranges and score modifiers; link criteria to CCSS/NGSS/Dutch kerndoelen standards, CEFR Can-Do descriptors, IB Learner Profile, Bloom's Taxonomy or the SLO kerndoelen for English (primary and secondary, with a per-kerndoel Statistics chart); grammar tagging; automatic versioning with restore. |
| Folders | Separate folder trees on the Rubrics, Tests, Essays and Flashcards lists, nesting up to four levels deep. The folder bar shows one level with a breadcrumb (“New folder” becomes “New subfolder” inside a folder). Drag a card onto a folder tile to file it; rubric and test cards and folder tiles also have a “Move to…” menu, and filed rubric and test cards show their folder path. Deleting a folder keeps its items — they move back into the full list — and moves its subfolders up one level. |
| Tests & quizzes | Nineteen question types (multiple-choice, multiple-response, true/false, short answer, open, fill-the-gap, word-bank fill-in with draggable tiles, dictation (deterministic word-level scoring), key word transformation (word limit, 2 marked parts), word-formation gaps with derived-form suggestions, error correction (click-to-mark fragments with accepted corrections; on Supabase-hosted assignments the corrections are withheld from students, while offline share-code tests include the answer key), sentence builder (shuffled word tiles, several accepted orders), grid of statements × shared options, matching, ordering, categorize, hot text, audio-response, …); rich-text prompts with inline images (drag the corner handles to resize) and click-to-insert gap pills; deterministic cloze-gap generators (fixed-ratio, C-test, part-of-speech, above-CEFR-level, academic vocabulary); a passage-level check against the class's target CEFR level with one-click pre-teaching decks; generate matching/multiple-choice/cloze/categorize questions straight from a flashcard deck; listening controls (play limits logged as proctor events, browser-voice stimuli, practice-mode transcripts, per-option audio) and speaking prep time with a cue card; rubric-scored writing/speaking tasks (per-criterion scoring mapped to question points, CEFR per criterion) with teacher-only writing insights; opt-in tolerant answer matching (punctuation, contractions, British/American spelling, minor slips) and distractor suggestions; per-question standards/CEFR links; class assignments with unique share links, due dates, optional Safe Exam Browser, and grade scales; per-student read-aloud accommodation for passages, prompts and options in every test mode, including graded ones; chip/card-based visual editors for error correction, key word transformation, word-bank fill-in and sentence builder (raw syntax stays one click away via "Edit as text"); and a live A4 preview pane in the test builder with an answer-key toggle, point values, print, and an editable header, instructions and footer. |
| Score sheet & test quality | Spreadsheet-style grid (students × questions) to type or paste the points earned per question, for any test or a quick-created score-only paper test whose columns can be linked to standards, CEFR and grammar descriptors; absent students; per-test grading norm — percentage → grade scale, Dutch N-term (CvTE rules), pass mark/caesura, or a points → grade table with a column per school year/track whose cells hold 1–10 grades or labels such as IO/B/E (with a per-column "sufficient from" label) (picked from each student's year/track, or every column shown and labelled when it can't be); norm grades flow into the results page and Magister/SOMtoday exports; analysis tab with mean/SD/median, Cronbach's α (KR-20), SEM, and — for numeric 1–10 norms — grade distribution and pass rate (a percentage distribution under a grade scale) and per-question P-value, Rit, Rir, upper/lower-27% discrimination and α-if-deleted with flags. |
| Practice & placement | Practice mode (listening, reading, grammar) that never affects graded results; three adaptive placement engines — multistage routing, staircase (A2→C2 ladder), and a generator that pulls questions live from your question bank (filtered by CEFR range, skill — the five CEFR skills plus grammar — and tag, and drawing whole reading/listening section bundles as well as standalone questions) with Elo ordering — each producing a provisional CEFR estimate. |
| Grading | Click/level/slider scoring in a card or compact grid layout; keyboard shortcuts (1–5, A+1, …); score modifiers with a reason; voice-graded comments; a taggable comment bank with smart suggestions; scan handwritten answers with the device camera or an imported photo/PDF and OCR them on-device (Tesseract.js) straight into feedback; scan a paper exam's filled-in CITO-style answer sheet back in — drop in a whole stack of photos or a multi-page PDF, assign pages to students by QR code or printed name, read multiple-choice/true-false bubbles by mark detection and short-answer/numeric/open text by per-question OCR across every page, then review and save every student in one action; group grading, comparative side-by-side grading (it loads and updates each student's existing grade rather than adding another, flags students who already have duplicate grade records so the extras can be moved to the archive, and keeps a per-rubric, per-student comparison cap that persists across reloads and devices so a class can't be compared forever), peer review with reviewer analytics, and self-assessment; co-grading with an independent second marking and a moderation queue for disputes. |
| Essays | Standalone essay assignments with prompts, rubric links, word/time limits and live monitoring; a distraction-free student focus mode; rich-text TipTap editor; anonymous submission codes; OCR (Tesseract.js) and DOCX parsing for uploaded documents; inline anchored comments on submissions; deterministic (no-AI) writing statistics in the grading view — sentence-length variance, transition-word frequency, Flesch-Kincaid — plus a CEFR-J grammar-range panel (a range indicator, not a grade) driven by a UD dependency parse (udpipe-wasm; the CC BY-NC-SA model is downloaded separately, see public/models/README.md). |
| Flashcards | Anki-style decks scheduled with the FSRS spaced-repetition algorithm; vocabulary decks with phonetic/part-of-speech fields and grammar decks that feed grammar practice; import from CSV/XLSX/DOCX/TXT; assign to classes and inspect per-student learning insights; students can create their own decks and share them read-only with a teacher. |
| CEFR & language assessment | Per-student and whole-class proficiency overviews (Reading, Writing, Speaking, Listening) benchmarked against track/year expected ranges; structured speaking sessions; rule-based learning paths and intervention flags; a grammar mastery profile merging test, flashcard and writing evidence; optional Cambridge English exam mapping. |
| Vocabulary profiling | A rule-based (no-AI) CEFR vocabulary profiler over analysed documents: per-class/student level distribution, off-list share and academic-word (AWL/NAWL) coverage; a "right for my class?" target-level verdict on any text; export word lists by band or seed a flashcard deck from them. |
| Student portal | Unique shareable link per student (no login needed): grades, teacher comments and files, a combined to-do list, essays and tests, flashcard study, self-assessment, portal search, messaging with the teacher, and an interface-language picker (EN/NL/FR/DE/ES). |
| Analytics | Home dashboard opens with a Student Snapshot — one row per student with CEFR level, Grammar mastery, Vocabulary mastery, and a needs-attention flag, class-filterable by chips and colored on a configurable scale; statistics dashboard with grade distributions, per-criterion breakdowns, multi-class comparison with an insights panel, progress trends that combine rubric grades and graded test results, a Customise panel to choose which cards are shown, and an advanced query builder (filter, group and aggregate every grade — e.g. students averaging below a pass or whose scores dropped — with saved queries and CSV export); an activity dashboard (rubric × test × essay × class grid); vocabulary profile dashboards; per-student profiles with portfolios; rule-based writing trends (weak or declining rubric criteria across graded essays) per student and per class; overdue tracking. |
| Export | PDF reports (individual or class); Word (.docx) styled by rubric-table and style templates; CSV with ready-made Magister/SOMtoday gradebook presets; aggregated period reports; per-student report cards; essay exports (Markdown/DOCX/PDF); calendar (.ics) exports; CITO-style printable exam export (question booklet, answer sheet, attachment, grading sheet) as PDF or Word. |
| Collaboration | A school marketplace to publish/clone rubrics, tests, decks and bank items; live sharing of rubrics and tests (and whole folders) with co-teachers or the whole school, view-only or editable, with a per-teacher default and a teacher-filterable admin view; class sharing with co-teachers (view-only or view & grade) from a class's options menu on the Students page — "view & grade" co-teachers grade the class with its rubrics and tests, change any grade or test result in it, and assign the owner's tests, while the class's student roster (name, class, track, school year) stays editable by the owner only; live test/essay monitoring with proctoring flags; batch grading-task assignment to colleagues; student messaging and a notification center. |
| Accessibility | WCAG 2.1 AA with AAA (7:1) colour contrast, gated by axe-core in CI; dyslexia-friendly reading mode; six theme bundles; in-app Joyride tours; global search (Ctrl/Cmd+K) with filter tokens. |
Offline-first, cloud-ready: the core grading workflow (rubrics, grading, statistics, flashcards) works fully offline with data in localStorage. Connecting an optional Supabase backend unlocks everything that needs a shared server — cross-device sync, multi-teacher collaboration, the student portal, messaging, notifications and the marketplace. If Supabase is configured but unavailable, the app continues with the reduced-capability localStorage fallback rather than failing.
More illustrated walkthroughs — including manual captures of live-service features — are on the wiki.
User documentation (wiki): Home · Getting started · Features · Tests & Practice · Flashcards · Student Portal · Supabase Sync · FAQ
Deployment & operations (repo docs/):
- HestiaCP setup — shared hosting / cPanel-style VPS
- Virtualmin setup — Virtualmin VPS deployment
- Observability on a HestiaCP subdomain — Loki/Alloy/Grafana behind a dedicated HTTPS subdomain
- Grafana dashboards — what the provisioned dashboards show and how to customize them
- Magister integration — importing students from Magister SIS
- Self-hosting operations — backup/restore, upgrades, resource sizing, pg_cron setup, troubleshooting
- Branch protection & production environment — recommended required CI checks for
mainand required reviewers for production deploys
To run the project locally:
-
Install dependencies:
npm install
-
Start the development server:
npm run dev
-
Open http://localhost:5173 in your browser.
Other useful commands:
npm run check # Full pre-push gate: typecheck + lint + format + unit tests
npm run typecheck # TypeScript check (run before commits)
npm run lint # ESLint
npm run test # Vitest unit tests
npm run coverage # Coverage report
npm run e2e # Playwright end-to-end tests (starts its own dev server, see below)
# Supabase local dev (optional)
npm run db:start # Start local Supabase stack
npm run db:reset # Reset and re-apply all migrations
# Backend load testing (k6) against a local Supabase stack (needs `npm run db:start`).
# Reads Supabase keys automatically; profiles: smoke | load | stress | spike | soak.
npm run loadtest # default 'load' profile (~50 VUs)
npm run loadtest:stress # climb VUs to find the breaking pointPlaywright starts and stops its own dev server, on a port derived from the checkout path: 5173 for the first checkout, a stable neighbour for any extra worktree, so two suites can run side by side without sharing a server. It never attaches to a server it did not start unless you ask it to.
| Variable | Effect |
|---|---|
E2E_PORT |
Use this port instead of the derived one |
E2E_BASE_URL |
Test an already-running origin (a preview deployment, a remote stack) and start no server |
E2E_REUSE_SERVER |
1 attaches to a server already on the port — only if it serves this checkout |
Every dev server advertises the checkout it serves in an x-rm-checkout header, and the suite
refuses to run when the stamp doesn't match. That is deliberate: a fixed port meant a second
worktree's dev server could answer instead, and the whole run then passed against different code.
New to the codebase? See the Development Guide and Architecture wiki pages.
| Layer | Choice |
|---|---|
| UI | React 19 + TypeScript, built with Vite 8 |
| Styling | Plain CSS + Radix UI primitives |
| Rich-text editing | TipTap (ProseMirror) |
| Backend & sync | Supabase (optional — Postgres, auth, storage, realtime, edge functions) |
| Spaced repetition | ts-fsrs (FSRS algorithm) |
| Unit testing | Vitest |
| End-to-end testing | Playwright |
| CI / CD | GitHub Actions (CI, CodeQL, Lighthouse, e2e, deployments) |
RubricMaker works in two modes:
- Offline-only — data lives in the browser's local storage. No server needed. Works on GitHub Pages, SharePoint, or any static host.
- With database sync — add an optional Supabase backend for multi-device sync, email login, and rubric sharing between teachers. Hosted on your own infrastructure.
Deploying for a school? Read the Installation wiki page first — it walks through the choices end to end.
The easiest way to run the full stack. Requires Docker.
Your own laptop or school LAN:
./scripts/generate-docker-secrets.sh # creates .env with random secrets (needs openssl)
docker-compose up -d --buildOpen http://localhost:8080. To make it accessible to other teachers on the network, set SITE_URL=http://<your-ip>:8080 in .env first.
docker-compose.yml no longer ships default secrets: it refuses to start until POSTGRES_PASSWORD, JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY and JWT_JWKS are set. Upgrading an existing installation: keep your current .env and run the script once. It only fills values that are empty (for an old .env that is JWT_JWKS, derived from your existing JWT_SECRET) and never changes the rest. If your .env still contains the old public demo values the script warns about it; rotating them on an existing database needs extra steps (the Postgres roles keep the password they were created with), so do not expose such an installation to the internet. POSTGRES_PASSWORD is applied to every database role (including the auth, REST and storage roles, via docker/db/roles.sql) only when the database volume is first created. To change it later — or to repair an older installation whose auth/REST/storage services cannot log in — update .env, then run docker compose up -d db, docker compose exec -e PGHOST=127.0.0.1 db psql -U supabase_admin -d postgres -f /docker-entrypoint-initdb.d/init-scripts/99-roles.sql and docker compose up -d --force-recreate.
VPS with a domain name (HTTPS):
./scripts/generate-docker-secrets.sh # random JWT_SECRET, POSTGRES_PASSWORD and API keys
# Edit .env:
# DOMAIN=rubricmaker.school.nl
# SITE_URL=https://rubricmaker.school.nl
docker-compose --profile https up -d --buildCaddy obtains a free Let's Encrypt certificate automatically. Open ports 80 and 443 on your firewall.
Enabling email login (OTP):
Without SMTP, teachers log in anonymously. To allow email-linked accounts:
# In .env:
MAILER_AUTOCONFIRM=false
SMTP_HOST=smtp.office365.com # or smtp.gmail.com, smtp-relay.brevo.com
SMTP_USER=rubricmaker@school.nl
SMTP_PASS=your-app-password
docker-compose up -d --force-recreate authTeachers receive an 8-digit sign-in code by email. The bundled GoTrue config sends code-only Dutch + English templates (public/email-templates/, one per GoTrue email type) with no clickable confirmation link — some email security scanners (e.g. Microsoft Safe Links) automatically open links in incoming mail, which would consume the one-time token before the teacher can enter the code, causing "Token has expired or is invalid" errors. On the official Supabase stack, use deploy/supabase/docker-compose.override.yml — see docs/SELF_HOSTING_OPS.md → Email.
Student login without email: many schools' spam filters block or delay Supabase's default OTP sender, leaving students unable to sign in. As an alternative, a teacher can generate a password for any student with an email on file (Students page → key icon on that student's row) and share it with them directly — the student then signs in at the landing page with "Student login (password)". This depends on the set-student-password edge function, which requires a functions runtime in front of an API gateway — this repo's own docker-compose.yml does not include one, so this feature (and the DB-backed essay submission flow, which relies on submit-essay/get-essay-assignment the same way) only works when Supabase is provided by the official self-hosted Supabase Docker stack (which ships a functions container behind Kong) or Supabase Cloud. On the official stack, deploy by copying the function's index.ts straight into that stack's volumes/functions/set-student-password/index.ts — there's no separate "deploy" step; the edge-runtime serves it as soon as the file is in place.
Backup and restore:
./scripts/backup.sh # saves to ./backups/YYYYMMDD_HHMMSS/
./scripts/restore.sh backups/20260515_120000Backups hold data only; the restore target must be a running stack migrated to at least the same version (docker-compose up -d runs the migrations). The restore runs in one transaction and stops on the first error. See docs/SELF_HOSTING_OPS.md.
Off-site backups and restore drills: a backup on the same server as the database is lost with it. Configure .env.backup (from .env.backup.example) with any S3-compatible bucket at a different provider (EU: Hetzner, Scaleway, OVHcloud, IONOS; or Cloudflare R2, Backblaze B2, Wasabi, …), then schedule ./scripts/scheduled-backup.sh nightly: it runs backup.sh, uploads the result encrypted on the server (rclone crypt, so the provider never sees student data), verifies the upload, prunes old copies and pings an optional heartbeat URL. ./scripts/restore-drill.sh (monthly) restores the newest off-site copy into a throwaway Compose project and checks every table's row count and every uploaded file against the dump. ./scripts/offsite-backup.sh pull latest fetches a copy for a real restore. See docs/SELF_HOSTING_OPS.md.
Moving from Supabase Cloud: ./scripts/export-cloud.sh (on your laptop) dumps users, rows and every Storage file; ./scripts/import-cloud.sh <export-dir> (on the server) loads them, compares file counts per bucket, and checks that each attachment, export template, essay, recording and scan row finds its file. See docs/SELF_HOSTING_OPS.md.
Updating to a new version:
git pull
docker-compose up -d --build # rebuilds the app image, restarts services
# Migrations run automatically on next startupNo database sync — all data stays in the browser. Works on any static host.
Build:
npm run build # output in dist/Deploy the dist/ folder to GitHub Pages, Vercel, Netlify, or any web server.
SharePoint:
- Run
npm run build - In
dist/, renameindex.html→index.aspx - Upload the entire
dist/folder to a SharePoint Document Library - Click
index.aspxto launch
For Standards Integration on SharePoint, add the SharePoint domain to your Common Standards Project API key's allowed origins.
The deep operational detail is in the self-hosting docs. Highlights:
- Nightly attachment cleanup (recommended): attachment files and their DB rows are deleted automatically when they age past the owner's school retention period (default: 7 years). Handwriting scans are removed by the same job once they are older than the current academic year. Schedule the bundled script with
crontab -e:0 2 * * * cd /path/to/rubricmaker && ./scripts/delete-old-attachments.sh >> /var/log/rubricmaker-cleanup.log 2>&1 || logger -t rubricmaker-cleanup 'attachment cleanup FAILED - see /var/log/rubricmaker-cleanup.log'. The script uses the Storage HTTP API (it does not delete rows directly fromstorage.objects, which Supabase blocks) and treats a 404 from storage as success so orphaned DB rows are always removed. It reaches storage throughSITE_URL(defaulthttp://localhost:8080, the compose stack's port) and exits non-zero when any storage delete fails. Because the example sends its output to a log, cron itself mails nothing — the|| loggeralert is what surfaces a failed run (drop the redirect instead if you would rather have cron mail the output). It validates every attachment id and path before using it, so keep your copy of the script up to date. On Supabase Cloud, schedule thedelete-old-attachmentsedge function instead (Supabase Dashboard → Edge Functions). - Nightly cloud backup (recommended for Supabase Cloud / the official self-hosted stack):
scripts/backup.sh(and the off-sitescripts/scheduled-backup.shbuilt on it) only works against this repo's owndocker-compose.yml(it runspg_dumpinside the Composedbservice and derives the uploads volume from the Compose project name, refusing to run if that volume does not exist). For Cloud or a separately self-hosted Supabase, use thenightly-backupedge function instead: it dumps each teacher/admin's rows viapublic.export_owner_backup()and uploads a JSON snapshot to the privatebackupsStorage bucket (7 most recent per user), scheduled via Supabase Dashboard → Edge Functions or apg_cron+pg_netjob. Note this is a disaster-recovery snapshot of raw table rows — restoring means re-inserting the rows directly (e.g. viapsql), not importing through the app's own Settings → Backup & Restore; and it's metadata only, so back up theessays/recordings/attachmentsbuckets separately if you need the files themselves recoverable. Its snapshots also stay in the same Storage as the database, so they are not an off-site copy. - Teacher email digest (optional,
pg_cron-driven): thescheduled-digestedge function emails opted-in teachers nightly about pending moderation disputes, overdue grading, or unread student messages (same three sources as the in-app Notification Center). It needs a functions runtime plus one-time per-deployment settings (app.settings.project_url/app.settings.service_role_keyviaALTER DATABASE …) before it can send anything — see docs/SELF_HOSTING_OPS.md for the fullpg_cron/pg_netsetup. - Stress-test logging (optional): set
VITE_STRESS_TEST_LOGGING=truein.env(requires migrations035_client_logs.sqland072_client_logs_observability.sql, included withdb:reset/docker-compose db_migrate) to log user actions, sync results, JS errors, pageviews and Web Vitals (LCP/FCP/FID/CLS + page load) to aclient_logstable — never free-text content. Unset and rebuild after the stress-test window. - Observability (optional): a standalone Loki + Prometheus + Alloy + Grafana stack (
docker-compose -f docker-compose.observability.yml --env-file .env.observability up -d, Grafana onhttp://localhost:3001) filters web-server and container logs, collects host, per-container and Postgres metrics (disk, memory, connections, cache hit ratio), can queryclient_logsand the whole Supabase schema via a Postgres datasource, and alerts by email (or a chat webhook) on a filling disk, exhausted Postgres connections, failed scheduled jobs and 5xx bursts. Scheduled jobs also ping optional dead-man's-switch heartbeat URLs, and an external uptime check covers the server itself; see Grafana dashboards → Alerting. SetGRAFANA_ADMIN_PASSWORDto a strong value in.env.observability— the stack refuses to start without it (no default credentials). See Observability on a HestiaCP subdomain and Grafana dashboards for remote HTTPS access and what each dashboard shows. - Backend load testing (optional): the k6 harness (
npm run loadtest, seek6/README.md) drives configurable virtual-user load at the Supabase backend. Run it against a local stack for regression smoke, or against a VM sized to a school's server to find its concurrency ceiling — docs/LOAD_TESTING_STAGING.md covers the staging-VM workflow and per-school sizing.
🧑💻 This section is for contributors — teachers and admins can stop here.
| Path | Page |
|---|---|
/ |
Dashboard |
/rubrics |
Rubric list |
/rubrics/new |
New rubric |
/rubrics/:id |
Rubric builder |
/rubrics/:rubricId/grade/:studentId |
Grade a student |
/rubrics/:rubricId/peer-review/:studentId |
Peer review view |
/peer-analytics/:rubricId |
Peer review analytics (consistency, feedback heatmap, reviewer trends) |
/rubrics/:rubricId/self-assess/:studentId |
Student self-assessment |
/essays |
Essay list |
/essays/new |
New essay |
/essays/:teacherKey |
Essay builder (prompt, rubric link, assign students, import submissions) |
/essays/:assignmentId/monitor |
Live essay monitor (presence, live word counts, draft preview) |
/speaking/:rubricId/:studentId |
Speaking session |
/grade-comparative/:classId/:rubricId |
Comparative grading |
/marketplace |
School marketplace for rubrics, tests, flashcard decks, and question bank items (browse, publish, clone, upvote) |
/tests |
Test list |
/tests/new |
New test |
/tests/:id |
Test builder |
/tests/:testId/results/:studentTestId |
Test results, manual grading, and class-average adjustment |
/tests/:testId/monitor |
Live test monitor (presence, response grid, proctoring flags) |
/tests/:id/scores |
Score sheet: type in points per question per student, per-test grading norm, and test quality analysis (P-value, Rit/Rir, Cronbach's α) |
/students |
Students list |
/students/:id |
Student profile |
/students/:id/cefr-overview |
Per-student CEFR overview |
/students/:id/learning-path |
Per-student learning path — rule-based rubric recommendations and intervention flags |
/cefr-overview |
Whole-class CEFR overview |
/vocabulary |
Vocabulary Insights hub (CEFR vocabulary distribution per class/student, cross-deck flashcard progress, pasted/uploaded text screening, CSV export) |
/flashcards |
Flashcard deck list |
/flashcards/:id |
Flashcard deck editor — cards, CSV/XLSX/DOCX import, class assignment, per-student insights |
/news-flashes |
News flashes — curate articles/books/videos to share with students |
/portal/:studentId |
Student portal (public) — grades, to-do list, self-assessment, pending co-grading moderation notices, learning-path/grammar recommendations |
/portal/:studentId/flashcards/:deckId |
Student flashcard study session (spaced repetition) |
/feedback/:code |
Student feedback view (public, no login — decodes a shared grade-summary link) |
/preview/:code |
Rubric preview (public, no login — decodes a shared rubric link, no student data) |
/essay/:code |
Essay writing session (public, no login — decodes a shared essay-assignment link) |
/test/:code |
Take a test (public, no login — answer questions, optional timer, submit) |
/attachments |
Attachment manager |
/comments |
Comment bank |
/question-bank |
Question bank — reusable test questions, insertable into any test |
/statistics |
Statistics dashboard (by-rubric, by-student, multi-class compare with insights, advanced query builder) |
/activity-dashboard |
Activity Dashboard — rubric/test/essay × class grid with link/assign/reorder actions, pending grading-task list |
/moderation |
Moderation queue — disputed co-graded submissions, per-criterion delta, keep/accept resolution |
/messages |
Messages inbox — reply to or start a thread with a portal-authenticated student |
/notifications |
Notification Center — full, filterable history of overdue grading, unread messages, and pending moderation reviews, with cross-device dismissals |
/export |
Export page |
/settings |
Settings |
/admin |
Admin panel (admin role only) |
/privacy |
Privacy statement |
| File | Purpose |
|---|---|
src/utils/gradeCalc.ts |
Score aggregation and weighted scoring engine |
src/utils/gradeSnapshot.ts |
Grade-scale comparison and carrying a grade's entries over to a newer rubric version (regrade on latest) |
src/utils/subItems.ts |
Sub-item rules shared by scoring, grading and exports: points vs checklist mode, ranges and legacy ticks, the score breakdown, best-fit level from ticks, list pasting |
src/utils/tieredRubric.ts |
Tiered rubrics (one column per school year/track, shared bands as levels): band presets and verdict cutoffs, validation, deriving levels from bands, a student's default tier, the final verdict with requirement caps and overrides, and checks on imported configs |
src/utils/tieredSheet.ts |
A tiered rubric as its paper sheet (requirements, tier columns, bands with statements; a grade's tier, ticks, chosen bands and verdict), rendered by tieredSheetHtml.ts (PDF/print), tieredSheetDocx.ts (DOCX) and TieredSheetView (previews) |
src/utils/import/sheetImport.ts |
Recognises tiered rubric sheets in DOCX (mammoth HTML, merged cells resolved) and PDF (text positions): tier columns, band codes, statements, requirements, criterion names, track/year mapping and broken-ligature repair |
src/utils/rubricWeights.ts |
Even criterion-weight split for the Rubric Builder, and the check that keeps an untouched split at 100% as criteria are added or removed |
src/utils/csvDownload.ts |
Shared CSV download (UTF-8 BOM so Excel keeps accented and non-Latin names), ISO YYYY-MM-DD dates, and collision-free column labels for repeated or blank criterion titles |
src/utils/studentValidation.ts |
Add-student checks (loose email format, same name already in the class) and the students CSV export rows (name, email, class, student number) |
src/utils/settingsDefaults.ts |
DEFAULT_SETTINGS and withSettingsDefaults, which fills fields an older backup or cloud settings row lacks (including inside defaultFormat) wherever settings enter state |
src/utils/rubricClone.ts |
Copies a rubric for Duplicate/Differentiate: fresh nested ids (vocabulary links remapped), no list position, sharing or attachments |
src/utils/testClone.ts |
Copies a test for Duplicate: fresh question/option ids, no list position or sharing |
src/utils/materialSharing.ts |
Resolves a rubric/test/folder's effective sharing (own → folder chain → teacher default → private, mirroring migration 095) and groups colleagues' shared material per teacher |
src/utils/hydratedGradeMerge.ts |
Lays edits made before a saved grade finished loading over that saved record, so untouched scores survive |
src/utils/attachmentLimits.ts |
Per-file attachment size limits (2 MB in local mode, 50 MB with a database) and file-size formatting |
src/utils/comparativeMatchups.ts |
Comparative-grading pairing logic: per-student matchup counts and next-pair selection against the persisted, per-rubric matchup history (Rubric.comparativeMatchupLimit) |
src/utils/folders.ts |
Folder helpers for all four lists: ALL_FOLDERS, MAX_FOLDER_DEPTH, ordered folders per level (childFolders), paths (folderPath/folderPathLabel, folderDepth), descendantFolderIds, countInFolderTree, canNestFolder, nextFolderOrder per parent |
src/utils/cefrStudentAggregator.ts |
CEFR level computation across assessments |
src/utils/cefrVocabularyProfiler.ts |
CEFR vocabulary profiler over a validated 64k-word index (src/data/cefrLevels.ts): level distribution, estimated level, off-list share, academic coverage; builds/reads the persisted per-document profile |
src/utils/academicWordList.ts |
Academic Word List (AWL) + NAWL membership and coverage over a token list |
src/utils/flashcardOverview.ts |
Cross-deck flashcard progress for the Vocabulary Insights hub: sums computeDeckInsights over each deck's assigned students |
src/utils/textLevelVerdict.ts |
Target-level "right for my class?" verdict: coverage %, above-target words, and a suitable/slightly-above/too-hard label, from a text or an aggregated distribution |
src/utils/vocabProfileAggregator.ts |
Per-student and per-class CEFR vocabulary aggregation (level distribution, off-list and AWL/NAWL shares) and the vocabulary-list CSV export |
src/utils/placementRouting.ts |
Section-routing engine for staged placement tests (threshold scoring, next-section resolution, cycle detection) |
src/utils/placementResult.ts |
Deterministic provisional CEFR estimate from a completed placement test's section path |
src/utils/placementStaircase.ts |
Staircase (adaptive-ladder) placement engine: level progression, next-question resolution, and item-level Elo self-calibration/rating bands per CEFR level |
src/utils/placementGenerator.ts |
Types/predicate for the Generator placement engine, which pulls questions live from the question bank at runtime via the next-placement-question edge function instead of a pre-authored pool |
src/utils/eloProgressAggregator.ts |
Builds a student's Elo-scaled CEFR progression across placement-test attempts, for the Statistics placement-progress chart |
src/utils/learningGoalsAggregator.ts |
Learning goal progress tracking |
src/utils/docxExport.ts |
DOCX generation via docx library |
src/utils/docxTemplateExport.ts |
Blank-rubric Word export styled by a rubric table template (level headers + header shading read from the template's XML) |
src/utils/exportDataPrep.ts |
Shared filename sanitization, points-range formatting, and HTML-comment stripping used by the DOCX/PDF export paths |
src/utils/docxStyleTemplate.ts |
Extracts heading/body font from an uploaded .docx for essay/period-report style templates |
src/utils/pdfExport.ts |
PDF report generation |
src/utils/testExamContent.ts |
Format-agnostic content/geometry for the CITO-style exam export: section grouping/numbering, partial-credit ladder derivation, hot-text passage rendering, and answer-sheet bubble-grid/fiducial-marker geometry |
src/utils/testExamExportHtml.ts / testExamExportDocx.ts |
Renders the exam booklet, attachment, answer sheet, and grading sheet — and the test builder's live A4 preview (buildTestPreviewSections/buildTestPreviewHtml) — as PDF (print-to-PDF HTML) and DOCX, from testExamContent.ts |
src/utils/textExtraction.ts |
OCR (Tesseract) + DOCX parsing (Mammoth); also DOCX→HTML conversion for the read-only grading document view, and recognizeImage (rich OCR result: text + per-word confidence) for the handwriting-scan flow |
src/utils/preprocessScan.ts |
Pure RGBA scan-preprocessing primitives (grayscale, contrast stretch, Otsu threshold, projection-profile deskew, bilinear resize/upscale) |
src/utils/documentCrop.ts |
Corner ordering and bilinear perspective-unwarp for cropping a document quad out of a scan photo |
src/utils/scanImport.ts |
Turns picked photos / scanned PDFs into OCR-ready image blobs (pdf.js page rasterization with memory guardrails) |
src/utils/ocrConfig.ts / ocrLanguage.ts |
Tesseract engine config (OEM/PSM from a capture hint, user-words/patterns) and UI-locale→Tesseract language routing |
src/utils/ocrReview.ts |
Per-word low-confidence annotation and confidence banding for the scan review UI |
src/utils/scanRecord.ts |
Builds the persisted Scan record on review confirm and the discard-after-OCR decision |
src/utils/scanRetention.ts |
One-academic-year retention: finds and sweeps scans past the cap (client counterpart to the server get_overdue_scans()) |
src/services/scanStore.ts |
Scan image blobs in the shared rm_media IndexedDB store, namespaced by a scan_ id prefix |
src/services/database/ScanSync.ts |
Cloud sync for scans (blob upload + metadata upsert to the scans bucket / scan_metadata table, signed-URL read), mirroring RecordingSync |
src/utils/examScanRegions.ts |
Maps a CITO exam answer sheet's answerSheetGeometry() mm boxes onto scanned-page pixel regions, and detects marked multiple-choice/true-false bubbles by ink coverage (OMR, not OCR) |
src/utils/examScanAnswers.ts |
Reads one printed page of a scanned answer sheet into draft TestAnswers (bubble detection or per-region OCR) and merges a student's pages in printed question order |
src/utils/examScanBatch.ts |
Batch page→student grouping, unassigned-page detection and scanned-vs-printed page-count checks |
src/utils/examScanQr.ts |
Decodes and validates the QR in an answer sheet's scan-marker band (test, student, printed page count) for automatic page assignment |
src/utils/examScanStudentMatch.ts |
OCR name-region fallback that matches a printed candidate name against the roster when no QR is readable |
src/utils/examScanVocabulary.ts |
Seeds the OCR user_words dictionary from a test's short-answer model answers |
src/utils/rgbaImageCanvas.ts |
Canvas glue between a captured photo (Blob/data URL) and the pure RgbaImage pixel pipeline |
src/utils/essayShareCode.ts |
Shareable codes for essay access (no auth needed) |
src/utils/pinHash.ts |
PIN hashing for student self-assessment locks |
src/utils/clozeParse.ts |
Cloze/hot-text rendering helpers; re-exports the gap and fragment parsers from supabase/functions/_shared/testScoring.ts |
src/utils/questionTypeKey.ts |
i18n keys for a question type's label and help popover (every hyphen in the type becomes _) |
src/utils/userRole.ts |
resolveUserRole: the one role the Settings badge, sidebar and route guards act on (legacy user → teacher, unset → local owner/admin) |
src/hooks/useActiveClassCohorts.ts |
Cohort-chip selection kept two-way in sync with the topbar class selector (settings.activeClassId; one class or unset = all) |
src/utils/pendingGrading.ts |
Overdue grading: handed-in tests and essays still ungraded past the threshold, grouped per test/essay (mirrored server-side by get_overdue_grading_count, migration 086) |
src/utils/clozeGenerators.ts |
Deterministic cloze-gap generators for the test builder: fixed-ratio, C-test, part-of-speech (compromise), above-CEFR-level and academic (AWL/NAWL) vocabulary |
src/components/Tests/LimitedAudio.tsx |
Listening stimulus player with a play limit (file or browser voice) and the per-option audio button |
src/utils/rubricTaskScoring.ts |
Maps a rubric-scored test answer's per-criterion entries onto the question's points (reuses gradeCalc) |
src/utils/wordFormation.ts |
Word-formation authoring aid: derived forms of a stem word (affix candidates that exist in the CEFR index plus compromise inflections) and per-gap stem parsing |
src/utils/distractorSuggestions.ts |
Deterministic distractor suggestions (compromise word forms, EFL confusables, same-level words from the CEFR index) for the test editor |
src/utils/deckToQuestions.ts |
Generates matching/multiple-choice/cloze/categorize test questions from a FlashcardDeck's cards |
supabase/functions/_shared/testScoring.ts |
Single implementation of test auto-scoring and cloze/hot-text parsing, shared by the client (testCalc.ts) and the submit-test / next-placement-question edge functions |
supabase/functions/_shared/placement*.ts |
Single implementation of MST routing and the staircase/generator CEFR ladder with Elo calibration (plus seededShuffle.ts), shared by the client and submit-test / next-placement-question |
supabase/functions/_shared/attachmentPurgeGuard.ts |
Shared eligibility rule for the attachment retention purge (plain ids, file in the owner's own folder); mirrored by scripts/delete-old-attachments.sh |
supabase/functions/_shared/studentPasswordGuard.ts / secureCompare.ts |
Caller/target authorization for set-student-password, and the constant-time secret comparison used by the cron-style functions |
supabase/functions/_shared/smtp.ts / notificationEmails.ts |
Dependency-free SMTP client (STARTTLS/implicit TLS, AUTH PLAIN/LOGIN) and the Dutch + English grade, message and digest emails; _deno/mail.ts connects them to Deno sockets and the SMTP_* / APP_URL env |
src/utils/audioResponseCode.ts |
Encodes/parses the data-URI JSON carried in an audio-response question's TestAnswer.response |
src/utils/testQuestionClone.ts |
Regenerates a test question's id and every nested option/pair/item id, and instantiates a fresh copy of a Question Bank item (single question or reading/listening section bundle) ready to insert into a test |
src/utils/testQuestionIssues.ts |
What keeps a test question from being answered or scored (missing options, correct answer, gaps, model answer, ...) per question type, and whether a test can be assigned; drives the test builder issue list and the Assign block |
src/utils/masteryProfileAggregator.ts |
Merges per-student test accuracy, flashcard FSRS state, and graded-writing scores into one row per grammar item, plus a blended 0–100 grammar mastery score for the Dashboard's Student Snapshot |
src/utils/masteryColorScale.ts |
Grammar-mastery cell color for the Dashboard Student Snapshot: teacher-configured Settings.masteryColorBands, falling back to CriterionHeatmap's red→yellow→green pctToColor |
src/utils/learningPathAggregator.ts |
Rule-based rubric recommendations, intervention flagging, and grammar practice recommendations |
src/utils/grammarLanguage.ts |
Picks the LanguageTool language for a document: detects English/Dutch/French/German/Spanish from function words, and uses the teacher's English spelling variant for English |
src/utils/writingTrendAggregator.ts |
Rule-based weak/declining rubric-criterion trends across a student’s graded essays, per student and per class |
src/utils/essayTextStats.ts |
Deterministic essay statistics (sentence-length variance, transition words, Flesch-Kincaid), mirrored from vocabkitchen-CLI analysis.py; parity cases in sync/writing-fixtures.json |
src/utils/grammarProfile.ts |
CEFR-J grammar-construction detectors over a UD parse, ported from vocabkitchen-CLI grammar_profile.py; registry in src/data/grammarConstructions.ts |
src/utils/udParse.ts |
CoNLL-U reader and lazy udpipe-wasm loader (model hosted separately) |
src/utils/testSummaryAggregator.ts |
Per-question/per-skill strong-weak test breakdown |
src/utils/testGradingNorm.ts |
Per-test grading norms: points → grade via grade scale, N-term (CvTE), caesura or per-year/track norm table (resolveTestGrade) |
src/utils/testQualityMetrics.ts |
Classical test-theory metrics: P-value, Rit/Rir, Cronbach's α (KR-20), SEM, α-if-deleted |
src/utils/scoreSheet.ts |
Score-sheet cell edits, paste parsing, absent marking and CSV export |
src/utils/reportCardAggregator.ts |
Composes CEFR, learning-goals, and test-summary data into one report card |
src/utils/globalSearch.ts |
Token-aware search (type:/class:/year:/track: filters, student+rubric grading shortcut) across rubrics, tests, students, classes, essays, flashcard decks, and news flashes |
src/utils/portalSearch.ts |
Student-portal search over a student's own graded rubrics, work (tests/essays), and flashcard decks |
src/utils/statsChartPresets.ts |
Recommended chart definitions for the Statistics "Custom Views" gallery |
src/utils/assessmentTimeline.ts |
Merges rubric grades with graded assessment-mode test results for progress trends (class, class comparison, student timeline) |
src/utils/statsQuery.ts |
Advanced Statistics query engine: flattens every grade into records, then filter → group → aggregate (avg, median, pass rate, spread, change) |
src/utils/statsSections.ts |
Catalogue of Statistics cards the teacher can show/hide in the Customise panel |
src/utils/liveStudentRubrics.ts |
One live grade per student and rubric for analytics: drops soft-deleted grades, archived students, deleted rubrics and duplicate records; scopes a rubric's grades to a class |
src/utils/coGradingModerationQueue.ts |
Flags disputed co-graded submissions (delta above threshold) for the Moderation queue |
src/utils/flashcardScheduler.ts |
Thin wrapper around ts-fsrs (FSRS spaced repetition): rating, study queue, interval preview |
src/utils/flashcardImport.ts |
Flashcard import from CSV (papaparse), XLSX (read-excel-file), DOCX (mammoth), and plain text |
src/utils/questionBankImport.ts |
Bulk Question Bank import from a JSON file (plain questions or section bundles, all TestQuestionTypes, CEFR/grammar/standard links, tags, optional Elo rating) |
src/utils/testImport.ts |
Test import from a RubricMaker test JSON file (single test, list, or backup export): fresh ids for sections and questions, per-question validation warnings, and the points, norm and print layout of the file kept as-is |
src/utils/flashcardInsights.ts |
Learner insights per deck: stage counts, due cards, focus words from FSRS state |
src/utils/displayOrder.ts |
Shared sort/reorder helpers for manually-orderable list views |
src/utils/cohortAggregator.ts |
Derives a cohort's student set from current + past class memberships by year/track |
src/utils/gradebookExportPresets.ts |
Per-SIS CSV column presets (Magister, SOMtoday) for the gradebook export |
src/utils/testExportPresets.ts |
Whole-class test results CSV (score, points, grade, status, per-question and per-skill accuracy) and Magister/SOMtoday gradebook rows |
src/utils/studentTestScore.ts |
The score TestResultsPage shows for one submission (path-aware for staged, staircase and generator placement runs), shared with the test exports |
src/utils/icsExport.ts |
Builds a minimal .ics calendar file from assignment deadlines |
src/utils/messageThreads.ts |
Groups flat student/teacher Message rows into threads by student + context |
src/utils/eraseStudent.ts |
Right to erasure: removes a student and every record keyed to them from local state (the client side of erase_student(), migration 084) |
src/utils/backupSettings.ts |
Settings → Backup: keeps role, account email, admin PIN, school and API keys out of exports and restores, and diffs the settings a restore would change |
src/utils/roleChangeError.ts |
Recognises the last-admin error raised by protect_role_changes() so Admin → Users and Onboarding can show a translated message |
src/services/standardsApi.ts |
Common Standards Project API (CCSS, NGSS) |
src/services/wordnetPack.ts |
Optional offline definitions: downloads the Open English WordNet pack (public/wordnet/, built by scripts/build-wordnet-pack.mjs, CC BY 4.0) after confirmation and stores it in IndexedDB |
src/services/wordLookup.ts |
Definition lookup chain (Free Dictionary → offline WordNet) plus keyless MyMemory word translation, used to fill decks seeded from Text screening |
src/services/freeDictionaryApi.ts |
Keyless word lookup (definition, phonetic, part of speech) via the Free Dictionary API (dictionaryapi.dev) for the vocabulary panel |
- Contributing guide — how to report issues, propose changes, and open pull requests
- Development Guide — codebase conventions and workflow
- Roadmap — where the project is heading, tracked in issues and the project board
- Security policy — responsible disclosure
- Privacy statement — what the app collects and stores, and when it stays fully local
License: MIT — free to use, modify and distribute, with attribution. See the LICENSE file for details. Bundled and downloadable word-list data keeps its own licences (notably the NAWL, CC BY-SA 4.0, and Open English WordNet, CC BY 4.0) — see docs/VOCAB_WORDLISTS_PROVENANCE.md.




