1. What it is
micro-learning-service-v2 holds the training content — modules, sections, lessons, screens and blocks — and everything a learner does with it: opening a lesson, answering quiz questions, keeping a streak, earning points and getting a certificate. Admins build and publish content here (or push it in from authoring-service-v2), and the mobile app reads every lesson it plays from this service.
The same repository also ships tts-worker-v2, a separate Deployment built from
app/tts_worker/ and Dockerfile.tts-worker. It turns lesson text into
spoken narration. The top-level tts-worker-v2/ directory is an empty shell —
it holds no tracked source (git ls-files tts-worker-v2 returns nothing); the code
moved here in M4 (ARCHITECTURE.md §2).
| Who uses it | Admin web console · mobile app (employees) · authoring-service-v2, content-worker, assignment-service, users-auth-service · its own in-process schedulers |
| Runtime | FastAPI + 3 in-process schedulers · Deployment micro-learning-service-v2; second Deployment tts-worker-v2 (NATS worker, same repo, own image) |
| Database | Postgres microlearning — shared with tts-worker-v2, which writes lesson_screen_audio directly (ARCHITECTURE.md §3.4) |
| Redis | DB index 2 (ARCHITECTURE.md §3.5) |
| NATS streams | Publishes onto MICRO_LEARNING (training.*, learning.*) and TRAINING_AUDIO (tts.*); tts-worker-v2 consumes TRAINING_AUDIO. MICRO_LEARNING has three rival create-if-missing definitions — known defect, ARCHITECTURE.md §4.1(4) |
| External APIs | tts-worker-v2 → Modal chatterbox TTS endpoint; both processes → asset-manager (S3/CDN uploads) |
| Entry points | 109 HTTP routes · 1 NATS subject consumed (2 subscribe call sites) · 15 publish call sites · 11 background tasks |
2. Feature map
flowchart LR ADM["Admin web"] --> CONTENT["Content tree and publishing"] ADM --> DASH["Dashboards and reports"] MOB["Mobile app"] --> PLAY["Catalogue and lesson player"] MOB --> PROG["Progress, points, streaks, certificates"] SVC["Other services"] --> INGEST["Module ingest and translations"] BG["Schedulers and TTS worker"] --> AUDIO["Narration audio and streak nudges"] CONTENT --> A1["/v1/admin/modules ... /screens"] DASH --> A2["/v1/admin/overview, /v1/admin/progress"] PLAY --> A3["/v1/mobile/modules, /v1/mobile/lessons"] PROG --> A4["POST /v1/mobile/progress/events"] INGEST --> A5["/v1/internal/modules, /v1/internal/translations"] AUDIO --> A6["tts.audio.requested"]
3. Features
Admin
Build the content tree live
An admin creates a module, adds sections to it, lessons inside those sections, and then fills each lesson with screens and blocks — text, images, quizzes, a quiz graph. Sections and lessons can be reordered, and a lesson can be moved to another section. Everything is a soft delete, so nothing a learner already opened disappears mid-course.
- Entry points
/v1/admin/modules,/v1/admin/modules/{module_id}/sections,/v1/admin/modules/{module_id}/sections/{section_id}/lessons,/v1/admin/lessons/{lesson_id}/blocks,/v1/admin/lessons/{lesson_id}/screens,POST /v1/admin/lessons/{lesson_id}/move- Touches
modules,sections,lessons,lesson_screens,lesson_blocks- Related
- API_ENDPOINTS.md, BLOCK_TYPES_REFERENCE.md, CREATE_MODULE_DIRECT.md
- Evidence
- app/routes/module_routes.py:34, app/routes/section_routes.py:26, app/routes/lesson_routes.py:36, app/routes/block_routes.py:53, app/routes/screen_routes.py:61; caller: assignment-service/app/services/training_service.py:88
Publish a module, and version it live
Publishing freezes the draft into a numbered version that learners can be assigned to. The admin can ask for spoken narration at the same time; when narration credit enforcement is switched on, the tenant's credit balance is debited before the publish commits, and refunded if the commit fails. Unpublishing and re-publishing produce a new version rather than editing the live one.
- Entry points
POST /v1/admin/modules/{module_id}/publish,POST /v1/admin/modules/{module_id}/unpublish,GET /v1/admin/modules/{module_id}/versions,GET /v1/admin/modules/{root_id}/latest,GET /v1/admin/modules/{root_id}/draft- Touches
modules(root_id / version), users-auth credits, NATStraining.module.published, TTS prewarm- Related
- NARRATION_CREDIT_ESTIMATE_FE.md, CREATE_MODULE_DIRECT.md
- Evidence
- app/routes/module_routes.py:175 (debit at :223, commit at :259, prewarm at :297); caller: authoring-service-v2/app/core/http.py:108
Delete a module without orphaning assignments live
Deleting a module archives it and cancels every active assignment for it in assignment-service, so nobody is left with a course that no longer exists.
- Entry points
DELETE /v1/admin/modules/{module_id},DELETE /v1/internal/modules/{module_id},DELETE /v1/internal/modules/by-root/{root_id}- Touches
modules; assignment-service cancel endpoint- Related
- MODULE_DELETION_STRATEGY.md; the cancel call was broken until bug-hunt #10 added
HttpClient.delete - Evidence
- app/routes/module_routes.py:318, app/clients/assignment_service.py:203; bug-hunt-reports/micro-learning-service-v2.md #10
Overview dashboard live
Nine read-only cards on the admin home: completion and score KPIs, recently edited modules, active assignments, daily learning time, total versus active users, score distribution, recently issued certificates, a leaderboard and a team pulse list. Every card accepts a location and group filter.
- Entry points
GET /v1/admin/overview/kpisand eight siblings under/v1/admin/overview- Touches
user_lesson_progress,user_module_progress,certificates,user_points; assignment-service and users-auth over HTTP- Related
- ADMIN_OVERVIEW_DASHBOARD_FE.md
- Evidence
- app/routes/admin_overview_routes.py:79; docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:312-320; ingress rule global-configs/k8s/users-auth-ingress.yaml:69
Assignment progress and reports live
For one assignment an admin sees a completion summary, the per-learner list, an analytics breakdown, and can generate and download a report. There is also a bulk progress update for corrections.
- Entry points
/v1/admin/progress/assignments/{assignment_id}/summary,/users,/analytics,/reports,/v1/admin/progress/reports/{report_id}/download,POST /v1/admin/progress/users/bulk- Touches
user_module_progress,user_lesson_progress; assignment-service- Related
- API_ENDPOINTS.md §Progress Admin
- Evidence
- app/routes/progress_admin_routes.py:52; docs/API_ENDPOINTS.md:137-142; bug-hunt #17 fixed 500s on four of these
Narration voice and credit estimate live
An admin picks the voice their tenant's lessons are spoken in, or uploads a reference clip of their own. Before publishing with narration they are shown what it will cost in credits, so the price is never a surprise after the fact.
- Entry points
/v1/admin/audio/voices,/v1/admin/audio/voice-settings,/v1/admin/audio/voice-upload-url,/v1/admin/audio/voice-uploads/{asset_id}/complete,POST /v1/internal/narration/quote- Touches
voice_catalog,tenant_audio_settings, asset-manager- Related
- tenant-voice-fe.md, NARRATION_CREDIT_ESTIMATE_FE.md
- Evidence
- app/routes/admin_audio_routes.py:107, app/services/narration_credit_service.py:173; caller: authoring-service-v2/app/core/http.py:83
Streak rules per tenant live
An admin decides which weekdays count towards a streak, so a Monday-to-Friday workforce does not lose its streak over the weekend.
- Entry points
GET /v1/admin/streak-settings,PUT /v1/admin/streak-settings- Touches
tenant_streak_settings- Related
- STREAKS_AND_POINTS.md
- Evidence
- app/routes/admin_streak_routes.py:54; docs/STREAKS_AND_POINTS.md:175-176; ingress rule global-configs/k8s/users-auth-ingress.yaml:59
Employee (mobile)
See what I have to learn live
The home screen shows one hero module, the next lesson to pick up, and the rest of the assigned modules in a ranked order, each with its own progress. Tapping a module shows its sections and lessons.
- Entry points
GET /v1/mobile/modules,GET /v1/mobile/modules/{module_id},GET /v1/mobile/modules/{module_id}/sections,GET /v1/mobile/recent- Touches
modules,sections,lessons,user_module_progress; assignment-service for the learner's assignments- Related
- FRONTEND_INTEGRATION_GUIDE.md, RANKING_MODULES.md
- Evidence
- app/routes/progress_mobile_routes.py:1961; caller: users-auth-service/app/routes/mobile_login.py:538 (mobile bootstrap)
Play a lesson live
Opening a lesson returns its screens, each with its blocks and — when narration exists — an audio clip plus word-level timings so the text can highlight as it is read out. A lesson summary gives the pass mark and the learner's attempts.
- Entry points
GET /v1/mobile/lessons/{lesson_id}/screens,GET /v1/mobile/lessons/{lesson_id}/summary- Touches
lesson_screens,lesson_blocks,lesson_screen_audio,lesson_block_translations- Related
- SCREENS_RESPONSE_EXAMPLE.md, BLOCK_TYPES_REFERENCE.md, tenant-voice-fe.md
- Evidence
- app/routes/progress_mobile_routes.py:2977, app/routes/progress_mobile_routes.py:2824; docs/FRONTEND_INTEGRATION_GUIDE.md:207
Progress that survives closing the app live
The app posts one kind of event — lesson started, progressed, completed, abandoned — and the service keeps the session, the screen the learner was on, time on task and the module rollup. Re-opening the app resumes where the learner stopped, and completing a lesson updates the assignment in assignment-service.
- Entry points
POST /v1/mobile/progress/events- Touches
lesson_sessions,user_lesson_progress,user_module_progress,lesson_screen_dwell; NATSlearning.lesson.*,training.progress.assignment_progress,training.module.completed- Related
- MOBILE_FRONTEND_EVENTS_GUIDE.md, mobile/progress-events-telemetry.md, CHANGE_PROGESS_ROUTES.md
- Evidence
- app/routes/progress_mobile_routes.py:471 (dispatcher at :507-526); open defects: bug-hunt #6/#12 (points re-awarded on replay), #13 (replay wipes quiz attempts)
Quizzes and scoring live
Multiple-choice and drag-and-drop questions ship their answer key with the block, so the app scores them instantly and reports the attempt. The quiz-graph question is the exception: its answer stays on the server, so the app submits the attempt and gets back whether it was right, hints, and whether the answer may be revealed.
- Entry points
POST /v1/mobile/progress/events(quiz.attempt),POST /v1/mobile/lessons/{lesson_id}/quiz/questions/{question_id}/attempts- Touches
quiz_question_attempts,quiz_graph_answer_keys,user_lesson_progress- Related
- QUIZ_GRAPH_FLUTTER_INTEGRATION.md, QUIZ_SCORE_TRACKING.md, QUIZ_GRAPH_BLOCK_EXAMPLES.md
- Evidence
- app/routes/progress_mobile_routes.py:2685, app/services/graph_quiz_service.py, app/services/lesson_score_service.py; open defects: bug-hunt #5 (points on every attempt), #9 (allowed_incorrect_questions ignored)
Streaks, points and the leaderboard live
Finishing lessons earns points and keeps a daily streak alive in the learner's own timezone. The learner sees their XP total and where they sit against colleagues this month and this year. Overtaking someone, or being overtaken, triggers a push notification.
- Entry points
GET /v1/mobile/xp-summary,GET /v1/mobile/leaderboard; the streak scanner and leaderboard diff run in the background- Touches
user_points,user_points_history,user_streaks,user_streak_stats,user_leaderboard_snapshot,leaderboard_event_cooldown; NATSlearning.leaderboard.*,learning.streak.at_risk.*- Related
- STREAKS_AND_POINTS.md
- Evidence
- app/routes/progress_mobile_routes.py:1833, app/routes/mobile_routes.py:198, app/services/points_service.py, app/services/leaderboard_diff_service.py:660; caller: users-auth-service/app/routes/mobile_login.py:539-540; bug-hunt #1/#11/#16 fixed 500s and an N+1 here
Certificates live
Completing a module issues a certificate in the same request that records the completion — there is no certificate worker any more. Two renders are uploaded, one sized for the phone and one for the console, and a startup backfill fills in certificates whose render never landed.
- Entry points
GET /v1/mobile/certificates,GET /v1/mobile/certificate/{module_id},GET /v1/admin/users/{target_user_id}/certificates,GET /v1/admin/certificates/{certificate_id},GET /v1/internal/users/{user_id}/certificates- Touches
certificates; asset-manager for the SVG renders- Related
- overview card
GET /v1/admin/overview/certificates/recent - Evidence
- app/services/certificate_service.py:390 (backfill), app/routes/progress_mobile_routes.py:1428 (issued inline on module completion), app/main.py:96 (startup backfill task); no
certificate-workersource exists in the repo
Internal (other services)
Take a module in from authoring live
authoring-service-v2 is where AI-assisted courses are written. When an author publishes, authoring pushes the whole tree here in one call and this service becomes the system of record for it. Unpublish and delete come back the same way, so authoring can undo a publish.
- Entry points
POST /v1/internal/modules/ingest,POST /v1/internal/modules/by-root/{root_id}/unpublish,DELETE /v1/internal/modules/by-root/{root_id},DELETE /v1/internal/modules/{module_id}- Touches
modules,sections,lessons,lesson_screens,lesson_blocks- Related
- ARCHITECTURE.md §3.2 edge H1
- Evidence
- app/routes/internal_routes.py:100; caller: authoring-service-v2/app/core/http.py:56, :197, :217, :235
Translations written back by content-worker live
A module can be offered in more than one language. content-worker pulls the source text from here, translates it, and writes each translated lesson and entity back, then marks the language ready. The mobile player then serves the learner's language automatically.
- Entry points
GET /v1/internal/translations/modules/{root_id}/source,PUT /v1/internal/translations/modules/{module_id}/{locale}/lessons/{lesson_id},PUT /v1/internal/translations/modules/{module_id}/{locale}/entities,POST /v1/internal/translations/modules/{module_id}/{locale}/status,GET /v1/internal/translations/modules/{module_id}/{locale}- Touches
lesson_block_translations,content_entity_translations,module_translation_status- Related
- ARCHITECTURE.md §3.2 edges H7 and H1
- Evidence
- app/routes/internal_routes.py:488-624; callers: authoring-content-worker/app/services/ml_client.py:103,121,136,154; authoring-service-v2/app/core/http.py:146,172
Answer other services' questions about a learner live
users-auth builds the back office and the mobile profile from these reads: how much training a user has done, their certificates, their activity, tenant-wide usage series, and the monthly-active-learner count that billing charges on. assignment-service reads module scores to decide whether an assignment passed. GDPR export hands over one user's whole learning record.
- Entry points
/v1/internal/users/{user_id}/training-stats,/certificates,/activity,/v1/internal/analytics/*,GET /v1/internal/billing/active-learners,POST /v1/internal/progress/module-scores,GET /v1/internal/export/{user_id}- Touches
user_lesson_progress,user_module_progress,certificates,user_points,user_streak_stats,lesson_sessions- Related
- ARCHITECTURE.md §3.2 edges H16–17 and H19
- Evidence
- app/routes/internal_routes.py:662,799,884,1265; callers: users-auth-service/app/clients/training_clients.py:134,149,167, users-auth-service/app/clients/backoffice_clients.py:103,123,172, users-auth-service/app/services/billing.py:265, users-auth-service/app/services/gdpr_export.py:156, assignment-service/app/services/training_service.py:37
Background
Turn lesson text into narration live
When a module is published with narration, or a learner opens a screen whose audio is missing, the service records a pending audio row and asks for it over NATS. tts-worker-v2 — a separate pod built from this repo — picks the request up, synthesises the speech on a Modal GPU, uploads the clip and its word timings through asset-manager, and writes the result straight back into the shared table. There is no completion event: the next read of the screen simply finds the audio there.
- Entry points
- publish →
app/services/screen_audio_service.py:108publishestts.audio.requested; tts-worker-v2 consumes it on streamTRAINING_AUDIO, durableaudio-gen-worker-v2-q - Touches
lesson_screen_audio(claimed withSELECT … FOR UPDATE SKIP LOCKED),lesson_screens,lesson_blocks; Modal chatterbox endpoint; asset-manager- Related
- tts-worker-v2.md; ARCHITECTURE.md §3.3 (Audio flow) and §3.4 (shared database)
- Evidence
- app/tts_worker/worker.py:115 (subscribe), app/tts_worker/pipeline.py:224 (SKIP LOCKED claim), app/tts_worker/pipeline.py:939 (asset-manager upload); Deployment k8s/tts-worker-v2-deployment.yaml:51
Chase a streak before it breaks live
Every hour the service looks for learners whose streak will lapse today in their own timezone and publishes one nudge per learner per local day. notification-worker turns that into a push, respecting quiet hours.
- Entry points
StreakReminderWorkerloop (hourly); publisheslearning.streak.at_risk.ten.<tenant>- Touches
user_streak_stats,tenant_streak_settings- Related
- STREAKS_AND_POINTS.md. ARCHITECTURE.md §4.1(3) is out of date: it reports a flat
learning.streak.at_riskthat could never match the consumer. The code publishes the five-token subject and the filter matches — see the evidence line; do not re-raise it. - Evidence
- app/services/streak_reminder_scheduler.py:183 builds
f"{self.subject_root}.ten.{user.tenant_id}"withsubject_root = settings.streak_reminder_subject=learning.streak.at_risk(app/core/config.py:77) and publishes at :209; consumer notification-worker/app/services/notification_service.py:1807 filterslearning.streak.at_risk.ten.*on durablenotification_worker_streak_v2
Repair stuck audio and abandoned sessions live
Two janitors keep the data honest: one re-requests audio rows that have been stuck generating for too long, the other closes lesson sessions a learner walked away from so their time on task is not counted forever.
- Entry points
AudioReconcilerWorker(every 5 min),SessionSweeperWorker(every 10 min)- Touches
lesson_screen_audio,lesson_sessions- Related
- manual equivalents exist under
/v1/internal/audio/*and/v1/internal/sessions/sweep-stalebut nothing calls them (§8) - Evidence
- app/main.py:103-111 starts both; app/workers/audio_reconciler_worker.py:33, app/workers/session_sweeper_worker.py:33; intervals app/core/config.py:118,129
4. API reference
All 109 scanned routes. Admin and mobile routes reach the service through the
shared auth.useoper.com ingress; /v1/internal/* has no ingress rule and
is reachable only from inside the cluster.
| Method | Path | Auth | Feature | Callers | Verdict |
|---|---|---|---|---|---|
| GET | /health/live | none | Platform health | kubelet liveness probe - k8s/deployment.yaml:88 | live |
| GET | /health/ready | none | Platform health | kubelet readiness probe - k8s/deployment.yaml:78 | live |
| GET | /healthz | none | TTS worker health | kubelet liveness probe - k8s/tts-worker-v2-deployment.yaml:181 | live |
| GET | /readyz | none | TTS worker health | kubelet readiness probe - k8s/tts-worker-v2-deployment.yaml:174 | live |
| GET | /v1/admin/audio/voice-settings | admin JWT | Narration voice | admin web - docs/tenant-voice-fe.md | live |
| PUT | /v1/admin/audio/voice-settings | admin JWT + settings.write | Narration voice | admin web - docs/tenant-voice-fe.md | live |
| POST | /v1/admin/audio/voice-upload-url | admin JWT + settings.write | Narration voice | admin web - docs/tenant-voice-fe.md | live |
| POST | /v1/admin/audio/voice-uploads/{asset_id}/complete | admin JWT + settings.write | Narration voice | admin web - docs/tenant-voice-fe.md | live |
| GET | /v1/admin/audio/voices | admin JWT | Narration voice | admin web - docs/tenant-voice-fe.md | live |
| GET | /v1/admin/certificates/{certificate_id} | admin JWT | Certificates | none found | suspect |
| GET | /v1/admin/lessons/{lesson_id}/blocks | admin JWT | Lesson blocks | admin web - API_ENDPOINTS.md:79 | live |
| POST | /v1/admin/lessons/{lesson_id}/blocks | admin JWT + content.publish | Lesson blocks | admin web - API_ENDPOINTS.md:81 | live |
| PUT | /v1/admin/lessons/{lesson_id}/blocks | admin JWT + content.publish | Lesson blocks | admin web - API_ENDPOINTS.md:80 | live |
| PATCH | /v1/admin/lessons/{lesson_id}/blocks/{block_id} | admin JWT + content.publish | Lesson blocks | admin web - API_ENDPOINTS.md:82 | live |
| DELETE | /v1/admin/lessons/{lesson_id}/blocks/{block_id} | admin JWT + content.publish | Lesson blocks | admin web - API_ENDPOINTS.md:83 | live |
| POST | /v1/admin/lessons/{lesson_id}/move | admin JWT + content.publish | Lessons | admin web - API_ENDPOINTS.md:71 | live |
| GET | /v1/admin/lessons/{lesson_id}/quiz | admin JWT | Quiz bank (legacy) | admin web - API_ENDPOINTS.md:104 | live |
| POST | /v1/admin/lessons/{lesson_id}/quiz | admin JWT + content.publish | Quiz bank (legacy) | admin web - API_ENDPOINTS.md:103 | live |
| GET | /v1/admin/lessons/{lesson_id}/quiz/{question_id} | admin JWT | Quiz bank (legacy) | admin web - API_ENDPOINTS.md:105 | live |
| PATCH | /v1/admin/lessons/{lesson_id}/quiz/{question_id} | admin JWT + content.publish | Quiz bank (legacy) | admin web - API_ENDPOINTS.md:106 | live |
| DELETE | /v1/admin/lessons/{lesson_id}/quiz/{question_id} | admin JWT + content.publish | Quiz bank (legacy) | admin web - API_ENDPOINTS.md:107 | live |
| GET | /v1/admin/lessons/{lesson_id}/screens | admin JWT | Lesson screens | admin web - API_ENDPOINTS.md:91 | live |
| POST | /v1/admin/lessons/{lesson_id}/screens | admin JWT + content.publish | Lesson screens | admin web - API_ENDPOINTS.md:93 | live |
| PUT | /v1/admin/lessons/{lesson_id}/screens | admin JWT + content.publish | Lesson screens | admin web - API_ENDPOINTS.md:92 | live |
| PATCH | /v1/admin/lessons/{lesson_id}/screens/{screen_id} | admin JWT + content.publish | Lesson screens | admin web - API_ENDPOINTS.md:94 | live |
| DELETE | /v1/admin/lessons/{lesson_id}/screens/{screen_id} | admin JWT + content.publish | Lesson screens | admin web - API_ENDPOINTS.md:95 | live |
| GET | /v1/admin/media | admin JWT | Media library | admin web - API_ENDPOINTS.md:115 | live |
| GET | /v1/admin/media/{media_id} | admin JWT | Media library | admin web - API_ENDPOINTS.md:116 | live |
| PATCH | /v1/admin/media/{media_id} | admin JWT + content.publish | Media library | admin web - API_ENDPOINTS.md:118 | live |
| DELETE | /v1/admin/media/{media_id} | admin JWT + content.publish | Media library | admin web - API_ENDPOINTS.md:119 | live |
| POST | /v1/admin/media/{media_id}/mark-uploaded | admin JWT + content.publish | Media library | admin web - API_ENDPOINTS.md:117 | live |
| GET | /v1/admin/modules | admin JWT | Module tree | admin web - API_ENDPOINTS.md:39 | live |
| POST | /v1/admin/modules | admin JWT + content.publish | Module tree | admin web - API_ENDPOINTS.md:38 | live |
| GET | /v1/admin/modules/{module_id} | admin JWT | Module tree | assignment-service/app/services/training_service.py:88 | live |
| PATCH | /v1/admin/modules/{module_id} | admin JWT + content.publish | Module tree | admin web - API_ENDPOINTS.md:41 | live |
| DELETE | /v1/admin/modules/{module_id} | admin JWT + content.publish | Module deletion | admin web - docs/MODULE_DELETION_STRATEGY.md | live |
| POST | /v1/admin/modules/{module_id}/duplicate | admin JWT + content.publish | Module tree | admin web - API_ENDPOINTS.md:43 | live |
| POST | /v1/admin/modules/{module_id}/publish | admin JWT + content.publish | Publish | authoring-service-v2/app/core/http.py:108 | live |
| GET | /v1/admin/modules/{module_id}/sections | admin JWT | Sections | assignment-service/app/services/training_service.py:206 | live |
| POST | /v1/admin/modules/{module_id}/sections | admin JWT + content.publish | Sections | admin web - API_ENDPOINTS.md:52 | live |
| POST | /v1/admin/modules/{module_id}/sections/reorder | admin JWT + content.publish | Sections | admin web - API_ENDPOINTS.md:57 | live |
| GET | /v1/admin/modules/{module_id}/sections/{section_id} | admin JWT | Sections | admin web - API_ENDPOINTS.md:54 | live |
| PATCH | /v1/admin/modules/{module_id}/sections/{section_id} | admin JWT + content.publish | Sections | admin web - API_ENDPOINTS.md:55 | live |
| DELETE | /v1/admin/modules/{module_id}/sections/{section_id} | admin JWT + content.publish | Sections | admin web - API_ENDPOINTS.md:56 | live |
| GET | /v1/admin/modules/{module_id}/sections/{section_id}/lessons | admin JWT | Lessons | assignment-service/app/services/training_service.py:226 | live |
| POST | /v1/admin/modules/{module_id}/sections/{section_id}/lessons | admin JWT + content.publish | Lessons | admin web - API_ENDPOINTS.md:65 | live |
| POST | /v1/admin/modules/{module_id}/sections/{section_id}/lessons/reorder | admin JWT + content.publish | Lessons | admin web - API_ENDPOINTS.md:70 | live |
| GET | /v1/admin/modules/{module_id}/sections/{section_id}/lessons/{lesson_id} | admin JWT | Lessons | admin web - API_ENDPOINTS.md:67 | live |
| PATCH | /v1/admin/modules/{module_id}/sections/{section_id}/lessons/{lesson_id} | admin JWT + content.publish | Lessons | admin web - API_ENDPOINTS.md:68 | live |
| DELETE | /v1/admin/modules/{module_id}/sections/{section_id}/lessons/{lesson_id} | admin JWT + content.publish | Lessons | admin web - API_ENDPOINTS.md:69 | live |
| GET | /v1/admin/modules/{module_id}/translations | admin JWT | Translations | users-auth-service/app/routes/oper_training.py:117 | live |
| POST | /v1/admin/modules/{module_id}/unpublish | admin JWT + content.publish | Publish | admin web - docs/CREATE_MODULE_DIRECT.md | live |
| GET | /v1/admin/modules/{module_id}/versions | admin JWT | Versioning | admin web - API_ENDPOINTS.md:127 | live |
| GET | /v1/admin/modules/{root_id}/draft | admin JWT | Versioning | admin web - API_ENDPOINTS.md:129 | live |
| GET | /v1/admin/modules/{root_id}/latest | admin JWT | Versioning | authoring-service-v2/app/core/http.py:247 | live |
| GET | /v1/admin/overview/assignments/active | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:314 | live |
| GET | /v1/admin/overview/certificates/recent | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:318 | live |
| GET | /v1/admin/overview/kpis | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:312 | live |
| GET | /v1/admin/overview/leaderboard | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:319 | live |
| GET | /v1/admin/overview/learning-time/daily | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:315 | live |
| GET | /v1/admin/overview/modules/recent | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:313 | live |
| GET | /v1/admin/overview/score-distribution | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:317 | live |
| GET | /v1/admin/overview/team-pulse | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:320 | live |
| GET | /v1/admin/overview/users/activity | admin JWT | Overview dashboard | admin web - docs/ADMIN_OVERVIEW_DASHBOARD_FE.md:316 | live |
| GET | /v1/admin/progress/assignments/{assignment_id}/analytics | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:142 | live |
| POST | /v1/admin/progress/assignments/{assignment_id}/reports | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:140 | live |
| GET | /v1/admin/progress/assignments/{assignment_id}/summary | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:137 | live |
| GET | /v1/admin/progress/assignments/{assignment_id}/users | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:138 | live |
| GET | /v1/admin/progress/reports/{report_id}/download | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:141 | live |
| POST | /v1/admin/progress/users/bulk | admin JWT | Assignment progress reports | admin web - API_ENDPOINTS.md:139 | live |
| GET | /v1/admin/streak-settings | admin JWT | Streak settings | admin web - docs/STREAKS_AND_POINTS.md:175 | live |
| PUT | /v1/admin/streak-settings | admin JWT + settings.write | Streak settings | admin web - docs/STREAKS_AND_POINTS.md:176 | live |
| GET | /v1/admin/users/{target_user_id}/certificates | admin JWT | Certificates | none found | suspect |
| POST | /v1/internal/analytics/last-seen | X-Oper-Key + X-Tenant-Id | Back-office analytics | users-auth-service/app/clients/backoffice_clients.py:123 | live |
| POST | /v1/internal/analytics/tenant-activity | X-Oper-Key | Back-office analytics | users-auth-service/app/clients/backoffice_clients.py:172 | live |
| GET | /v1/internal/analytics/usage-series | X-Oper-Key + X-Tenant-Id | Back-office analytics | users-auth-service/app/clients/backoffice_clients.py:103 | live |
| POST | /v1/internal/audio/prewarm/{module_id} | admin JWT | Audio ops | none found | dead |
| POST | /v1/internal/audio/reconcile | admin JWT | Audio ops | none found | dead |
| POST | /v1/internal/audio/retry-failed | admin JWT | Audio ops | none found | dead |
| GET | /v1/internal/billing/active-learners | X-Oper-Key + X-Tenant-Id | Billing MAU | users-auth-service/app/services/billing.py:265 | live |
| GET | /v1/internal/export/{user_id} | X-Oper-Key + X-Tenant-Id | GDPR export | users-auth-service/app/services/gdpr_export.py:156 | live |
| DELETE | /v1/internal/modules/by-root/{root_id} | X-Oper-Key + X-Tenant-Id | Module deletion | authoring-service-v2/app/core/http.py:217 | live |
| POST | /v1/internal/modules/by-root/{root_id}/unpublish | X-Oper-Key + X-Tenant-Id | Publish | authoring-service-v2/app/core/http.py:197 | live |
| POST | /v1/internal/modules/ingest | X-Oper-Key + X-Tenant-Id | Module ingest | authoring-service-v2/app/core/http.py:56 | live |
| POST | /v1/internal/modules/versions/batch | admin JWT | Versioning | none found | dead |
| DELETE | /v1/internal/modules/{module_id} | X-Oper-Key + X-Tenant-Id | Module deletion | authoring-service-v2/app/core/http.py:235 | live |
| POST | /v1/internal/narration/quote | X-Oper-Key + X-Tenant-Id | Narration credits | authoring-service-v2/app/core/http.py:83 | live |
| POST | /v1/internal/progress/module-scores | X-Oper-Key + X-Tenant-Id | Score lookup | assignment-service/app/services/training_service.py:37 | live |
| POST | /v1/internal/sessions/sweep-stale | admin JWT | Session ops | none found | dead |
| GET | /v1/internal/translations/modules/{module_id}/{locale} | X-Oper-Key + X-Tenant-Id | Translations | authoring-service-v2/app/core/http.py:172 | live |
| PUT | /v1/internal/translations/modules/{module_id}/{locale}/entities | X-Oper-Key + X-Tenant-Id | Translations | authoring-content-worker/app/services/ml_client.py:136 | live |
| PUT | /v1/internal/translations/modules/{module_id}/{locale}/lessons/{lesson_id} | X-Oper-Key + X-Tenant-Id | Translations | authoring-content-worker/app/services/ml_client.py:121 | live |
| POST | /v1/internal/translations/modules/{module_id}/{locale}/status | X-Oper-Key + X-Tenant-Id | Translations | authoring-content-worker/app/services/ml_client.py:154 | live |
| GET | /v1/internal/translations/modules/{root_id}/source | X-Oper-Key + X-Tenant-Id | Translations | authoring-service-v2/app/core/http.py:146; authoring-content-worker/app/services/ml_client.py:103 | live |
| GET | /v1/internal/users/{user_id}/activity | X-Oper-Key + X-Tenant-Id | Learner stats | users-auth-service/app/clients/training_clients.py:134 | live |
| GET | /v1/internal/users/{user_id}/certificates | X-Oper-Key + X-Tenant-Id | Learner stats | users-auth-service/app/clients/training_clients.py:167 | live |
| GET | /v1/internal/users/{user_id}/training-stats | X-Oper-Key + X-Tenant-Id | Learner stats | users-auth-service/app/clients/training_clients.py:149 | live |
| GET | /v1/mobile/certificate/{module_id} | learner JWT | Certificates | none found | suspect |
| GET | /v1/mobile/certificates | learner JWT | Certificates | ingress rule only | suspect |
| GET | /v1/mobile/leaderboard | learner JWT | Leaderboard | users-auth-service/app/routes/mobile_login.py:540 | live |
| POST | /v1/mobile/lessons/{lesson_id}/quiz/questions/{question_id}/attempts | learner JWT | Quiz attempts | mobile app - docs/FRONTEND_INTEGRATION_GUIDE.md:339 | live |
| GET | /v1/mobile/lessons/{lesson_id}/screens | learner JWT | Lesson player | mobile app - docs/FRONTEND_INTEGRATION_GUIDE.md:207 | live |
| GET | /v1/mobile/lessons/{lesson_id}/summary | learner JWT | Lesson player | assignment-service/docs/question-retry-settings.md:57 | live |
| GET | /v1/mobile/modules | learner JWT | Catalogue | users-auth-service/app/routes/mobile_login.py:538 | live |
| GET | /v1/mobile/modules/{module_id} | learner JWT | Catalogue | mobile app - docs/FRONTEND_INTEGRATION_GUIDE.md:126 | live |
| GET | /v1/mobile/modules/{module_id}/sections | learner JWT | Catalogue | mobile app - docs/FRONTEND_INTEGRATION_GUIDE.md:201 | live |
| POST | /v1/mobile/progress/events | learner JWT | Progress events | mobile app - docs/MOBILE_FRONTEND_EVENTS_GUIDE.md:6 | live |
| GET | /v1/mobile/recent | learner JWT | Catalogue | ingress rule only | suspect |
| GET | /v1/mobile/xp-summary | learner JWT | Points and streaks | users-auth-service/app/routes/mobile_login.py:539 | live |
5. Async contracts
Consumes
| Subject | Stream | Durable | Published by | Feature | Verdict |
|---|---|---|---|---|---|
tts.audio.requested | TRAINING_AUDIO | audio-gen-worker-v2-q | this service — app/services/screen_audio_service.py:108, app/services/audio_reconciler_scheduler.py:127 | Narration audio | live app/tts_worker/worker.py:115; NATS_USE_JETSTREAM=true k8s/tts-worker-v2-deployment.yaml:109 |
tts.audio.requested (core-NATS fallback) | none | queue audio-gen-workers-v2, no durable | same | Narration audio (local dev) | dead app/tts_worker/worker.py:128 runs only when nats_use_jetstream is false; the Deployment sets it true (k8s/tts-worker-v2-deployment.yaml:109-110), so this branch never executes in a deployed environment |
Publishes
Fifteen publish call sites. learning.streak.at_risk.ten.<tenant> is
built from an instance attribute, so the scanner cannot resolve it and it is absent from the scan
JSON — it is nonetheless a real published subject and is listed here.
| Subject | Consumed by | Feature | Verdict |
|---|---|---|---|
tts.audio.requestedapp/services/screen_audio_service.py:108 | tts-worker-v2 (app/tts_worker/worker.py:115) | Narration audio | live |
tts.audio.requestedapp/services/audio_reconciler_scheduler.py:127 | tts-worker-v2 (app/tts_worker/worker.py:115) | Audio reconciler | live |
training.module.completedapp/events/events.py:80 | assignment-service durable progress_module_completion_worker (assignment-service/app/workers/progress_event_worker.py:137) | Module completion | live |
training.progress.assignment_progressapp/events/events.py:125 | assignment-service durable progress_event_worker, filter training.progress.> (assignment-service/app/workers/progress_event_worker.py:110) | Progress mirror | live |
learning.leaderboard.<event>.ten.<tenant>app/services/leaderboard_diff_service.py:662-664 (scan JSON records it as learning.leaderboard.*.ten.*) | notification-worker durable notification_worker_leaderboard, filter learning.leaderboard.> (notification-worker/app/core/config.py:100, notification-worker/app/services/notification_service.py:2297) | Leaderboard nudges | live |
learning.streak.at_risk.ten.<tenant>app/services/streak_reminder_scheduler.py:183,209 — absent from the scan JSON (f-string off self.subject_root) | notification-worker durable notification_worker_streak_v2, filter learning.streak.at_risk.ten.* (notification-worker/app/services/notification_service.py:1807) | Streak nudges | live five tokens on both sides; ARCHITECTURE.md §4.1(3), which calls this unroutable, is stale |
training.module.publishedapp/events/events.py:55 | feed-service maps it to a NEW_MODULE post (feed-service/app/services/event_processor.py:309) but nothing ever starts that consumer | Publish | suspect publisher is on the live publish path (app/routes/module_routes.py:276); the only subscriber is feed-service's EventProcessorService, which is never instantiated — ARCHITECTURE.md §4.1(5) |
training.certificates.module_completedapp/events/events.py:257 | same feed-service CERTIFICATE_EARNED mapping (feed-service/app/services/event_processor.py:309); consumer never started | Certificates | suspect emitted at app/routes/progress_mobile_routes.py:1428; no running consumer — ARCHITECTURE.md §4.1(5) |
training.module.createdapp/events/events.py:30 | none — no service subscribes to it | Module tree | dead emitted at app/routes/module_routes.py:44; a repo-wide search finds the string only in feed-service seed/undo migrations (feed-service/migrations/versions/fix_feed_category.py re-points those rows to training.module.published), never in a subscribe call |
learning.lesson.startedapp/routes/progress_mobile_routes.py:783 | none | Progress events | dead captured by MICRO_LEARNING via learning.> (nats/streams/training-modules.json) but no consumer filter in any service matches learning.lesson.* |
learning.lesson.progressedapp/routes/progress_mobile_routes.py:900 | none | Progress events | dead as above — published, stored, never consumed |
learning.lesson.completedapp/routes/progress_mobile_routes.py:1264 | none (completion reaches assignment-service over training.module.completed instead) | Progress events | dead as above |
learning.lesson.abandonedapp/routes/progress_mobile_routes.py:1514 | none | Progress events | dead as above |
training.module.archivedapp/events/events.py:142 | none | — | dead its publisher emit_module_archived has zero call sites in the service, and no consumer subscribes to the subject |
training.module.assignedapp/events/events.py:167 | none | — | deademit_module_assigned has zero call sites; assignments are owned by assignment-service, which publishes its own events |
user.notification.trainingapp/events/events.py:186 | none | — | deademit_notification_for_user has zero call sites; its only would-be caller assign_module_to_employees_sync (app/events/events.py:197) is itself uncalled, and notification-worker's stream copy omits user.notification.> entirely (ARCHITECTURE.md §4.1(4)) |
Background jobs
| Job | Schedule | What it does | Verdict |
|---|---|---|---|
StreakReminderWorker._loopapp/workers/streak_reminder_worker.py:36 | every 3600s | Finds learners about to lose a streak and publishes one nudge each | live started app/main.py:98-101; interval app/core/config.py:75 |
AudioReconcilerWorker._loopapp/workers/audio_reconciler_worker.py:33 | every 300s | Re-requests narration rows stuck in processing | live started app/main.py:103-106; interval app/core/config.py:118 |
SessionSweeperWorker._loopapp/workers/session_sweeper_worker.py:33 | every 600s | Closes lesson sessions the learner abandoned | live started app/main.py:108-111; interval app/core/config.py:129 |
_run_certificate_backfillapp/main.py:96 | once at startup | Renders certificates whose asset never uploaded | live gated on certificate_backfill_on_startup (default true, app/core/config.py:65) and the internal API key |
_disarm_hang_watchdogapp/main.py:114 | once at startup | Cancels the 75s faulthandler stack dump armed at import, so only a wedged event loop dumps | live app/main.py:22 arms it, :114 schedules the disarm |
LeaderboardDiffService.on_points_awardedapp/routes/progress_mobile_routes.py:1256 | after every points award | Recomputes the board and emits overtake / rank-one notifications | live scheduled inside _handle_lesson_completed; bug-hunt #15 notes the task reference is not retained |
_emit_audio_requested._runnerapp/services/screen_audio_service.py:126 | per missing screen audio | Publishes the TTS request from whichever thread found the gap | live loop pinned by bind_event_loop() at app/main.py:87-88 |
spawn_module_audio_prewarm._runnerapp/services/screen_audio_service.py:625 | per publish with narration | Enumerates a module's screens and queues every narration job after the publish commits | live called from app/routes/module_routes.py:297 |
AudioGenWorker._bounded_handleapp/tts_worker/worker.py:140 | per NATS message | Runs one narration generation under a concurrency semaphore | live dispatched from the consume loop, app/tts_worker/worker.py:183 |
_shutdownapp/tts_worker/main.py:53 | on SIGINT / SIGTERM | Drains in-flight generations before the pod exits | live registered as a signal handler at app/tts_worker/main.py:53 |
_fire_and_forget(coro)app/events/events.py:192 | never | Would run assign_module_to_employees_sync's notification fan-out | dead its only caller is assign_module_to_employees_sync (app/events/events.py:197), which has zero call sites anywhere in the repo |
6. Data it owns
Postgres logical DB microlearning, 35 alembic revisions under
migrations/versions/. Every table is written by this service except where noted.
| Table | What it holds | Written by |
|---|---|---|
modules | A course: title, status, version, root_id identity across versions, audio_enabled, narration locales | micro-learning |
sections | Chapters inside a module, ordered | micro-learning |
lessons | Lessons inside a section, ordered; pass mark and retry rules | micro-learning |
lesson_screens | The screens a learner swipes through in a lesson | micro-learning |
lesson_blocks | The content on a screen: text, image, quiz, quiz graph | micro-learning |
media_assets | Images and videos referenced by blocks, registered against asset-manager | micro-learning |
lesson_screen_audio | Narration clip and word-timings sidecar per screen and locale, with a pending / processing / ready / failed lifecycle | micro-learning and tts-worker-v2 — the worker claims rows with SELECT … FOR UPDATE SKIP LOCKED and writes the result (app/tts_worker/pipeline.py:224; ARCHITECTURE.md §3.4) |
voice_catalog, tenant_audio_settings | Available narration voices and the tenant's chosen one | micro-learning |
quiz_questions, quiz_answers | Legacy server-side quiz bank; block-authored quizzes do not use it (bug-hunt #19) | micro-learning |
quiz_graph_answer_keys | Answer keys for quiz-graph questions, deliberately kept off the device | micro-learning |
lesson_sessions | One learner's run at a lesson: start, last screen, duration, status | micro-learning |
lesson_screen_dwell | Per-screen time-on-task telemetry | micro-learning |
user_lesson_progress, user_module_progress | What the learner has finished and their score | micro-learning |
quiz_question_attempts | Every answer submitted, right or wrong | micro-learning |
user_points, user_points_history | XP total and the ledger behind it | micro-learning |
user_streaks, user_streak_stats, tenant_streak_settings | Current and longest streak, the learner's timezone, and which weekdays count | micro-learning |
user_leaderboard_snapshot, leaderboard_event_cooldown | Last known rank per learner and the anti-spam window for overtake pushes | micro-learning |
certificates | One issued certificate per learner and module, with mobile and admin renders | micro-learning |
lesson_block_translations, content_entity_translations, module_translation_status | Translated text and per-language readiness | micro-learning — rows written through the internal API by content-worker |
suggestion_runs, suggestions, suggestion_dismissals | Content suggestions attached to a module | micro-learning |
7. Dependencies
flowchart LR
AV2["authoring-service-v2"] --> ML["micro-learning-service-v2"]
CW["content-worker"] --> ML
AS["assignment-service"] --> ML
UA["users-auth-service"] --> ML
ML --> UAO["users-auth: profiles and credits"]
ML --> ASO["assignment-service: employee assignments"]
ML --> AVO["authoring-service-v2: recent modules"]
ML --> AM["asset-manager: uploads"]
ML --> NATS["NATS: MICRO_LEARNING and TRAINING_AUDIO"]
NATS --> TTS["tts-worker-v2"]
TTS --> MODAL["Modal chatterbox TTS"]
TTS --> AM
TTS --> DB[("microlearning DB")]
ML --> DB
Inbound edges H1 (authoring ingest / publish), H7 (content-worker translations), H16–17 (assignment-service progress and module validation) and H19 (users-auth analytics, billing, training stats). Outbound edges H11–12 (users-auth users and the publish-time credit debit), H13 (assignment-service on the mobile read path), H14 (authoring recent-modules card) and H9 (tts-worker-v2 to asset-manager) — ARCHITECTURE.md §3.2.
8. Dead-code verdicts
Every entry point with no in-repo caller. Deleting is a separate decision — see the hub roll-up.
| Entry point | Kind | Verdict | Evidence |
|---|---|---|---|
POST /v1/internal/modules/versions/batch | HTTP | dead | Internal-only path, and no in-repo service calls it: a repo-wide search for versions/batch finds only the route (app/routes/internal_routes.py:227), its test, and scripts/smoke_internal_batch.py. /v1/internal/* has no ingress rule, so no browser or app can reach it either. Confirmed dormant by bug-hunt #18. |
POST /v1/internal/audio/reconcile | HTTP | dead | No caller anywhere in the repo, and the same work runs on a timer (AudioReconcilerWorker, app/main.py:103). Internal path with no ingress rule (global-configs/k8s/users-auth-ingress.yaml has no /v1/internal entry). |
POST /v1/internal/audio/retry-failed | HTTP | dead | As above — no caller (app/routes/internal_routes.py:401), and the reconciler covers the same case. |
POST /v1/internal/audio/prewarm/{module_id} | HTTP | dead | No caller: the publish flow calls spawn_module_audio_prewarm in process (app/routes/module_routes.py:297) instead of this route. Internal path, no ingress rule. |
POST /v1/internal/sessions/sweep-stale | HTTP | dead | No caller; SessionSweeperWorker runs the same sweep every 10 minutes (app/main.py:108). Internal path, no ingress rule. |
GET /v1/admin/certificates/{certificate_id} | HTTP | suspect | Searched the repo for the path and for admin_get_certificate, plus every docs/*.md in this service: no caller, no FE doc. The admin web app is not in this repo, so not dead — but note the ingress has no /v1/admin/certificates rule, so today the call would fall through to the users-auth catch-all. |
GET /v1/admin/users/{target_user_id}/certificates | HTTP | suspect | Same search, same result: no in-repo caller, no FE doc, and no matching ingress rule. Admin-facing, so suspect rather than dead. |
GET /v1/mobile/certificates | HTTP | suspect | No in-repo caller and no FE doc names it; the only hit is the ops script management-scripts/mobile_list_certificates.py. A dedicated ingress rule exists (global-configs/k8s/users-auth-ingress.yaml:111), so the mobile app — which is not in this repo — is expected to call it. |
GET /v1/mobile/certificate/{module_id} | HTTP | suspect | No caller, no FE doc, and no ingress rule matches the singular path (the /v1/mobile/certificates prefix at users-auth-ingress.yaml:111 does not cover it). Mobile-facing, so suspect; worth confirming with the app team. |
GET /v1/mobile/recent | HTTP | suspect | No in-repo caller and no FE doc; only tests/test_recent_modules.py. A dedicated ingress rule was added for it (global-configs/k8s/users-auth-ingress.yaml:118-123), which shows intent but is not proof of traffic. |
tts.audio.requested core-NATS subscribe | NATS consumer | dead | app/tts_worker/worker.py:128 is the else branch of the JetStream subscribe; NATS_USE_JETSTREAM is "true" in the Deployment (k8s/tts-worker-v2-deployment.yaml:109-110), so it is a local-dev-only path. |
training.module.created | NATS publish | dead | Emitted at app/routes/module_routes.py:44; no subscribe call in any service matches it. feed-service's subject list (feed-service/app/services/event_processor.py:308-316) carries training.module.published instead, and its migration re-pointed the old config rows. |
training.module.archived | NATS publish | dead | Publisher emit_module_archived (app/events/events.py:132) has zero call sites, and no consumer subscribes to the subject. |
training.module.assigned | NATS publish | dead | Publisher emit_module_assigned (app/events/events.py:145) has zero call sites; no consumer. |
user.notification.training | NATS publish | dead | Publisher emit_notification_for_user (app/events/events.py:170) has zero call sites, and no consumer subscribes to user.notification.* — notification-worker's stream copy omits the subject (ARCHITECTURE.md §4.1(4)). |
learning.lesson.started / .progressed / .completed / .abandoned | NATS publish | dead | Published on every learner event (app/routes/progress_mobile_routes.py:783, 900, 1264, 1514) and captured by MICRO_LEARNING via learning.>, but no service subscribes to any learning.lesson.* filter. |
training.module.published | NATS publish | suspect | A matching subscriber exists (feed-service/app/services/event_processor.py:309) but EventProcessorService is never instantiated — ARCHITECTURE.md §4.1(5). The event is correct; the consumer is broken. |
training.certificates.module_completed | NATS publish | suspect | Same feed-service consumer, same reason. |
_fire_and_forget and assign_module_to_employees_sync | Background / functions | dead | app/events/events.py:189 and :197; assign_module_to_employees_sync has zero call sites anywhere in the repo, so both it and the helper are unreachable, together with the three uncalled emitters above. |
Doc drift, not a code verdict: API_ENDPOINTS.md still documents
PUT /v1/mobile/lessons/{lesson_id}/session,
GET /v1/mobile/lessons/{lesson_id}/quiz/summary and
POST /v1/mobile/progress/assignments/{assignment_id}/lessons/{lesson_id}/complete.
None of those routes exist in the scan; they were replaced by the single
POST /v1/mobile/progress/events endpoint
(CHANGE_PROGESS_ROUTES.md).
9. Sources
- docs/API_ENDPOINTS.md — admin and mobile route inventory (with the drift noted above)
- docs/FRONTEND_INTEGRATION_GUIDE.md — mobile catalogue and player calls
- docs/MOBILE_FRONTEND_EVENTS_GUIDE.md, docs/CHANGE_PROGESS_ROUTES.md, docs/TEST_CASES_CHANGE_EVENTS_REOUTE.md, docs/mobile/progress-events-telemetry.md — the progress-events contract
- docs/QUIZ_SCORE_TRACKING.md, docs/QUIZ_GRAPH_FLUTTER_INTEGRATION.md, docs/QUIZ_GRAPH_BLOCK_EXAMPLES.md, docs/QUIZ_GRAPH_IMPLEMENTATION_PLAN.md — quizzes and the quiz graph
- docs/BLOCK_TYPES_REFERENCE.md, docs/SCREENS_RESPONSE_EXAMPLE.md — block and screen payloads
- docs/STREAKS_AND_POINTS.md, docs/STREAKS_AND_POINTS_IMPLEMENTATION_PLAN.md — streaks, points, leaderboards
- docs/ADMIN_OVERVIEW_DASHBOARD_FE.md — the nine overview cards
- docs/NARRATION_CREDIT_ESTIMATE_FE.md, docs/tenant-voice-fe.md, docs/tts-worker-v2.md — narration cost, voice selection, the TTS worker
- docs/CREATE_MODULE_DIRECT.md, docs/MODULE_DELETION_STRATEGY.md, docs/RANKING_MODULES.md — module lifecycle and home-screen ranking
- ARCHITECTURE.md §2, §3.2 (H1, H7, H9, H11–14, H16–17, H19), §3.3, §3.4, §4.1
- bug-hunt-reports/micro-learning-service-v2.md — 20 findings, 8 fixed
global-configs/k8s/users-auth-ingress.yaml— which paths actually reach this service