Student Engagement & Gamification¶
This document is the canonical developer reference for the Student Engagement & Gamification domain of the MAS / Mr. Mentor backend. It covers the four interlocking systems that drive the student dashboard's level ribbon and badges, the batch-lead "Students at Risk" card, the daily-card nudges, the warning automation, and the super-mentor rating / performance-metric pipeline. The domain is a mix of real-time XP hooks (fired inline when a student does something) and scheduled worker pipelines (risk scoring, badge safety-net evaluation, warning automation, daily cards) that roll those signals up into dashboards and outbound actions.
Status: documented from source on this branch.
Overview¶
The domain answers three questions for three audiences:
| Audience | Surface | Question answered |
|---|---|---|
Student (USER role) |
Dashboard level ribbon, badge wall, daily cards | "How am I progressing, and what should I do next today?" |
Batch Lead (BatchLead / batch-lead-gated) |
Students-at-Risk card | "Which of my students are disengaging, why, and what action do I take?" |
Super Mentor (EXPERT-ish, super-mentor-gated) |
Weekly ratings, skill ratings | "How is each mentee performing on soft + technical skills, and how does that roll into their overall score?" |
The systems and their source of truth:
| System | Drives | Primary sources |
|---|---|---|
| XP / levels / streaks | Student dashboard ribbon | src/config/xpRules.ts, src/services/StudentProgressService.ts, src/entities/XpEvent.ts, src/entities/StudentProgress.ts |
| Badges | Badge wall + celebration modal | src/config/badges.config.ts, src/services/BadgeService.ts, src/entities/StudentBadge.ts |
| Student-at-Risk | BL Students-at-Risk card + Miss Ozone framing | src/config/riskRules.ts, src/services/StudentRiskService.ts, src/entities/StudentRiskScore.ts |
| Warnings automation | Attendance warnings + 3-strike escalation | src/services/WarningAutomationService.ts, src/entities/Warning.ts |
| Daily cards | Student "today" nudges | src/workers/daily-cards.worker.ts, src/entities/DailyCard.ts |
| Performance metrics | Weighted student score | src/services/PerformanceMetricService.ts, src/entities/PerformanceMetric.ts |
| Skill / weekly ratings | Super-mentor scoring inputs | src/entities/StudentSkillRating.ts, src/entities/WeeklyRating.ts |
A detailed algorithm reference lives in-repo at docs/student-engagement-algorithms.md; this document summarizes the numbers and focuses on the data model, API surface, and end-to-end journeys.
Key concepts & entities¶
Glossary
- XP event — an append-only ledger row for every point-granting action. Idempotent on
(userId, type, sourceId). - Meaningful action — an XP event that is not a login, streak milestone, action milestone, or badge unlock. Counts toward one of the three level gates.
- Level gate — a level L is held only when
totalXp >= req.xpANDmeaningfulActions >= req.actionsANDlongestStreak >= req.streak. - Streak — consecutive IST days with at least one event. Grace is exactly one missed day; a 2+ day gap resets to 1.
- Blocking axis — the axis (xp / actions / streak) with the lowest percentage toward the next level; the dashboard highlights it.
- Badge — a starter achievement keyed by a string id (
first_step,week_warrior, ...). Unlock grants per-badge XP. - Risk score — composite 0-100 score per
(student, batch), from five weighted signals; tiered low / medium / high. - Review suppression — when a BL marks a risk row reviewed (or a Miss Ozone call auto-reviews), the row is hidden for
RISK_REVIEW_HOURS(48h). - Warning — a strike against a student (attendance / performance / payment / behavior / submission). The 3rd active warning escalates to the batch lead.
- Daily card — a per-student, per-day nudge of one of three types (top priority / deadline alert / weak-area nudge), expiring after 24h.
- Performance metric — a single weighted overall score per student from six components (eval test, learn progress, super-mentor rating, communication, attendance, mock interview).
Main TypeORM entities
| Entity | Table | File | Notes |
|---|---|---|---|
XpEvent |
xp_events |
src/entities/XpEvent.ts |
Append-only ledger. UQ_XP_EVENT_DEDUP (userId, type, sourceId). Stores amount, baseXp, multipliersApplied, occurredAt. |
StudentProgress |
student_progress |
src/entities/StudentProgress.ts |
Cached O(1) snapshot. PK userId. totalXp, meaningfulActions, level, currentStreak, longestStreak, optimistic version. |
StudentBadge |
student_badges |
src/entities/StudentBadge.ts |
One row per (userId, badgeId). UQ_STUDENT_BADGE. seenAt null until celebration modal shown. |
StudentRiskScore |
student_risk_scores |
src/entities/StudentRiskScore.ts |
Latest-only per (studentId, batchId). UQ_STUDENT_RISK_SCORE. signals jsonb, tier, reason, reasonSource, reviewedUntil, lastCallSummary. |
Warning |
warnings |
src/entities/Warning.ts |
WarningType + WarningSeverity enums. isActive, escalatedToBatchLead, resolvedAt. |
DailyCard |
daily_cards |
src/entities/DailyCard.ts |
DailyCardType enum. priority, expiresAt, dismissed, ctaLabel/ctaLink. |
PerformanceMetric |
performance_metrics |
src/entities/PerformanceMetric.ts |
Six component scores + weighted overallScore. |
StudentSkillRating |
student_skill_ratings |
src/entities/StudentSkillRating.ts |
Per (superMentorId, studentId, skillName). Beginner/intermediate/advanced + overallRating (0-100) + smRating (0-5). |
WeeklyRating |
weekly_ratings |
src/entities/WeeklyRating.ts |
Unique (studentId, weekNumber, year). Five 0-5 sub-ratings + overallAverage. |
Architecture¶
flowchart TD
subgraph Actors["Actors"]
STU["Student (USER)"]
BL["Batch Lead"]
SM["Super Mentor"]
end
subgraph Routes["Express routes"]
SR["/api/student/me/* progress + badges"]
DCR["/api/student/daily-cards"]
BLR["/api/batchlead/risk-students/*"]
BLW["/api/batchlead/students/:id/warnings"]
SMR["/api/supermentor/students/:id/ratings + skills"]
end
subgraph Ctrls["Controllers"]
SPC["StudentProgressController"]
NC["NotificationController (daily cards)"]
SRC["StudentRiskController"]
BLC["BatchLeadController"]
SMC["SuperMentorController"]
end
subgraph Svcs["Services"]
SPS["StudentProgressService grantXp"]
BS["BadgeService"]
SRS["StudentRiskService"]
WAS["WarningAutomationService"]
PMS["PerformanceMetricService"]
end
subgraph Hooks["Inline XP hooks"]
QZ["studentQuiz.controller"]
SLOT["SlotCompletionService"]
MLS["MrLearnSyncService"]
MTS["MrTestSyncService"]
end
subgraph Workers["BullMQ workers (scheduled)"]
BEW["badgeEvaluation.worker 24h"]
SRW["studentRiskComputation.worker 24h"]
WW["warning.worker 23:59 IST"]
DCW["daily-cards.worker 00:00 IST"]
end
subgraph Data["PostgreSQL + Redis"]
XPE[("xp_events")]
SP[("student_progress")]
SB[("student_badges")]
RSK[("student_risk_scores")]
WRN[("warnings")]
DC[("daily_cards")]
PM[("performance_metrics")]
RED[("Redis daily-cards cache")]
end
subgraph Ext["External"]
GROQ["Groq LLM (risk reason + AI recs)"]
OZONE["Miss Ozone LiveKit voice agent"]
end
STU --> SR --> SPC --> SPS
SPC --> BS
STU --> DCR --> NC
BL --> BLR --> SRC --> SRS
BL --> BLW --> BLC --> WAS
SM --> SMR --> SMC --> PMS
QZ --> SPS
SLOT --> SPS
MLS --> SPS
MTS --> SPS
QZ --> BS
SLOT --> BS
SPS --> XPE
SPS --> SP
BS --> SB
BEW --> BS
SRW --> SRS --> RSK
WW --> WAS --> WRN
DCW --> DC
DCW --> RED
PMS --> PM
SRS --> GROQ
SRS --> OZONE
NC --> RED
Data model¶
erDiagram
USER ||--o| STUDENT_PROGRESS : "has snapshot"
USER ||--o{ XP_EVENT : "earns"
USER ||--o{ STUDENT_BADGE : "unlocks"
USER ||--o{ STUDENT_RISK_SCORE : "scored per batch"
USER ||--o{ WARNING : "receives"
USER ||--o{ DAILY_CARD : "is nudged by"
USER ||--o{ PERFORMANCE_METRIC : "measured by"
USER ||--o{ WEEKLY_RATING : "rated weekly"
USER ||--o{ STUDENT_SKILL_RATING : "rated per skill"
STUDENT_PROGRESS {
uuid userId PK
int totalXp
int meaningfulActions
smallint level
int currentStreak
int longestStreak
date lastActiveDate
int version
}
XP_EVENT {
uuid id PK
uuid userId FK
varchar type
int amount
int baseXp
varchar sourceId
timestamp occurredAt
jsonb multipliersApplied
}
STUDENT_BADGE {
uuid id PK
uuid userId FK
varchar badgeId
timestamp unlockedAt
timestamp seenAt
}
STUDENT_RISK_SCORE {
uuid id PK
uuid studentId FK
uuid batchId
smallint score
varchar tier
jsonb signals
text reason
varchar reasonSource
timestamp reviewedUntil
text lastCallSummary
timestamp computedAt
}
WARNING {
uuid id PK
uuid studentId FK
enum type
enum severity
text description
boolean isActive
boolean escalatedToBatchLead
timestamp resolvedAt
}
DAILY_CARD {
uuid id PK
uuid userId FK
enum cardType
varchar title
int priority
timestamp expiresAt
boolean dismissed
}
PERFORMANCE_METRIC {
uuid id PK
uuid studentId FK
decimal evalTestScore
decimal learnProgressScore
decimal superMentorRating
decimal communicationScore
decimal attendanceScore
decimal mockInterviewScore
decimal overallScore
}
WEEKLY_RATING {
uuid id PK
uuid studentId FK
uuid superMentorId
int weekNumber
int year
decimal overallAverage
}
STUDENT_SKILL_RATING {
uuid id PK
uuid studentId FK
uuid superMentorId
varchar skillName
decimal overallRating
decimal smRating
}
Notable enums / status fields
RiskTier = 'low' | 'medium' | 'high'(src/entities/StudentRiskScore.ts).reasonSource = 'ai' | 'template'— whether the risk reason came from Groq or the deterministic fallback.WarningType = attendance | performance | payment | behavior | submission;WarningSeverity = low | medium | high | critical(src/entities/Warning.ts).DailyCardType = top_priority | deadline_alert | weak_area_nudge(src/entities/DailyCard.ts).- XP event
typevalues:daily_login,assignment_submitted,test_taken,module_completed,mentor_call_completed,streak_milestone,action_milestone,badge_unlocked,tenure_bonus. Non-action types are excluded from the meaningful-actions gate.
API surface¶
All paths below are derived from the actual route files. Mount prefixes (from src/routes/index.ts): student routes mount at /api/student; batch-lead routes at /api/batchlead; super-mentor routes at /api/supermentor.
Student-facing (src/routes/student.routes.ts)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/student/me/progress |
authMiddleware (any authed user) |
Level ribbon snapshot: level, totalXp, three axes, blockingAxis, unseenBadges. |
| GET | /api/student/me/badges |
authMiddleware |
Full badge wall (all starter badges with unlocked/seen flags). |
| POST | /api/student/me/badges/seen |
authMiddleware |
Clears the unseen flag for all badges (after celebration modal). |
| GET | /api/student/daily-cards |
authMiddleware |
Today's non-dismissed, non-expired daily cards. |
| POST | /api/student/daily-cards/:id/dismiss |
authMiddleware |
Dismiss one daily card. |
Batch-lead facing (src/routes/batchLead.routes.ts, gated by authMiddleware + batchLeadMiddleware)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/batchlead/risk-students |
Batch Lead | Top N actionable at-risk students for this BL's batches (limit 1-20, default 5). |
| POST | /api/batchlead/risk-students/recompute |
Batch Lead | On-demand recompute of risk scores across all of this BL's batches. |
| POST | /api/batchlead/risk-students/:id/review |
Batch Lead | Mark a risk row reviewed, suppressing it for RISK_REVIEW_HOURS (48h). |
| POST | /api/batchlead/risk-students/:id/miss-ozone |
Batch Lead | Fire a Miss Ozone outbound check-in call for the student. |
| POST | /api/batchlead/risk-students/:id/ai-classroom |
Batch Lead | File an AI Classroom request for the at-risk student. |
| GET | /api/batchlead/students/:id/warnings |
Batch Lead | List a student's warnings. |
| POST | /api/batchlead/students/:id/warnings |
Batch Lead | Create a manual warning. |
| PATCH | /api/batchlead/warnings/:warningId/resolve |
Batch Lead | Resolve a warning. |
| GET | /api/batchlead/warnings/escalated |
Batch Lead | List students escalated (3 active warnings). |
| GET | /api/batchlead/students/:id/skills |
Batch Lead | Read a student's skill ratings. |
| PATCH | /api/batchlead/students/:id/skills |
Batch Lead | Update skill ratings. |
Super-mentor facing (src/routes/superMentor.routes.ts)¶
| Method | Path | Auth/role | Purpose |
|---|---|---|---|
| GET | /api/supermentor/students/ratings/week |
Super Mentor | Weekly ratings for all mentees for a given week. |
| GET | /api/supermentor/students/:id/ratings |
Super Mentor | A mentee's weekly rating for the current week. |
| GET | /api/supermentor/students/:id/ratings/history |
Super Mentor | Weekly rating history. |
| POST | /api/supermentor/students/:id/ratings |
Super Mentor | Save/update a weekly rating (feeds the super-mentor performance component). |
| GET | /api/supermentor/students/:id/skills |
Super Mentor | Read skill ratings. |
| PATCH | /api/supermentor/students/:id/skills |
Super Mentor | Update skill ratings. |
| POST | /api/supermentor/students/:id/warnings |
Super Mentor | Issue a warning. |
| PATCH | /api/supermentor/warnings/:warningId/resolve |
Super Mentor | Resolve a warning. |
Per-request ownership is re-checked inside the services (e.g.
StudentRiskService.batchesLedBy+ a "Not your student" guard) even though routes are role-gated upstream.
User journeys¶
Journey 1 — XP event grant then level / streak update¶
A student takes an action (submits an assignment, takes a test, completes a module, finishes a mentor call). The originating controller/service calls StudentProgressService.grantXp(userId, type, sourceId, ctx). The grant runs in a single DB transaction that locks the student_progress row, computes multipliers, inserts the ledger event, and recomputes the cached level/streak. The unique key (userId, type, sourceId) makes the whole thing idempotent — replays no-op.
sequenceDiagram
participant SRC as Quiz or Slot or Sync hook
participant SPS as StudentProgressService
participant DB as PostgreSQL
participant BS as BadgeService
SRC->>SPS: grantXp userId type sourceId ctx
SPS->>DB: BEGIN transaction
SPS->>DB: check xp_events for userId type sourceId
alt event already exists
DB-->>SPS: row found
SPS-->>SRC: return null no-op idempotent
else new event
SPS->>DB: SELECT student_progress FOR UPDATE lock row
SPS->>SPS: count recent same-type events for anti-farm
SPS->>SPS: check first meaningful event of IST day
SPS->>SPS: computeMultipliers streak quality timing freshness anti-farm clamp 0.5 to 2.0
SPS->>DB: INSERT xp_events amount base multipliers
SPS->>SPS: walk streak using IST date diff one-day grace
SPS->>SPS: recompute level from xp actions longestStreak gates
SPS->>DB: UPDATE student_progress totals streak level version
opt streak hit multiple of 7
SPS->>DB: recursive grant streak_milestone plus 50 XP skipStreakUpdate
end
SPS->>DB: COMMIT
SPS-->>SRC: GrantResult with event and leveledUp
SRC->>BS: evaluateForUser userId non-blocking
end
Key behaviors (src/services/StudentProgressService.ts):
- Idempotency — pre-check plus the DB unique constraint as the final defense against races.
- Anti-farm uses createdAt (grant time), not occurredAt, so backfills do not poison the live 24h counter.
- Backfill safety — historical events must pass skipStreakUpdate: true or the streak engine rewinds lastActiveDate.
Journey 2 — Student reads the dashboard ribbon¶
sequenceDiagram
participant FE as Student dashboard
participant SPC as StudentProgressController
participant SPS as StudentProgressService
participant BS as BadgeService
participant DB as PostgreSQL
FE->>SPC: GET /api/student/me/progress
SPC->>SPS: getProgress userId
SPS->>DB: SELECT student_progress
alt no row yet
DB-->>SPS: none
SPS-->>SPC: synthesized zero snapshot level 1
else found
DB-->>SPS: snapshot
end
SPC->>SPC: requirementForLevel current and next
SPC->>SPC: compute three axes percent and pick lowest as blockingAxis
SPC->>BS: listUnseenBadgeIds userId
BS->>DB: SELECT student_badges where seenAt is null
DB-->>BS: unseen ids
SPC-->>FE: level axes overallPercent blockingAxis unseenBadges
The dashboard renders three axis bars; blockingAxis tells the UI which "X more to unlock L_n" line to highlight. isOnFireToday is true when lastActiveDate === today in IST.
Journey 3 — Badge unlock (inline hook + safety-net cron)¶
Badges unlock two ways: (1) inline after an XP-granting action via BadgeService.evaluateForUser, and (2) the daily badgeEvaluation.worker safety net (mainly so profile_pro, whose source does not pass through grantXp, still unlocks). Unlock is idempotent on (userId, badgeId) and credits the per-badge XP reward via the normal grantXp path.
sequenceDiagram
participant TRG as Inline hook or 24h cron
participant BS as BadgeService
participant DB as PostgreSQL
participant SPS as StudentProgressService
TRG->>BS: evaluateForUser userId
BS->>DB: SELECT already-earned badge ids
loop each starter badge not yet earned
BS->>BS: run evaluator first_step week_warrior test_taker module_master etc
alt criteria met
BS->>DB: pre-check student_badges for userId badgeId
alt not present
BS->>DB: INSERT student_badges defer to UQ_STUDENT_BADGE
BS->>SPS: grantXp badge_unlocked badgeId baseOverride xpReward
SPS->>DB: ledger plus progress update
BS->>BS: append id to newlyUnlocked
else duplicate
BS->>BS: skip no double-award
end
end
end
BS-->>TRG: newlyUnlocked ids
The student later calls GET /api/student/me/badges; freshly unlocked ids show up in unseenBadges on the progress endpoint, the UI shows the celebration modal, then calls POST /api/student/me/badges/seen to clear the flag. Test/module badges are batch-scoped — only Mr Test exams and Mr Learn courses attached to the student's batch count.
Journey 4 — Daily risk computation then BL action¶
The studentRiskComputation.worker runs every 24h, walking active batches and writing one student_risk_scores row per enrolled student (five weighted signals to a 0-100 composite, tiered). For each row it tries Groq for a one-sentence reason and falls back to a deterministic template. The BL then reads the card and acts.
sequenceDiagram
participant CRON as studentRiskComputation worker 24h
participant SRS as StudentRiskService
participant DB as PostgreSQL
participant GROQ as Groq LLM
participant BL as Batch Lead
participant OZ as Miss Ozone pipeline
CRON->>SRS: runDailyComputation
loop each active batch and enrolled student
SRS->>DB: gather logins learn-progress tests warnings placement
SRS->>SRS: compute five signals weight to composite and tier
SRS->>SRS: skip new students within 7-day grace
SRS->>GROQ: generate one-sentence reason
alt Groq ok
GROQ-->>SRS: AI reason reasonSource ai
else any failure
SRS->>SRS: deterministic template reasonSource template
end
SRS->>DB: UPSERT student_risk_scores on studentId batchId
end
SRS-->>CRON: batchesProcessed studentsProcessed flagged
BL->>SRS: GET /api/batchlead/risk-students
SRS->>DB: top rows where reviewedUntil passed score desc
DB-->>SRS: rows
SRS-->>BL: at-risk list with reasons
alt BL marks reviewed
BL->>SRS: POST risk-students id review
SRS->>DB: set reviewedAt and reviewedUntil now plus 48h
else BL triggers Miss Ozone
BL->>SRS: POST risk-students id miss-ozone
SRS->>OZ: triggerMissOzone writes call row state scheduled
Note over OZ: voice agent calls student then analyzer summarizes
OZ->>SRS: attachCallSummary studentId batchId summary
SRS->>DB: write lastCallSummary plus auto-review 48h reviewedBy null
end
Gates and grace (src/config/riskRules.ts, src/services/StudentRiskService.ts):
- 7-day new-student grace — applications created within the last 7 days are skipped.
- 48h review suppression (RISK_REVIEW_HOURS) — reviewed/called rows drop off the card, except rows carrying a lastCallSummary which stay visible so the BL can read it.
- App-status filter — only enrolled / batch_allocated / paid applications are scored.
Journey 5 — Daily warning automation + 3-strike escalation¶
The warning.worker fires at 23:59 IST. WarningAutomationService.processDailyAbsences finds yesterday's ABSENT attendance records and creates one attendance warning per absent student, deduping on description-date and skipping students who already have 3 active warnings. The 3rd warning escalates to the batch lead.
sequenceDiagram
participant CRON as warning worker 23:59 IST
participant WAS as WarningAutomationService
participant DB as PostgreSQL
participant BL as Batch Lead dashboard
CRON->>WAS: processDailyAbsences yesterday
WAS->>DB: find ABSENT attendance for target date
WAS->>DB: load existing active attendance warnings for those students
loop each absent student
alt already warned for this date or already 3 warnings
WAS->>WAS: skip increment skipped
else
WAS->>DB: INSERT warning attendance medium createdBy SYSTEM
WAS->>DB: increment user activeWarningCount
alt count reaches 3
WAS->>DB: set user escalatedToBatchLead and escalatedAt
WAS->>DB: mark warning escalatedToBatchLead
WAS->>WAS: increment studentsEscalated
end
end
end
WAS-->>CRON: processed warningsCreated studentsEscalated skipped errors
BL->>BL: GET /api/batchlead/warnings/escalated shows 3-strike students
Batch leads and super mentors can also create warnings manually via the warning endpoints; resolving a warning flips isActive and stamps resolvedAt / resolvedBy.
Journey 6 — Daily card generation + delivery¶
The daily-cards.worker runs at midnight IST. For every enrolled student it generates up to three cards (top priority, deadline alert, weak-area nudge), deletes the prior day's cards, persists the new set, and caches them in Redis with a 24h TTL. The student fetches them and can dismiss individual cards.
sequenceDiagram
participant CRON as daily-cards worker 00:00 IST
participant DB as PostgreSQL
participant RED as Redis
participant FE as Student dashboard
participant NC as NotificationController
CRON->>DB: distinct enrolled userIds enrollment plus active applications
loop each student
CRON->>DB: latest perf recent quizzes upcoming assignments ai-classroom progress
CRON->>CRON: build top_priority deadline_alert weak_area_nudge cards
CRON->>DB: DELETE old daily_cards then INSERT new set
CRON->>RED: SET daily-cards student key TTL 86400
end
FE->>NC: GET /api/student/daily-cards
NC->>DB: non-dismissed non-expired cards for user
NC-->>FE: today cards
FE->>NC: POST daily-cards id dismiss
NC->>DB: set dismissed true
Card selection logic (src/workers/daily-cards.worker.ts): top-priority prefers an upcoming assignment, else a recent quiz under 50%, else an in-progress AI classroom, else a generic explore card. Weak-area nudge picks the lowest of eval-test / learn-progress / communication / attendance scores from the latest performance_metrics row.
Journey 7 — Super-mentor rating then performance-metric roll-up¶
A super mentor saves a weekly rating or skill rating. PerformanceMetricService recomputes the affected component(s) and the weighted overall score. The super-mentor component is 50% weekly ratings + 50% skill ratings, normalized and stored as a 0-5 value, then weighted 30% into the overall.
sequenceDiagram
participant SM as Super Mentor
participant SMC as SuperMentorController
participant DB as PostgreSQL
participant PMS as PerformanceMetricService
SM->>SMC: POST /api/supermentor/students/:id/ratings weekly sub-ratings
SMC->>DB: UPSERT weekly_ratings overallAverage
SMC->>PMS: updateSuperMentorScore studentId freshWeeklyValue
PMS->>DB: load skill_ratings overallRating
PMS->>PMS: combine weekly 50pct plus skill 50pct then divide by 20 for 0-5
PMS->>DB: getOrCreate performance_metrics set superMentorRating
PMS->>PMS: calculateOverallScore weighted sum six components
PMS->>DB: save overallScore
PMS-->>SMC: done
SMC-->>SM: saved
Overall-score weights (PerformanceMetricService.calculateOverallScore): eval test 15%, learn progress 15%, super-mentor rating 30% (the 0-5 value normalized x20), communication 15% (currently a not-implemented placeholder, always 0), attendance 15%, mock interview 10%. Component updaters (updateEvalTestScore, updateAttendanceScore, updateMockInterviewScore, etc.) are called inline when source data changes.
Background jobs & async¶
| Worker | Queue | Schedule | Source | Purpose |
|---|---|---|---|---|
badgeEvaluation.worker |
badgeEvaluationQueue |
every 24h (scheduleBadgeEvaluation(24), repeat.every) |
src/workers/badgeEvaluation.worker.ts |
Pages enrolled students (200/page) and re-runs evaluateForUser as a safety net. |
studentRiskComputation.worker |
studentRiskComputationQueue |
every 24h (scheduleStudentRiskComputation(24), stable jobId student-risk-daily) |
src/workers/studentRiskComputation.worker.ts |
Recomputes student_risk_scores for all active batches/students. |
warning.worker |
warningQueue |
cron 59 23 * * * (11:59 PM daily) |
src/workers/warning.worker.ts |
Processes yesterday's absences into attendance warnings + escalations. |
daily-cards.worker |
dailyCardsQueue |
cron 0 0 * * * tz Asia/Kolkata (midnight IST) |
src/workers/daily-cards.worker.ts |
Generates + caches three daily cards per enrolled student. |
Scheduling is wired in src/index.ts (worker imports + queueService.schedule* calls) and the schedule definitions live in src/services/QueueService.ts. All four workers run concurrency: 1 — the work iterates internally, so queue-level parallelism is unnecessary. Repeatable jobs use stable jobIds / clear-then-re-add so a reboot replaces rather than stacks schedules.
Related queues (adjacent, not owned here): the KPI worker (kpiQueue, every 15 min) computes dashboard KPIs; Miss Ozone has its own reconciler (scheduleMissOzonePolling, every 60s) and retention prune (daily 03:30) that interact with risk rows via attachCallSummary.
No webhooks are owned directly by this domain; the inbound async surface is the Miss Ozone evaluation fetch (BullMQ job) which calls back into StudentRiskService.attachCallSummary. There are no domain-specific Socket.IO events.
Inline (non-worker) XP/badge hooks — grantXp + evaluateForUser are also called synchronously (fire-and-forget, non-blocking) from: studentQuiz.controller.ts (assignment submission), SlotCompletionService.ts (mentor call completed), MrLearnSyncService.ts (module completed), MrTestSyncService.ts (test taken), and auth.controller.ts / DemoSeedService.ts.
External integrations¶
| Integration | Where | Env / config | Failure / fallback |
|---|---|---|---|
Groq LLM (llama-3.3-70b-versatile) |
Risk-row reason generation + AI recommendations (StudentRiskService, AiStudentProfileService) |
GROQ_API_KEY; DEBUG_AI_RECS=1 for verbose logs |
Fails soft: no key / timeout (~4.5s) / non-200 / malformed JSON / multiline / over length all fall back to the deterministic template (reasonSource = 'template') or [] recs. The algo path is the floor. |
| Miss Ozone voice agent (LiveKit + Twilio SIP) | Outbound at-risk check-in calls (MissOzoneService, triggered from StudentRiskService.triggerMissOzone) |
LiveKit / Twilio config (see Miss Ozone docs) | If the student does not pick up the call goes to failed with no auto-review; the dedupe gate stops blocking re-triggers. |
| Redis | Daily-card cache (24h TTL) and all BullMQ queues | REDIS_HOST, REDIS_PORT |
Cache miss falls back to the DB read in NotificationController.getDailyCards. |
| Mr Learn / Mr Test (Graphy + EzExam) | Source of module_completed / test_taken XP and risk signals; batch-scoping for badges |
sync worker configs | Email-matched (prefix-tolerant for Graphy's <orgPrefix>-<email>). |
There is no dedicated feature flag for the gamification systems; they are always on. Tuning constants live in src/config/xpRules.ts, src/config/badges.config.ts, and src/config/riskRules.ts.
Status lifecycles¶
Risk-row review state¶
A student_risk_scores row cycles between visible-on-card and suppressed. The composite score/tier is recomputed daily; the review state controls visibility independent of score.
stateDiagram-v2
[*] --> Computed
Computed --> Visible : tier medium or high and reviewedUntil passed
Computed --> Hidden : tier low below 40
Visible --> Suppressed : BL marks reviewed sets reviewedUntil now plus 48h
Visible --> Called : BL triggers Miss Ozone
Called --> SummaryAttached : call wraps with analyzer summary auto-review 48h
Called --> Visible : student not picked up failed no auto-review
SummaryAttached --> Visible : 48h elapses still flagged shows Call again
Suppressed --> Visible : 48h elapses and still at risk on next cron
Hidden --> Visible : signals worsen on next cron
Warning lifecycle¶
stateDiagram-v2
[*] --> Active : created manual or SYSTEM auto-absence
Active --> Escalated : student reaches 3 active warnings
Active --> Resolved : BL or SM resolves sets resolvedAt resolvedBy
Escalated --> Resolved : warning resolved
Resolved --> [*]
Daily-card lifecycle¶
stateDiagram-v2
[*] --> Generated : midnight IST worker creates card expiresAt now plus 24h
Generated --> Dismissed : student dismisses
Generated --> Expired : expiresAt passes
Generated --> Replaced : next midnight run deletes and regenerates
Dismissed --> [*]
Expired --> [*]
Replaced --> [*]
Badge lifecycle¶
stateDiagram-v2
[*] --> Locked
Locked --> Unlocked : criteria met insert row grant xp reward
Unlocked --> Seen : student views celebration then POST badges seen
Seen --> [*]
Edge cases, limits & gotchas¶
- Idempotency everywhere. XP grants dedupe on
(userId, type, sourceId); badge unlocks on(userId, badgeId); risk rows upsert on(studentId, batchId). Cron + real-time evaluators can both fire without double-counting. - Streak uses
longestStreakfor level gates (notcurrentStreak) so a student who once hit 7 days but broke the run is not demoted. - Backfill streak hazard. Historical XP replays MUST pass
skipStreakUpdate: trueor the streak engine rewindslastActiveDate.src/scripts/repairStreaks.tsexists to clean up rows clobbered by this. - Anti-farm uses grant time, not occurredAt. Submitting five tests in a row earns less per test; backfills don't poison the live 24h counter.
- Multiplier clamp. The product of all multipliers is clamped to
[0.5, 2.0]so a 100-day streak student doesn't lap the cohort. - Route prefix mismatch. The risk controller doc-comment says
/api/batch-lead, but the actual mount insrc/routes/index.tsis/api/batchlead(no hyphen). Use/api/batchlead/risk-students. Express requiresrisk-students/recomputeto be declared before the dynamic:idroutes so "recompute" is not parsed as a student id. markReviewedreturns 404 when no risk row exists yet for the student — the BL must wait for (or trigger) a cron run first. Ownership failures ("Not your student") return 403.reviewedByis null on Miss Ozone auto-review — that's how the system distinguishes automated review from a BL clicking "Mark Reviewed".- Communication score is a placeholder —
PerformanceMetricService.updateCommunicationScorealways sets 0 (15% weight currently dead). Documented TODO in source. zeroLearnpenalizes missing learner rows — returns 100 when the batch has courses attached but the student has no learner row (they should have one).- Badge batch-scoping —
test_takerandmodule_masteronly count Mr Test exams / Mr Learn courses attached to the student's enrolled batch, matched by email with Graphy prefix tolerance. - Daily cards are fully replaced each run —
cardRepo.delete({ userId })then insert; dismissals do not persist across days. - Performance metric reads use latest-by-
calculatedAt—getOrCreateMetricfinds the most recent row or creates a zeroed one; there is no single canonical row, so component updaters all write to the latest. - Multi-platform — these endpoints are not platform-branched; they key entirely off the authenticated
userId/ batch membership, independent of thex-platformheader.
Related docs¶
- Mentorship & Meetings — mentor calls feed
mentor_call_completedXP. - Token Economy — mock-interview tokens feed the performance metric's mock score.
- Identity & Access — roles (
USER, batch lead, super mentor) and auth middleware that gate these routes. - Mr. Hire — Voice Interviews & AI Calling — sibling voice-agent pipeline (Miss Ozone shares the LiveKit/voice pattern).
- In-repo algorithm reference:
mr-mentor-backend/docs/student-engagement-algorithms.md— exact constants, Miss Ozone prompt template, and operational scripts.