
Role-based learning portal for Level Up Training Center: parents follow progress, sessions and invoices; trainers run schedules and grade digital assessments; administrators manage learners, payments and reporting in one place.
- My role
- Product, architecture and full-stack build
- Links
- Live demo ↗GitHub ↗
- Stack
- JavaScriptHTML5CSSPython
- JavaScript · HTML/CSS · Role-based access · Python build pipeline · In-portal assistant
- Microsoft-ecosystem fit
▸ Accessibility: keyboard-first, light and dark themes
▸ Role-based access by design
3
Roles: parent, trainer, admin
1
Place for schedule, progress and payments
[REPLACE]
Active learners
Live demo
Interactive demo
Parent, trainer and administrator views with sample data: schedule, assessments, payments and progress.


Source · Level Up LMS website (sign-in landing) and the public LMS demo with sample data
01 — Problem
Learner progress, session schedules and payments were tracked across spreadsheets and messaging apps, so families and staff had no single view.
02 — Approach
- 01
One data model behind three role-specific views: parent, trainer and administrator.
- 02
Parents see the next session, skill progress, trainer notes and invoices without contacting the centre.
- 03
Trainers manage the day's schedule, record session notes and grade digital assessments that feed the progress view.
- 04
Administrators track learners, collections and outstanding invoices; an in-portal assistant answers questions from the signed-in user's own data.
03 — Architecture
System architecture
S01
Parent · Trainer · Admin
S02
Role-based views
S03
Schedule + assessments
S04
Payments
S05
Progress reports
S06
Assistant
04 — Engineering notes
Reminders that cannot silently fail
Notifications are written in the same transaction as the change that caused them. A worker then claims due deliveries, and SKIP LOCKED lets several workers run without ever sending the same message twice.
// apps/api/src/notifications/dispatcher.ts (production)
const claimed = await db.execute(sql`
UPDATE notification_deliveries d
SET status = 'sending', attempts = d.attempts + 1, updated_at = now()
WHERE d.id IN (
SELECT id FROM notification_deliveries
WHERE status IN ('pending', 'failed') AND next_attempt_at <= now()
ORDER BY next_attempt_at
LIMIT ${batch}
FOR UPDATE SKIP LOCKED)
RETURNING d.id, d.notification_id, d.channel, d.attempts`);Failures back off exponentially. After the last attempt a delivery is marked dead and shown in the admin delivery log with a retry button.
Closure you can verify
// apps/api/src/lib/crypto.ts (production)
/** Deterministic JSON (sorted keys) so hashes are reproducible by auditors. */
export function canonicalJson(value: unknown): string {
if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null);
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, v]) => v !== undefined)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`;
}Each certificate stores the SHA-256 of that canonical payload, covering owner, verifier, checklist and evidence file hashes. A public endpoint re-hashes it on demand.



Outcome
Families follow progress directly, and administrators manage schedules, assessments and payments in one system.
Next project
Follow-up System
