- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
Lumos is the platform's learning module. Students browse the shared catalog, enroll in courses, and watch lessons; teachers and admins contribute videos that flow through a review pipeline and stay under the contributor's control — including per-video paid unlocks. Two journeys meet at the lesson page, and this page documents both, then the code layout that implements them.
Governance. The Subject → Chapter → Lesson hierarchy is platform-owned: schools adopt it from the catalog and may request new subjects, but never author structure themselves. A school customizes by hiding/activating chapters, lessons, videos, or a lesson's quiz per school, and contributes at the lesson level — videos, study materials, and quiz questions. Approved lesson materials appear to students in the lesson's Resources section alongside the video.
The acceptance script lives in hogwarts#377 — the same flows as walkable test checklists.
How it flows
The full journey
The student journey (browse → enroll → watch → complete) and the contributor journey (upload → platform review → live → owner control) connect where an approved video lands in a lesson. There is one review pipeline: a school uploads, the platform decides, and the school is notified when the video goes live.
Three invariants hold everywhere:
- An unpaid-for video never has a playable link. The lock icon is cosmetic; the real paywall is that the server emits no video URL until the purchase exists.
- The player is never an empty black box. No video → the marketing story clip plays — but the fallback never replaces a paid video's lock screen, and never writes lesson progress.
- Schools never see each other's private data. School-visibility videos stay inside their school; only content approved as Public/Paid crosses schools, always credited to the uploader. Every video — whatever its audience — is approved by the platform desk, never by a school admin.
Discover & enroll
The catalog page opens as a browse view rather than a wall of cards. First, where the learner left off — a single card carrying the subject, its grade, the chapter, the lesson and the minutes watched, in the same shape the live-classes page gives the class that matters right now; it is simply absent for anyone who has started nothing. Then Recommended for you, a scrolling row of the student's own grade, minus whatever is already on that card. Then More courses, the rest of the catalog as a grid with the grades nearest the student's first, revealed a page at a time. Recommendations exist only for students, because a student's grade is the one thing this catalog knows about a person; every other role goes straight to the grid. Picking a grade badge, or searching, replaces all of it with the plain filtered grid.
The catalog shows only published subjects the school has selected; content a school hid via override never appears for its students. Free courses enroll in one click with no payment UI; paid courses activate only after the Stripe webhook confirms. Free-course lessons are open even without enrolling — paid-course lessons are blocked for non-enrolled students, including by direct link. A school's own students never have to enrol themselves: placing a student in a grade or section, promoting them, or adding a subject to their grade enrols them in that grade's subjects (filtered by stream), and moving grade never takes last year's courses away.
Two ways in, on the same bar. Explore opens a shelf of six real courses with their artwork — scoped to the grade being browsed, one card per subject — above the popular search terms. Typing two characters raises a live list of matching courses, each with its thumbnail, grade and lesson count, so the six near-identical "Mathematics" rows in a K–12 catalog stay distinguishable; arrow keys and Enter open one, and See all results falls through to the full grid. Search is bilingual — a term typed in Arabic finds a course stored in English, and vice-versa — and it reads only the translation cache, so a keystroke never costs a translation API call. The whole catalog is one grade-wide query away: the grade filter applies to browsing, not to search.
Watch a lesson
Pressing Play opens the video fullscreen and starts it there, and leaving fullscreen — the control, the f key, Escape — puts the lesson page back exactly as it was, at the same address, with the poster and its resume pill. On a device that will not grant a real fullscreen (iOS Safari), the player fills the screen itself rather than handing the bare video to the system, so the forensic watermark stays on the picture.
On a phone the player draws a different set of controls to the one a desktop gets: a 44px round close, a picture-in-picture and share pill, and a mute toggle across the top; oversized rewind, play and forward circles centred in the frame; and a bottom block of lesson title, elapsed and remaining clocks either side of a full-width scrubber, and an Up Next capsule. Both menus — the share scopes and the playback speeds — are cards that cover the control that opened them, and the speed list scrolls rather than growing up over the play button. Every measurement is taken from a real Apple TV player, and the layout is verified against it by capture rather than by eye.
A lesson with no videos — or one whose file is dead or undecodable — plays the story fallback clip instead of a black screen. Fallback playback records no progress and never auto-completes (the manual "Mark as Complete" still works). Real videos resume where the student left off, auto-complete on finish, and offer up-next. With multiple instructors, chips switch the player; the school's preferred instructor plays by default, instructors the school has disabled never appear at all, and every video credits its uploader.
The practice quiz
Below the video, a lesson can carry a short practice quiz drawn from the catalog's question bank — multiple choice, true/false, and fill-in-the-blank. A school can hide any lesson's quiz without hiding the lesson.
The quiz is graded on the server, against exactly the questions it showed you. Nothing about the answers reaches the browser until you submit: you pick an option, the server says which one was right and why. Once you submit, each question shows the correct answer with any explanation, and the score appears with it.
Scores reach the school gradebook when the student belongs to a class for that subject, and land there as their own row per lesson. Only the first attempt is recorded — you can retake a practice quiz as often as you like to study, and the quiz says plainly whether the attempt you just finished counted or not. Report cards read the same gradebook, which is why a retakeable practice score is not allowed to keep rewriting it.
Where quiz questions come from
Questions live in the platform catalog and attach to a lesson. A school gets them there two ways:
- Question Bank (Exams → Question Bank) — author a question, pick its subject, then optionally attach it to a chapter and lesson. Attaching it is what puts it in that lesson's practice quiz. The lesson picker offers only what the school actually teaches, minus anything it has hidden, and names read in your own language — the same picker the video upload dialog uses.
- Subject contributions — submitted for platform review, and visible to the school's own lessons once approved. School-authored questions default to school visibility; contributing one to the global catalog is a deliberate choice, not the default.
Contribute a video
Teachers, admins, and developers contribute through the same three-step dialog. Step one walks the catalog's own hierarchy rather than listing it flat, the same way the course catalog does: numbered grade buttons with that grade's subjects beneath them, then the chapters and lessons inside the subject you pick, with a search box for the last stretch. Grade comes first because a subject is defined per grade, so "Mathematics" means nothing on its own. Only what the school actually teaches is on offer — content the school has hidden from its LMS never appears — names read in your own language, picking a lesson moves you straight to the next step, and lessons come back a page at a time. Direct uploads go straight to storage with a progress bar — click to choose or drag the file in, and the file name becomes the title if you haven't typed one; a link that isn't a video is flagged right on the field; wrong types, oversized files, and a full storage quota are refused before any bytes move, and an uploaded-but-unsubmitted file cleans itself up when the dialog is abandoned. Quota counts the file's real size on storage, not what the client claims. Every submission lands as Pending — no role skips review — and the platform team is notified, with the requesting school named in the notification. The school's other admins get an informational copy.
Review & publish
Two desks. The school admin's Review tab approves content that stays inside the school; anything Public or Paid belongs to the platform catalog desk, because it surfaces in every school — a school admin sees Approve disabled with an explanation (and the server refuses regardless), while Reject stays available. Both desks notify the contributor, and a rejection carries its reason into My Videos.
Ownership, control & paid unlocks
The contributor owns the video for good: visibility changes apply instantly, except widening an already-approved video to Public, which resubmits it for platform review (narrowing is always free). Revoke flips to Private and purges the CDN; replace resets approval; delete releases the storage quota. A paid video can't be un-paywalled through the visibility menu — only the explicit remove-paywall action, which warns that buyers aren't refunded. On the buying side, a lone paid video shows an Unlock-with-price pill on the lesson hero, checkout runs through Stripe, and the unlock is per-student with time-limited signed links.
Behind the scenes
Settings tabs are role-gated (teachers see only Videos; admins see all five, with a pending-count badge on Review). The Instructors surface lists every instructor whose videos reach the school's own subjects — the platform library, the school's own teachers, and any partner school's — with the coverage each one has, in lessons. A school can disable an instructor (their videos are then served on no surface, including mobile), star one as the default so their video plays first wherever they have one, or lock Lumos to a single instructor — its own teachers, or anyone it prefers. The lock is deliberately not absolute: on a lesson the locked instructor never covered, the remaining allowed instructors still play, because a lesson with no video records no progress and would put completion out of reach. Content overrides hide catalog items for one school without affecting others; parents get a read-only view of their child's progress; enrollment and completion emails go out via Resend. The whole surface — player, dialogs, review desk, search — follows the school's language, Arabic RTL included.
Certificates. Completing every lesson a school actually shows — hidden chapters and lessons don't count, so hiding content can never put a certificate out of reach — marks the enrollment complete and issues a certificate of completion. It carries the learner, the subject, the school, the date, and a certificate number, reads in the reader's own language, and is reachable from the course page as well as from the completion email. "Download" opens the browser's print dialog, which is also where Save as PDF lives. Certificates today attest to completing the course, not to passing an assessment — quiz and exam scores do not gate issuance.
Protecting school video
A school's lesson video is its own work. Lumos treats the link to that video as the thing worth guarding, because the link is what actually leaks.
Nothing playable in a Lumos page is a storage address. A lesson carries an
opaque reference — /api/lumos/video/<id> for video, /api/lumos/file/<kind>/<id>
for worksheets and notes — and that endpoint re-checks, on every single
request, who is asking: their session, their school, whether the video is
public or school-only or private, and whether a paid video has actually been
paid for. Only then does it hand back a signed link that stops working after
two hours. Revoke someone's access and the next request fails; there is no
durable URL left behind to keep working.
What that buys, concretely:
-
A URL copied out of the browser stops working the same afternoon, and only ever worked for content that viewer was already allowed to watch.
-
A link forwarded to somebody outside the school is useless — it is checked against their session, not the sender's.
-
Videos from other schools, unapproved uploads, and unpurchased paid content are not merely hidden in the UI; the server refuses to produce a link at all.
-
A PRIVATE video is the owner's alone — the school's administrators cannot play it either; only the platform team, which approves every upload, can.
-
The video address works only inside the player. Opened in its own tab, it is refused, because a tab is the browser's bare player with a Download button.
Inside the player, downloading is discouraged the ordinary ways — no download control, no right-click save, no drag-to-desktop — and every frame carries a watermark in two layers:
- Always on, below notice. The viewer's account code is tiled across the
picture at 1% opacity. You will not see it while watching; a screenshot or a
screen recording keeps it, and raising the contrast of a leaked frame brings
the code back (
scripts/watermark/reveal.py). - Visible when a capture may be starting. The viewer's own masked email and code appear on the signals a browser does give: the screenshot keys' modifiers (⌘⇧ / Win⇧), PrintScreen, switching to another window or tab, a picture-in-picture attempt.
Picture-in-Picture and casting are switched off for school video specifically because they would strip that watermark.
Screen recording cannot be prevented in a web browser — by us or by
anyone. No web page can stop a phone camera, a screen recorder, or the
operating system's own screenshot key, and no web page is even told a capture
is happening — which is why the visible mark rides on signals and the
invisible one is always on. The watermark does not stop a recording; it makes
a leaked screenshot or screen recording traceable to the account that made
it. A second phone filming the screen defeats the invisible layer. Blocking
capture outright takes a native app shell (Android FLAG_SECURE; iOS can
blank the screen while it is recorded) or studio DRM (Widevine/FairPlay).
Tell schools what is true: casual copying is closed off, and determined
recording is attributable, not impossible.
Structure
The block follows the mirror pattern: routes under app/[lang]/s/[subdomain]/(school-dashboard)/lumos/ are thin wrappers, and everything lives in src/components/lumos/ (the component directory keeps the old name; the URL and UI are Lumos). Read fetchers in data/catalog/ are server-only modules wrapped in React cache() — deliberately not "use server" actions.
src/components/lumos/— one block, mirrored 1:1 to /streamtypes.ts— LumosDictionary, LumosContentProps, course typesauthorization.ts— RBAC matrix (DEVELOPER > ADMIN > TEACHER > STUDENT)list-params.ts— URL state (search, category, grade)header.tsx— stream navigation headersearch-bar.tsx— localized search + Explore dropdown (popular chips)lib/quota.ts— per-school video storage quota (check / inc / dec)home/— landing page — server sectionscontent.tsx— orchestrator (hero, skills, releases, continue)continue-watching-section.tsx— resume strip from LessonProgresscourses/— catalog browse + detail + enrollmentcontent.tsx— course grid (search, grade badges)[slug]/content.tsx— course detail — chapters, lessons, progressenrollment/— free enroll + Stripe checkout actionsdashboard/— the learning surfacescontent.tsx— student dashboard (enrolled courses)lesson/content.tsx— lesson player — fallback clip, unlock pill, progresslesson/catalog-actions.ts— progress writes, complete/incomplete, quizparent/— guardian read-only child progresslesson/instructor-switcher.tsx— instructor chips + per-video unlocksettings/— admin hub — 5 tabscontent.tsx— tab shell (overview / enrollments / instructors / review / videos)video-review-actions.ts— school review lane (platform gate for PUBLIC/PAID)video-review-content.tsx— pending queue UI (localized)instructor-settings.tsx— per-subject instructor preferenceenrollments/— school enrollment managementteach/— contributor surfacesvideos-content.tsx— My Videos table + upload entrypropose-video-dialog.tsx— 3-step wizard — URL or direct S3 uploadvideo-settings-dialog.tsx— owner controls (visibility, revoke, delete)get-proposable-lessons.ts— lessons the caller may contribute tovideo/— video lifecycle actionsvideo-actions.ts— uploadVideo — quota, HEAD size, reviewer notifyvideo-owner-actions.ts— visibility / paywall / revoke / replace / deletevideo-purchase-actions.ts— per-video Stripe unlock checkoutpayment/— Stripe success / cancel pagesemails/— enrollment + completion templates (Resend)shared/— player + validators + email servicevideo-player/— player, seek, up-next, watermark, onSourceErrorurl-validators.ts— video / image / document URL allowlistsdata/catalog/— read fetchers — React cache(), NOT "use server"get-lesson-with-progress.ts— ranked videos, paywall nulls, preference sortget-all-courses.ts— published subjects + search + filtersREADME.md— feature purpose, routes, statusISSUE.md— close log + post-release engineering debtCLAUDE.md— block-level context and key decisionsintegration points— outside the blocksrc/app/[lang]/s/[subdomain]/(school-dashboard)/lumos/— route mirrors (page.tsx thin wrappers)src/app/api/blob/presign/route.ts— direct-upload presign (POST) + orphan cleanup (DELETE)src/app/api/webhooks/stripe/route.ts— enrollment + video_purchase completionsrc/lib/s3.ts— HEAD (authoritative bytes) + DELETE helperssrc/components/internationalization/lumos-{en,ar}.json— the stream dictionary subtreesrc/tests/school-dashboard/lumos/— 278 unit tests across 19 filesRelated
- Catalog — owns
Subject/Chapter/Lesson, selections, overrides, and the instructor policy (per-subject preference, school-wide default, lock, block list) that Lumos reads. - Architecture — the mirror pattern and file contracts this block follows.
- Onboarding videos — the marketing "story" clip that doubles as the lesson fallback.
- QA guide (hogwarts#377) — these flows as a walkable acceptance script.