1. What it is
assignment-service is the service that says who has to do which training module by when. An admin picks a module and an audience (one person, a group, or the whole company), gives it a due date, and this service turns that into one row per person, pushes "new training assigned" to their phone, chases them with reminders until they finish, tells admins who is late, and answers every other service's question about a learner's outstanding training.
| Who uses it | Admin web app (through the ingress, /v1/admin/assignments only) · the mobile app indirectly, via micro-learning-service-v2 · users-auth-service, micro-learning-service-v2 and notification-worker server-to-server · its own in-pod schedulers |
| Runtime | FastAPI (uvicorn, sync SQLAlchemy) · Deployment assignment-service, replicas: 1 (k8s/deployment.yaml:9), 5 in-process NATS/scheduler workers started from the app lifespan |
| Database | Postgres logical DB assignments on the shared instance (ARCHITECTURE.md §3.4) · 15 alembic revisions · 12 tables |
| Redis | DB index 4 is assigned and configured (k8s/configmap.yaml:8-11) but nothing in app/ imports redis — the service keeps no cache |
| NATS streams | MICRO_LEARNING only (subjects training.assignments.>, training.progress.>, training.module.completed, training.notifications.>) — shared with micro-learning and notification-worker (ARCHITECTURE.md §3.3) |
| External APIs | None. All outbound traffic is in-cluster HTTP: users-auth-service (groups, members, profiles, timezone), micro-learning-service-v2 (module validation, progress), notification-worker (device check) |
| Entry points | 61 HTTP routes (50 distinct handlers — 11 are mounted twice, see below) · 5 NATS consumer registrations over 3 subjects · 13 published subjects · 9 background tasks |
The double-mounted router
app/main.py:158 mounts assignment_routes.router at /v1/admin and
app/main.py:164 mounts the same router again at /v1. All 11 of its
routes therefore answer on two paths, which is why the scan reports 61 routes for 50 handlers. Exactly one
path of each pair is useful: the 10 admin routes are reachable only under /v1/admin/assignments…
(the ingress rule, global-configs/k8s/users-auth-ingress.yaml:76-82), and the one employee route
is called only at /v1/employee/assignments (micro-learning-service-v2,
app/clients/assignment_service.py:31). The other 11 paths — /v1/assignments… and
/v1/admin/employee/assignments — have no caller and no route to the outside world. The fix is to
split the router in two (admin + employee) rather than to drop a mount, because dropping /v1
would take the live employee route with it. See §8.
2. Feature map
flowchart LR ADMIN["Admin web app"] --> ASSIGN["Assign training to people"] ADMIN --> TRACK["See who has done it"] EMP["Employee mobile app"] --> MINE["My assignments"] SVC["Other services"] --> ASK["Internal lookups"] SCHED["In-pod schedulers"] --> NUDGE["Reminders, escalation, digest"] ASSIGN --> R1["POST /v1/admin/assignments"] ASSIGN --> W1["expansion_worker (NATS)"] TRACK --> R2["GET /v1/admin/assignments/:id"] MINE --> R3["GET /v1/employee/assignments"] ASK --> R4["12 internal endpoints"] NUDGE --> W2["reminder + digest loops"]
3. Features
Admin
Assign a module to a person, a group, or everyone live
An admin picks a published module, picks an audience, sets a start and a due date (the due date
is mandatory), and optionally marks it required, sets a priority, or overrides the quiz retry/passing rules.
Naming individual employees materialises their rows straight away, so the list is correct the moment the call
returns. A group or company audience is resolved in the background instead (next feature), and the response
says expansion_status: "pending" so the UI can show "being rolled out".
- Entry points
POST /v1/admin/assignments· duplicate mountPOST /v1/assignments(dead)- Touches
assignments,assignment_targets,assignment_recipients; micro-learning (module validation), users-auth (target validation); publishesassignment.created- Related
- BUSINESS_LOGIC.md §Assignment Creation Flow · question-retry-settings.md
- Evidence
- app/routes/assignment_routes.py:56 · app/services/assignment_service.py:282 · docs/BUSINESS_LOGIC.md:70
Change the deadline, the priority, or the audience live
An admin can move the due date, raise the priority, flip "required", or replace the target list on a live assignment. Replacing the targets re-runs the audience resolution: new people get added and pushed, people who left the audience are retired. A cancelled assignment can no longer be edited.
- Entry points
PATCH /v1/admin/assignments/{assignment_id}· duplicate mountPATCH /v1/assignments/{assignment_id}(dead)- Touches
assignments,assignment_targets; publishesassignment.updatedand, on a target change,assignment.retargeted- Related
- BUSINESS_LOGIC.md §Assignment Update Flow
- Evidence
- app/routes/assignment_routes.py:463 · app/services/assignment_service.py:790,819 · docs/BUSINESS_LOGIC.md:122
Cancel an assignment live
Cancelling is a soft delete: the assignment stays in the database with status =
"canceled", who cancelled it and when, so history and reports survive. An assignment everybody already
finished cannot be cancelled (422) — you do not un-assign completed training.
- Entry points
DELETE /v1/admin/assignments/{assignment_id}· duplicate mountDELETE /v1/assignments/{assignment_id}(dead)- Touches
assignments; publishesassignment.canceled- Related
- assignment-details-api-gaps.md §notes; known defect: the "everyone completed" check reads at most 1000 progress rows (to-be-reviewed AS-A14)
- Evidence
- app/routes/assignment_routes.py:508 · app/services/assignment_service.py:982 · docs/BUSINESS_LOGIC.md:145
The assignment list and its counters live
The main admin screen: every assignment of the tenant, filterable by module, status and type, paginated, each row carrying a completed/assignees ratio. A separate counters call feeds the dashboard tiles (active, overdue, completed).
- Entry points
GET /v1/admin/assignments(live) ·GET /v1/admin/assignments/stats(suspect — no doc or in-repo caller names it) · duplicate mountsGET /v1/assignments,GET /v1/assignments/stats(dead)- Touches
assignments,assignment_recipients,assignment_progress- Related
- BUSINESS_LOGIC.md §Assignment Listing
- Evidence
- app/routes/assignment_routes.py:126,168 · docs/BUSINESS_LOGIC.md:165,504
One assignment in detail: audience, progress, history live
Opening an assignment shows who created it, the module card, the audience as the admin
described it, a completion summary and trend, the per-person roster with status and search, and a typed
activity feed ("assigned", "reminder sent", "completed", "due date changed"). Cross-service blocks degrade to
null rather than failing the screen when an upstream is down.
- Entry points
GET /v1/admin/assignments/{assignment_id},GET /v1/admin/assignments/{assignment_id}/assignees,GET /v1/admin/assignments/{assignment_id}/activity· duplicate mounts under/v1/assignments/…(dead)- Touches
assignments,assignment_recipients,assignment_progress,assignment_events; users-auth (profiles), micro-learning (module card)- Related
- assignment-details-api-gaps.md (what shipped vs what was deliberately not built)
- Evidence
- app/routes/assignment_routes.py:194,247,297 · docs/assignment-details-api-gaps.md:12-15 · docs/manual-reminders-fe-integration.md:231-234
Nudge people now, or schedule the nudge live
An admin can send an extra reminder outside the automatic cadence: pick the audience (everyone / only the incomplete / only the overdue), optionally add a 160-character message, optionally pick a time. The call answers 202 with how many people it will reach; a scheduled send is stored and delivered by the scheduler tick. A per-assignment log shows every batch that went out, manual and automatic, newest first.
- Entry points
POST /v1/admin/assignments/{assignment_id}/reminders,GET /v1/admin/assignments/{assignment_id}/notifications· duplicate mounts under/v1/assignments/…(dead)- Touches
scheduled_reminders,assignment_events; publishestraining.assignments.reminders.ten.*- Related
- manual-reminders-fe-integration.md (frontend contract)
- Evidence
- app/routes/assignment_routes.py:379,338 · app/services/manual_reminder_service.py:232 · docs/manual-reminders-fe-integration.md:14,191
Progress read-outs, analytics and completion reports suspect
A second, older admin surface: per-assignment progress summaries, per-person progress lists, a
user's assignment history, analytics, a manual "re-sync from micro-learning" button, and JSON completion
reports stored in the database for later download. It works, but the ingress has no rule for
/v1/admin/progress, so today the admin app cannot reach any of it — those paths fall through to
the users-auth catch-all. Only in-repo smoke scripts call them.
- Entry points
- 12 routes under
/v1/admin/progress/…(see §4) - Touches
assignment_progress,progress_milestones,progress_events,completion_reports,assignment_statistics; micro-learning progress API- Related
- Reports are JSON rows only — no CSV/PDF, no files, no email (assignment-details-api-gaps.md §5). Cross-tenant report download was fixed (bug-hunt #1);
POST /v1/admin/progress/test-eventstill trusts a body-supplied tenant (AS-A2) - Evidence
- app/routes/progress_routes.py:25-531 · global-configs/k8s/users-auth-ingress.yaml:76-82 (no /v1/admin/progress rule) · assignment-service/scripts/smoke_test_group_assignment.py:463
Event console: audit trail, dead letters, retries dead
An operator surface over the service's own event bookkeeping: list processed events, inspect
one, read statistics, list dead letters, resolve a dead letter, retry failures, archive old rows, poke the
worker, inject a test event. Nothing calls it: it is gated by the internal service key (not an admin JWT), no
other service in the repo calls it, and the ingress has no rule for /v1/admin/events. The
dead-letter listing could not answer even if something did call it — it is shadowed by
/events/{event_id}, registered first.
- Entry points
- 9 routes under
/v1/admin/events/…(see §4) - Touches
assignment_events,event_processing_logs,event_dead_letters,event_deduplication- Related
- known defects: route shadowing (bug-hunt #20), client-supplied tenant on internal reads (#2, #3)
- Evidence
- app/routes/event_routes.py:25-372 (all
Depends(validate_internal_token)) · no caller repo-wide · global-configs/k8s/users-auth-ingress.yaml (no /v1/admin/events rule)
Notification preview and per-employee preferences dead
"Show me who would be notified for this assignment", plus get/set one employee's notification
preferences and a worker-status probe. All four are internal-key routes with no caller in the repo, and the
ingress sends /v1/admin/notifications to notification-worker instead — so even an external
caller reaches a different service. The preferences pair is doubly inert: its HTTP client points at a
notifications-service host that does not exist and fails open to defaults.
- Entry points
POST /v1/admin/notifications/preview-assignment,GET /v1/admin/notifications/preferences/{employee_id},PUT /v1/admin/notifications/preferences/{employee_id},GET /v1/admin/notifications/workers/status- Touches
assignment_recipients; the (non-existent) preferences host- Related
- known defect: to-be-reviewed AS-A13 (preferences client points at a dead host, fails open)
- Evidence
- app/routes/notification_routes.py:22,92,127,160 · global-configs/k8s/users-auth-ingress.yaml:142-149 (prefix → notification-worker-service) · app/services/notification_service.py:122-126
Employee (mobile)
My assignments live
The learner's own list: the active assignments for the employee in the JWT, with due date, priority, required flag and the quiz overrides that apply to the assigned module. The mobile app does not call it directly — micro-learning-service-v2 calls it while assembling the module cards, which is why the mobile module card can say "assigned, due Friday".
- Entry points
GET /v1/employee/assignments(live) · duplicate mountGET /v1/admin/employee/assignments(dead)- Touches
assignments,assignment_recipients- Related
- question-retry-settings.md (the learner-facing quiz knobs travel on this response); known defect: fetches all rows then paginates in memory (to-be-reviewed AS-A15)
- Evidence
- app/routes/assignment_routes.py:543 · micro-learning-service-v2/app/clients/assignment_service.py:31 · ARCHITECTURE.md §3.2 H13
Internal (other services)
"Does this person / group still have training outstanding?" live
Two cheap yes/no lookups other services need before they act. users-auth asks for the active-assignment count per group so an admin cannot delete a group that still owes training. notification-worker asks whether a user has any active assignment before it bothers them with a push.
- Entry points
POST /v1/internal/groups/active-count,GET /v1/internal/users/{user_id}/active-assignments- Touches
assignments,assignment_targets,assignment_recipients- Related
ARCHITECTURE.md§3.2 H20, H26; known defect: the group count includes assignments everybody already finished (bug-hunt #24)- Evidence
- users-auth-service/app/routes/groups.py:243 · notification-worker/app/services/assignment_client.py:63
Feeding other services' admin screens live
Five read endpoints exist purely so another service can render a screen it owns: users-auth's per-user admin page (their assignments and their activity feed), and micro-learning's admin overview, module detail, per-learner module card and "is the rollout finished yet" indicator.
- Entry points
GET /v1/internal/users/{user_id}/assignments,GET /v1/internal/users/{user_id}/activity,GET /v1/internal/assignments/overview,GET /v1/internal/modules/{module_id}/assignments,GET /v1/internal/modules/{module_id}/employees/{employee_id}/assignment,GET /v1/internal/assignments/{assignment_id}/expansion-status- Touches
assignments,assignment_recipients,assignment_progress,assignment_events- Related
ARCHITECTURE.md§3.2 H13, H20- Evidence
- users-auth-service/app/clients/training_clients.py:120,134 · micro-learning-service-v2/app/clients/assignment_service.py:87,109,132,154
Cancel the assignments of a module that was deleted live
When an admin deletes or unpublishes a module in micro-learning, that service calls back here to cancel the assignments pointing at it, so nobody is chased to complete training that no longer exists. The cancellation is attributed to whoever deleted the module.
- Entry points
POST /v1/internal/assignments/{assignment_id}/cancel- Touches
assignments; publishesassignment.canceled- Related
ARCHITECTURE.md§3.2 H13- Evidence
- app/routes/internal.py:394 · micro-learning-service-v2/app/clients/assignment_service.py:203
GDPR: one person's assignment data live
When a subject asks for a copy of their data, users-auth fans out to the same endpoint on every service that holds personal data and merges the slices. This service's slice is the person's assignments, recipient rows, progress, milestones and reminder state.
- Entry points
GET /v1/internal/export/{user_id}- Touches
- every learner-scoped table in the
assignmentsDB - Related
ARCHITECTURE.md§3.2 H23 (GDPR export fan-out, runs on a CronJob)- Evidence
- app/routes/internal.py:442 · app/services/data_export_service.py · users-auth-service/app/services/gdpr_export.py:147,156
Patching one assignment's roster by hand dead
Two internal endpoints to read an assignment's employee list and to bolt one extra employee
onto it. Nothing in the platform calls either. The write half also silently dropped its row until recently —
it returned a recipient_id for an INSERT that was rolled back (fixed, but it still has no
caller).
- Entry points
GET /v1/internal/assignments/{assignment_id}/employees,POST /v1/internal/assignments/{assignment_id}/employees/{employee_id}- Touches
assignment_recipients- Related
- bug-hunt #9 ("no live inter-service caller of this POST was found")
- Evidence
- app/routes/internal.py:178,246 · no caller repo-wide for either path
Background
Turning "the whole company" into a list of people live
Group and company assignments are not expanded in the request — the create call publishes an
event and returns. The expansion worker picks it up, asks users-auth who is in those groups (or in the
tenant), writes one recipient row per person, pushes "New training assigned" to each of them, and flips the
assignment to expansion_status = "completed". Retargeting runs the same path and also retires
people who left the audience. Failures are published so admins are told the rollout did not happen.
- Entry points
- NATS
training.assignments.>, durableexpansion_worker, streamMICRO_LEARNING(handles thecreatedandretargetedsubjects only) - Touches
assignment_recipients,assignments; users-auth group members; publishesassignment.expanded,assignment.expansion_completed,assignment.expansion_failedand per-recipienttraining.notifications.ten.*.assignment.*- Related
- This used to be a separate Deployment consuming the same subject with different dedup rules; that duplicate was merged away (
ARCHITECTURE.md§6 M1, landed #501) and the in-pod worker is now the only expander (bug-hunt #14). Known defect: it logs the full service JWT (#6) - Evidence
- app/main.py:76-77 (lifespan starts it) · app/workers/expansion_worker.py:142,150,490-495
Mirroring how far each person has got live
As a learner finishes each lesson, micro-learning announces the new lesson count and this worker mirrors it onto the person's row here — percentage, lessons done, started-at. That is what makes the admin list and the assignee roster show "3 of 8 lessons" instead of a flat 0, and it is what the reminder engine reads to stop nagging someone who is clearly making progress.
- Entry points
- NATS
training.progress.assignment_progress, matched by the durableprogress_event_workerfiltertraining.progress.> - Touches
assignment_progress,progress_milestones,progress_events- Related
- The publisher's docstring states the contract: without this event assignment-service would learn only about full completions and partial progress would read as 0. The handler now commits, so bug-hunt AS-A8's rollback half no longer applies. The worker's own comment at
progress_event_worker.py:127("has no producer") and bug-hunt #8 / to-be-reviewed AS-A10 are stale on this point - Evidence
- app/workers/progress_event_worker.py:109,119,188 (routes on
assignment_progress), :315 (db.commit()) · publisher micro-learning-service-v2/app/events/events.py:125 via app/routes/progress_mobile_routes.py:1344
Knowing that someone finished live
When a learner finishes a module outright, micro-learning announces that too and this worker finds every assignment of that module for that person, drives it to 100%, sends the "Training completed" push once, and tells admins as soon as the last recipient is done. An hourly sweep re-reads progress for recent assignments so a dropped event cannot leave the numbers wrong forever.
- Entry points
- NATS
training.module.completed, durableprogress_module_completion_worker· hourly_periodic_sync_loop - Touches
assignment_progress,progress_milestones,progress_events,assignments.fully_completed_notified_at; publishestraining.notifications.ten.*.assignment.*andtraining.notifications.ten.*.admin.*- Related
- Both progress inputs are live and they are different signals: partial progress arrives on
training.progress.assignment_progress(previous block), full completion ontraining.module.completed. The completion push had no production trigger at all until this consumer was added (to-be-reviewed A1, fixed). Known defects: a failed apply is acked as success (AS-A21 / bug-hunt #18), non-atomic completion-push idempotency (#12) - Evidence
- app/main.py:61-64 · app/workers/progress_event_worker.py:136,144,53 · micro-learning-service-v2/app/events/events.py:80 (publisher)
Chasing people until they finish live
Every 5 minutes the scheduler asks, per person per assignment, "is it time to remind them again?" The interval comes from a priority × time-to-due matrix: critical work reminds twice a day in the last three days and daily once overdue, low-priority work only on the due day. People who are part-way through get reminded less often, finished people not at all, and the recipient's timezone and quiet hours are respected unless the admin overrode that.
- Entry points
ReminderSchedulerWorker._loop→run_tick(), tickREMINDER_SCHEDULER_TICK_SECONDS(default 300s)- Touches
assignment_reminders,assignment_events; users-auth (timezone/preferences); publishestraining.assignments.reminders.ten.*- Related
- SDD-OPER-007 · consumed by notification-worker's
notification_worker_remindersdurable. Known defect: the "I reminded them" row is committed before the publish, so a NATS hiccup skips a whole cadence step (to-be-reviewed AS-A7) - Evidence
- app/main.py:56-58 · app/workers/reminder_scheduler_worker.py:34 · app/services/reminder_scheduler.py:37-63,496,524
Escalating a critical assignment nobody touched live
If an assignment is marked critical and a person has been reminded three times
with zero progress, the scheduler fires one escalation event for that person — a final, louder nudge that
ignores quiet hours. It fires once per person per assignment.
- Entry points
ReminderScheduler._should_escalate→_publish_escalation, inside the same tick; publishestraining.assignments.escalations.ten.*- Touches
assignment_reminders.escalated- Related
- This was unreachable twice over and both halves are now fixed:
criticalwas rejected by the DB constraint untilalembic/versions/2026_07_07_widen_priority_check.py, and the subject had no consumer until notification-worker added one (to-be-reviewed/README.md row #3/N2). It delivers to the employee, not a manager — there is no manager resolution - Evidence
- app/services/reminder_scheduler.py:460-470,529 · alembic/versions/2026_07_07_widen_priority_check.py:29 · notification-worker/app/services/notification_service.py:1454-1467,2381
Announcing an assignment on the day it starts live
An assignment can be created with a future start date. Nobody is told about it until that day arrives; the same scheduler loop sweeps for assignments whose start day has come, pushes the "New training assigned" notification then, and stamps the assignment so it is announced exactly once.
- Entry points
ReminderScheduler.run_start_activation_tick(), same loop as the reminders- Touches
assignments.start_notified_at; publishestraining.notifications.ten.*.assignment.*- Related
alembic/versions/2026_07_07_add_start_notified_at.py- Evidence
- app/workers/reminder_scheduler_worker.py:62-66 · app/services/reminder_scheduler.py:414
Delivering admin-scheduled reminders live
A manual reminder with a send_at is stored, not sent. The same loop sweeps for
scheduled reminders that have come due, resolves the audience the admin chose at that moment, sends them, and
records the batch in the assignment's notification log.
- Entry points
ReminderScheduler.run_manual_reminder_tick(), same loop as the reminders- Touches
scheduled_reminders,assignment_events; publishestraining.assignments.reminders.ten.*- Related
- manual-reminders-fe-integration.md
- Evidence
- app/workers/reminder_scheduler_worker.py:68-72 · app/services/reminder_scheduler.py:347 · app/services/manual_reminder_service.py:232
The daily "who is late" digest for admins live
Once a day at 07:00 UTC each tenant's admins get one inbox item: how many learners and how many assignments are overdue, plus the three worst offenders. Tenants with nothing overdue get nothing — silence is the default. The event id is derived from tenant + date, so several pods firing cannot produce duplicate inbox rows.
- Entry points
AdminDigestWorker._loop, checked everyADMIN_DIGEST_CHECK_SECONDS(default 60s), fires atADMIN_OVERDUE_DIGEST_HOUR_UTC(default 7)- Touches
- reads
assignments,assignment_recipients,assignment_progress; publishestraining.notifications.ten.*.admin.* - Related
- PLAN-admin-notif-centre-tier1; consumed by notification-worker's
notification_worker_admindurable - Evidence
- app/main.py:67-70 · app/workers/admin_digest_worker.py:90 · app/services/admin_notifications.py:63
Event bookkeeping: audit trail, dedup, retry, cleanup live
Everything that happens to an assignment is written down: one audit row per event, a deduplication key so a redelivered event is not acted on twice, a processing log, and a dead-letter row when processing fails. Those audit rows are what the admin activity feed reads. Two side loops retry failed events and archive old ones.
- Entry points
- NATS
training.assignments.>, durableenhanced_event_worker(skipscreated— that belongs to the expansion worker — and skips reminder/escalation subjects) ·_retry_failed_events_loop·_cleanup_old_events_loop - Touches
assignment_events,event_deduplication,event_processing_logs,event_dead_letters- Related
- None of it persisted until the missing commit was added (bug-hunt #7/#19, fixed). Known defects: acks on processing error (AS-A11/A13), and this durable was observed stalled with 120 unprocessed messages (
ARCHITECTURE.md§4.1(7)) - Evidence
- app/main.py:51-53 · app/workers/enhanced_event_worker.py:41,42,90,196
Superseded duplicate notification consumer dead
An older worker that consumed the same assignment events and sent the same "New training assigned" pushes. It is deliberately not started — running it next to the two live consumers would double-publish every created notification. The file survives only because a test imports it.
- Entry points
- would be NATS
training.assignments.>, durablenotification_event_worker, streamMICRO_LEARNING - Touches
- nothing — never instantiated at runtime
- Related
- to-be-reviewed A4/A12 (import removed on purpose; full deletion deferred until the test is decoupled)
- Evidence
- app/main.py:18-21 (the comment explaining the removal) · app/workers/notification_event_worker.py:61 · only other reference: tests/test_notifications.py:14
4. API reference
61 rows = every route in the scan JSON. dup in the Feature column marks the second
mount of the shared router (see §1). Auth: admin JWT = tenant admin/manager bearer token plus the
named scope; internal = X-Oper-Key or an internal-service JWT;
employee JWT = a learner's token.
| Method | Path | Auth | Feature | Callers | Verdict |
|---|---|---|---|---|---|
| GET | /health/live | none | liveness | kubelet (k8s/deployment.yaml:98-101) | live |
| GET | /health/ready | none | readiness (DB check) | kubelet (k8s/deployment.yaml:87-90) | live |
| POST | /v1/admin/assignments | admin JWT · assignments.write | Assign a module | admin web app (docs/BUSINESS_LOGIC.md:70) | live |
| GET | /v1/admin/assignments | admin JWT · analytics.read | Assignment list | admin web app (docs/BUSINESS_LOGIC.md:165) | live |
| GET | /v1/admin/assignments/stats | admin JWT · analytics.read | Dashboard counters | none found — ingressed, but no doc or in-repo caller names it | suspect |
| GET | /v1/admin/assignments/{assignment_id} | admin JWT · analytics.read | Assignment detail | admin web app (docs/manual-reminders-fe-integration.md:231) | live |
| PATCH | /v1/admin/assignments/{assignment_id} | admin JWT · assignments.write | Edit assignment | admin web app (docs/BUSINESS_LOGIC.md:122) | live |
| DELETE | /v1/admin/assignments/{assignment_id} | admin JWT · assignments.write | Cancel assignment | admin web app (docs/assignment-details-api-gaps.md:204) | live |
| GET | /v1/admin/assignments/{assignment_id}/assignees | admin JWT · analytics.read | Assignment detail | admin web app (docs/manual-reminders-fe-integration.md:232) | live |
| GET | /v1/admin/assignments/{assignment_id}/activity | admin JWT · analytics.read | Assignment detail | admin web app (docs/manual-reminders-fe-integration.md:132,234) | live |
| GET | /v1/admin/assignments/{assignment_id}/notifications | admin JWT · analytics.read | Reminder log | admin web app (docs/manual-reminders-fe-integration.md:191) | live |
| POST | /v1/admin/assignments/{assignment_id}/reminders | admin JWT · assignments.write | Manual reminder | admin web app (docs/manual-reminders-fe-integration.md:14) | live |
| GET | /v1/admin/employee/assignments | employee JWT | dup — employee route under the admin mount | none; not matched by the /v1/admin/assignments ingress prefix | dead |
| GET | /v1/admin/events/statistics | internal | Event console | none | dead |
| GET | /v1/admin/events | internal | Event console | none | dead |
| GET | /v1/admin/events/{event_id} | internal | Event console | none | dead |
| GET | /v1/admin/events/dead-letters | internal | Event console | none; also shadowed by /events/{event_id} | dead |
| POST | /v1/admin/events/dead-letters/{dead_letter_id}/resolve | internal | Event console | none | dead |
| POST | /v1/admin/events/retry-failed | internal | Event console | none | dead |
| POST | /v1/admin/events/cleanup | internal | Event console | none | dead |
| GET | /v1/admin/events/worker/status | internal | Event console | none | dead |
| POST | /v1/admin/events/test-event | internal | Event console (debug) | none | dead |
| POST | /v1/admin/notifications/preview-assignment | internal | Notification preview | none; ingress prefix goes to notification-worker | dead |
| GET | /v1/admin/notifications/preferences/{employee_id} | internal | Employee preferences | none; ingress prefix goes to notification-worker | dead |
| PUT | /v1/admin/notifications/preferences/{employee_id} | internal | Employee preferences | none; ingress prefix goes to notification-worker | dead |
| GET | /v1/admin/notifications/workers/status | internal | Worker probe | none; ingress prefix goes to notification-worker | dead |
| GET | /v1/admin/progress/assignments/{assignment_id}/summary | admin JWT · analytics.read | Progress read-outs | no ingress rule; only scripts/smoke_test_*.py | suspect |
| GET | /v1/admin/progress/assignments/{assignment_id}/users | admin JWT · analytics.read | Progress read-outs | no ingress rule; only smoke scripts | suspect |
| GET | /v1/admin/progress/assignments/{assignment_id}/analytics | admin JWT · analytics.read | Progress read-outs | no ingress rule; no caller | suspect |
| POST | /v1/admin/progress/assignments/{assignment_id}/sync | admin JWT · assignments.write | Manual progress re-sync | no ingress rule; only smoke scripts | suspect |
| POST | /v1/admin/progress/assignments/{assignment_id}/reports | admin JWT · analytics.read | Completion report | no ingress rule; no caller | suspect |
| GET | /v1/admin/progress/reports | admin JWT · analytics.read | Completion report | no ingress rule; no caller | suspect |
| GET | /v1/admin/progress/reports/{report_id}/download | admin JWT · analytics.read | Completion report | no ingress rule; no caller | suspect |
| GET | /v1/admin/progress/users/{user_id}/assignments | admin JWT · analytics.read | Progress read-outs | no ingress rule; only smoke scripts | suspect |
| GET | /v1/admin/progress/users/{user_id}/history | admin JWT · analytics.read | Progress read-outs | no ingress rule; no caller | suspect |
| POST | /v1/admin/progress/sync/all | admin JWT · assignments.write | Manual progress re-sync | no ingress rule; no caller | suspect |
| GET | /v1/admin/progress/worker/status | admin JWT · analytics.read | Worker probe | no ingress rule; no caller | suspect |
| POST | /v1/admin/progress/test-event | admin JWT · assignments.write | Debug: forge a completion | no ingress rule; only smoke scripts. Known cross-tenant write (AS-A2) | suspect |
| POST | /v1/assignments | admin JWT · assignments.write | dup — /v1 mount | none; /v1/assignments is not ingressed to this service | dead |
| GET | /v1/assignments | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| GET | /v1/assignments/stats | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| GET | /v1/assignments/{assignment_id} | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| PATCH | /v1/assignments/{assignment_id} | admin JWT · assignments.write | dup — /v1 mount | none | dead |
| DELETE | /v1/assignments/{assignment_id} | admin JWT · assignments.write | dup — /v1 mount | none | dead |
| GET | /v1/assignments/{assignment_id}/assignees | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| GET | /v1/assignments/{assignment_id}/activity | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| GET | /v1/assignments/{assignment_id}/notifications | admin JWT · analytics.read | dup — /v1 mount | none | dead |
| POST | /v1/assignments/{assignment_id}/reminders | admin JWT · assignments.write | dup — /v1 mount | none | dead |
| GET | /v1/employee/assignments | employee JWT | My assignments | micro-learning-service-v2 (app/clients/assignment_service.py:31) | live |
| POST | /v1/internal/groups/active-count | internal | Group delete guard | users-auth-service (app/routes/groups.py:243) | live |
| GET | /v1/internal/users/{user_id}/active-assignments | internal | Push gating | notification-worker (app/services/assignment_client.py:63) | live |
| GET | /v1/internal/users/{user_id}/assignments | internal | users-auth user page | users-auth-service (app/clients/training_clients.py:120) | live |
| GET | /v1/internal/users/{user_id}/activity | internal | users-auth user page | users-auth-service (app/clients/training_clients.py:134) | live |
| GET | /v1/internal/assignments/overview | internal | micro-learning admin overview | micro-learning-service-v2 (app/clients/assignment_service.py:132) | live |
| GET | /v1/internal/assignments/{assignment_id}/expansion-status | internal | Rollout progress indicator | micro-learning-service-v2 (app/clients/assignment_service.py:87) | live |
| GET | /v1/internal/modules/{module_id}/assignments | internal | Module detail screen | micro-learning-service-v2 (app/clients/assignment_service.py:154) | live |
| GET | /v1/internal/modules/{module_id}/employees/{employee_id}/assignment | internal | Learner module card | micro-learning-service-v2 (app/clients/assignment_service.py:109) | live |
| POST | /v1/internal/assignments/{assignment_id}/cancel | internal | Module deleted → cancel | micro-learning-service-v2 (app/clients/assignment_service.py:203) | live |
| GET | /v1/internal/export/{user_id} | internal | GDPR export | users-auth-service (app/services/gdpr_export.py:156) | live |
| GET | /v1/internal/assignments/{assignment_id}/employees | internal | Manual roster read | none | dead |
| POST | /v1/internal/assignments/{assignment_id}/employees/{employee_id} | internal | Manual roster patch | none (bug-hunt #9) | dead |
5. Async contracts
Everything runs on the one shared MICRO_LEARNING stream. All publishing goes
through the one NATSClient.publish() helper (app/core/nats_client.py:116) and all
subscribing through its subscribe() / pull_subscribe() delegates, so the subjects
below are the ones the callers pass in.
Consumes
| Subject | Stream | Durable | Published by | Feature | Verdict |
|---|---|---|---|---|---|
training.assignments.> | MICRO_LEARNING | expansion_worker (pull, max_deliver 4, ack_wait 300s) | this service (assignment.created / assignment.retargeted) | Audience expansion | live |
training.assignments.> | MICRO_LEARNING | enhanced_event_worker (push, manual ack) | this service (updated, canceled, expansion_*) | Event bookkeeping | live |
training.assignments.> | MICRO_LEARNING | notification_event_worker | — never subscribed; the worker is not started (app/main.py:18-21) | Superseded duplicate | dead |
training.progress.>matches the published training.progress.assignment_progress | MICRO_LEARNING | progress_event_worker (pull, max_deliver 4, ack_wait 300s) | micro-learning-service-v2 (app/events/events.py:125, from the mobile progress path app/routes/progress_mobile_routes.py:1344) | Partial-progress mirror | live |
training.module.completed | MICRO_LEARNING | progress_module_completion_worker (pull) | micro-learning-service-v2 (app/events/events.py:80) | Completion detection | live |
Publishes
| Subject | Consumed by | Feature | Verdict |
|---|---|---|---|
training.assignments.ten.*.assignment.created | this service's expansion_worker (app/workers/expansion_worker.py:207) | Audience expansion | live |
training.assignments.ten.*.assignment.retargeted | this service's expansion_worker (same handler) | Audience change | live |
training.assignments.ten.*.assignment.updated | this service's enhanced_event_worker (audit row only) | Event bookkeeping | live |
training.assignments.ten.*.assignment.canceled | this service's enhanced_event_worker | Event bookkeeping | live |
training.assignments.ten.*.assignment.expansion_completed | notification-worker durable notification_worker_admin_exp_ok (app/core/config.py:123-128) → admin inbox | Rollout finished notice | live |
training.assignments.ten.*.assignment.expansion_failed | notification-worker durable notification_worker_admin_exp_fail (app/core/config.py:130-133) | Rollout failed alert | live |
training.assignments.ten.*.assignment.expanded | only this service's enhanced_event_worker, which records it as assignment.expansion_completed; no external consumer despite the code comment claiming notification-worker reads it | Event bookkeeping | live |
training.assignments.expanded | nobody acts on it — published only from _process_assignment_created, a method the file labels "test helper", and swallows its own publish errors | none (test-only path) | dead |
training.assignments.ten.*.progress.init_required | nobody — and publish_progress_init_required() has no call site in the service | none | dead |
training.assignments.reminders.ten.* (scheduler) | notification-worker durable notification_worker_reminders (app/services/notification_service.py:1274,2380) | Automatic reminders | live |
training.assignments.reminders.ten.* (manual sends) | same durable | Manual reminders | live |
training.assignments.escalations.ten.* | notification-worker durable notification_worker_escalations (app/services/notification_service.py:1454-1467,2381) | Escalation | live |
training.notifications.ten.*.assignment.* | notification-worker durable on training.notifications.ten.*.assignment.> (app/services/notification_service.py:1836) → FCM push + history | Assigned / completed pushes | live |
training.notifications.ten.*.admin.* | notification-worker durable notification_worker_admin on training.notifications.ten.*.admin.> (app/core/config.py:114-118) | Admin digest, fully-completed notice | live |
Known defect: all of the above use core nc.publish, not
js.publish, so the publisher never sees a JetStream ack and sets no Nats-Msg-Id — the
stream's 120s duplicate window cannot help (to-be-reviewed
AS-A9).
Background jobs
| Job | Schedule | What it does | Verdict |
|---|---|---|---|
GroupExpansionWorker._process_messagesexpansion_worker.py:150 | continuous pull loop, batch 10, 5s fetch timeout | Started by the lifespan (app/main.py:76-77). Turns group/company assignments into recipient rows and pushes each person. | live |
_ack_in_progressexpansion_worker.py:171 | every 30s per in-flight message | Started per message by the loop above. Extends the JetStream ack deadline so a company-wide rollout is not redelivered mid-write. | live |
EnhancedEventWorker._retry_failed_events_loopenhanced_event_worker.py:41 | continuous, interval from the worker | Started by EnhancedEventWorker.start(), which the lifespan calls (app/main.py:51-53). Re-processes events whose first attempt failed. | live |
EnhancedEventWorker._cleanup_old_events_loopenhanced_event_worker.py:42 | continuous, interval from the worker | Same start path. Archives old audit/dedup rows. | live |
ProgressEventWorker._process_module_completion_messagesprogress_event_worker.py:144 | continuous pull loop | Started by the lifespan (app/main.py:61-64). The real completion path: drives assignments to 100% and fires the completion push. | live |
ProgressEventWorker._periodic_sync_loopprogress_event_worker.py:53 | every 3600s (hourly), for assignments from the last 7 days | Same start path — and it starts even if the NATS subscription failed ("degraded, sync-only" mode). Re-reads progress from micro-learning so a lost event self-heals. | live |
ProgressEventWorker._process_progress_messagesprogress_event_worker.py:119 | continuous pull loop | Started by the lifespan (app/main.py:61-64). Drains training.progress.> and routes on the last subject token: assignment_progress is the live partial-progress mirror (and commits, :315); the lesson_completed and assignment_completed branches have no publisher (§8). | live |
ReminderSchedulerWorker._loopreminder_scheduler_worker.py:34 | every REMINDER_SCHEDULER_TICK_SECONDS (default 300s); skipped entirely if REMINDER_SCHEDULER_ENABLED=false | Started by the lifespan (app/main.py:56-58). Three sweeps per tick: reminder cadence + escalation, start-day announcements, due admin-scheduled reminders. | live |
AdminDigestWorker._loopadmin_digest_worker.py:90 | wakes every ADMIN_DIGEST_CHECK_SECONDS (default 60s), fires once per UTC day at hour ADMIN_OVERDUE_DIGEST_HOUR_UTC (default 07); skipped if ADMIN_DIGEST_ENABLED=false | Started by the lifespan (app/main.py:67-70). One overdue digest per tenant with anything overdue. | live |
NotificationEventWorker (whole worker)notification_event_worker.py:61 | never runs | Not started. The lifespan import was removed on purpose; starting it beside EnhancedEventWorker would double-publish every created notification. | dead |
There are no k8s CronJobs for this service — every schedule above lives inside the single API
pod (replicas: 1), so a rolling restart pauses reminders and the digest for the duration of the
roll.
6. Data it owns
One logical Postgres DB, assignments, on the shared instance. 15 alembic revisions; head adds
performance indexes (2026_08_30_add_perf_indexes.py). ARCHITECTURE.md §3.4 still lists
a second writer, assignment-expansion-worker — that is stale: the merge landed (§6 M1, PR #501)
and the repo directory now holds no source, so assignment-service is the only writer.
| Table | What it holds | Written by |
|---|---|---|
assignments | One row per "this module is assigned to this audience": module id + version snapshot, type, start/due date, priority, required flag, quiz overrides, notification config, status, expansion status, cancellation and the fully_completed_notified_at / start_notified_at claim stamps | assignment routes, expansion worker, progress worker, reminder scheduler |
assignment_targets | The audience as the admin described it (employee ids, group ids, or "company") | assignment routes |
assignment_recipients | The audience resolved to people — one row per employee. This is what reminders, aggregates and the mobile list read | assignment routes (individual targets), expansion worker (group/company), internal add-employee route |
assignment_progress | Per person per assignment: status, percentage, time spent, started/completed timestamps | progress worker, progress routes |
progress_milestones | 25/50/75/100% markers, with the notification_sent flag that keeps the completion push single | progress worker |
progress_events | Raw progress event history | progress worker |
completion_reports | Generated JSON reports with an expiry and a download counter | progress routes |
assignment_statistics | Cached per-assignment aggregates | progress worker / progress routes |
assignment_reminders | Per person per assignment reminder state: remind count, last reminded, resolved timezone, escalated flag | reminder scheduler |
scheduled_reminders | Admin-scheduled manual reminders: audience, message, send time, status, recipient count | manual reminder route, reminder scheduler |
assignment_events | The audit trail behind the admin activity feed and the notification log | enhanced event worker, reminder scheduler, manual reminder service |
event_deduplication, event_processing_logs, event_dead_letters | Event pipeline bookkeeping: redelivery suppression, per-attempt logs, poison events | enhanced event worker (the admin API over these tables is dead — §8) |
Tables this service writes in someone else's database: none. Tables in
assignments written by anyone else: none.
7. Dependencies
flowchart LR ML["micro-learning-service-v2"] --> AS["assignment-service"] UA["users-auth-service"] --> AS NW["notification-worker"] --> AS AS --> UA AS --> ML AS --> NW AS -.-> JS["MICRO_LEARNING stream"] JS -.-> NW JS -.-> AS
Inbound HTTP: micro-learning (H13, mobile read path, 8 call sites), users-auth (H20, plus the
GDPR CronJob H23), notification-worker (H26). Outbound HTTP: users-auth (H15 — groups, members, profiles,
timezone), micro-learning (H16/H17 — progress and module validation), notification-worker (H18 — device
check). Async both ways over the one shared stream. Numbering per 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 |
|---|---|---|---|
The 10 /v1/assignments… paths | HTTP (duplicate mount) | dead | (1) no caller: repo-wide search for /v1/assignments finds only this service's own outbound client for micro-learning's …/progress-summary (app/services/progress_service.py:102) and the scan JSON. (2) unreachable: the only ingress routes /v1/admin/assignments to this service and everything else to the users-auth catch-all (global-configs/k8s/users-auth-ingress.yaml:76-82,229-236), and no in-cluster service calls these paths. Same handlers are live under /v1/admin. Deleting the /v1 mount (app/main.py:164) would also remove /v1/employee/assignments, which IS live — split the router instead. |
GET /v1/admin/employee/assignments | HTTP (duplicate mount) | dead | (1) no caller: micro-learning calls the /v1/employee/assignments form (micro-learning-service-v2/app/clients/assignment_service.py:31); nothing references the /v1/admin/employee form. (2) unreachable: the ingress prefix is /v1/admin/assignments, which does not match /v1/admin/employee/…, so external calls fall through to users-auth and 404. Artifact of app/main.py:158. |
The 9 /v1/admin/events… routes | HTTP (internal-only) | dead | (1) no caller: every handler takes Depends(validate_internal_token) (app/routes/event_routes.py:28,66,126,192,249,289,313,336,374) and no service in the repo calls any of these paths. (2) unreachable as an admin surface: no ingress rule for /v1/admin/events. GET /v1/admin/events/dead-letters is additionally unreachable in-process — /events/{event_id} is registered first and turns it into a 400 (bug-hunt #20). |
The 4 /v1/admin/notifications… routes | HTTP (internal-only) | dead | (1) no caller: internal-token routes (app/routes/notification_routes.py:25,93,129,163) with no in-repo caller. (2) unreachable: the ingress sends the whole /v1/admin/notifications prefix to notification-worker-service (global-configs/k8s/users-auth-ingress.yaml:142-149), so an external caller never arrives here. The preferences pair would also fail open against a non-existent notifications-service host (to-be-reviewed AS-A13). |
GET /v1/internal/assignments/{assignment_id}/employees | HTTP (internal-only) | dead | (1) no caller: searched every service's client layer and the path string repo-wide; micro-learning's client calls expansion-status / module assignments / cancel / overview / per-employee module assignment only (micro-learning-service-v2/app/clients/assignment_service.py:87-203). (2) internal-only surface (app/routes/internal.py:182), not exposed by any ingress rule. |
POST /v1/internal/assignments/{assignment_id}/employees/{employee_id} | HTTP (internal-only) | dead | (1) no caller: same search; bug-hunt #9 independently states "no live inter-service caller of this POST was found". (2) internal-only surface (app/routes/internal.py:249), no ingress rule. Its missing commit was fixed, so it would now work if anyone called it. |
The 12 /v1/admin/progress… routes | HTTP (admin) | suspect | Searched: the whole repo for each path string, every service's client layer, and every docs/*.md / FE integration note. Only hits are this service's own scripts/smoke_test_*.py (not production callers) and tests. The admin web app is not in this repo, so this stays suspect — but note the ingress has no /v1/admin/progress rule (global-configs/k8s/users-auth-ingress.yaml), so it cannot be reached from admin.useoper.com today. Resolve by checking the admin app's network calls, or add the ingress rule if the screens exist. |
GET /v1/admin/assignments/stats | HTTP (admin) | suspect | Searched the repo for the path and for get_assignment_stats: only tests (tests/test_assignment_list_stats.py:457, tests/test_admin_scope_matrix.py:82). It IS inside the ingressed /v1/admin/assignments prefix and is the obvious source for the dashboard tiles, but no doc names it. Resolve by checking the admin app. |
NotificationEventWorker + NotificationEventWorkerManagerapp/workers/notification_event_worker.py (243 lines) | NATS consumer + module | dead | (1) Never started: app/main.py:18-21 carries the explicit comment that the import was removed, and the lifespan (app/main.py:45-89) constructs only EnhancedEventWorker, ReminderSchedulerWorker, ProgressEventWorker, AdminDigestWorker and GroupExpansionWorker. Repo-wide, the only other reference is tests/test_notifications.py:14. (2) Must not be started: it subscribes the same training.assignments.> under its own durable notification_event_worker (:61), so it would receive its own copy of every created event and re-send the "New training assigned" push that the expansion worker already sent — double-publishing. Cross-referenced: to-be-reviewed A4/A12; ARCHITECTURE.md §3.3 ("deliberately-disabled notification_event_worker"). |
Handlers _handle_lesson_completion_event and _handle_completion_event (the lesson_completed / assignment_completed branches of the training.progress.> router) | NATS handlers | dead | (1) Reachable only when the subject's last token is lesson_completed or assignment_completed (app/workers/progress_event_worker.py:188-193). (2) No publisher: the only training.progress.* subject anyone emits is training.progress.assignment_progress (micro-learning-service-v2/app/events/events.py:125) — grepped every training.progress. occurrence repo-wide; the rest are stream-subject config and docs. micro-learning's lesson event is learning.lesson.completed, a different subject tree, and completion arrives as training.module.completed on the separate durable. NOTE: the consumer itself and _process_progress_messages are live — they carry the partial-progress mirror (§3, §5). bug-hunt #8 / to-be-reviewed AS-A10 and the worker's own comment at :127 call the whole subscription producerless; that is stale, only these two branches are. |
Publisher training.assignments.expanded | NATS publish | dead | (1) Its only call site is inside _process_assignment_created (app/workers/expansion_worker.py:359), a method whose own docstring says "Test helper" and which swallows publish errors "to keep tests focused"; the live path is _handle_assignment_created → _expand_assignment (:217,367), which publishes the ten.* subjects instead. (2) No consumer filter targets the flat 3-token subject: notification-worker's assignment bridges filter training.assignments.ten.*.assignment.expansion_completed / …expansion_failed (notification-worker/app/core/config.py:123-133). |
Publisher method publish_progress_init_required → training.assignments.ten.*.progress.init_required | NATS publish | dead | (1) No call site: searched app/ for publish_progress_init_required — only the definition (app/core/nats_client.py:318). (2) No consumer: nothing outside this service subscribes anything under training.assignments.>, and the in-pod consumers key on the assignment.* token. Documented as a real event in docs/streams.md:222, which is stale. |
AssignmentService.validate_assignment_targets | unreachable function | dead | Only callers are unit tests (tests/test_assignment_service.py); the live expansion path uses expansion_worker._expand_targets. Its company branch references an undefined jwt_token, so it would raise NameError immediately if wired up — bug-hunt #17 / to-be-reviewed AS-A4 (app/services/assignment_service.py:885). |
WorkerManager in app/workers/expansion_worker.py:696 | unreachable class | dead | The lifespan constructs GroupExpansionWorker() directly (app/main.py:76); no code path instantiates this manager wrapper. Searched the repo for WorkerManager outside the file. |
app/services/notifications_service.py (the NotificationsServiceClient preferences client) | unreachable dependency | unknown | Its only users are the dead /v1/admin/notifications/preferences/* routes and an unused injection in AssignmentProgressService.__init__; it targets http://notifications-service:8000/v1/preferences/…, a host that does not exist, and every call fails open to defaults (to-be-reviewed AS-A13/AS-A20). Marked unknown rather than dead because whether created/cancelled pushes are supposed to be preference-gated is a product question: if yes, this is a broken live feature to repoint at users-auth; if no, the client and the routes go. Resolve with the notification-preferences owner. |
9. Sources
- ARCHITECTURE.md — §2 (service inventory), §3.2 HTTP edges H13/H15-18/H20/H24/H26, §3.3 streams and consumer conventions, §3.4 shared databases, §4.1 live bugs, §5-§6 (M1 expansion-worker merge).
- assignment-service/docs/BUSINESS_LOGIC.md — creation / update / cancellation / listing flows, expansion, progress, notifications, worker inventory, endpoint summary.
- assignment-service/docs/ARCHITECTURE.md — module layout and the (partly stale) worker/endpoint tables.
- assignment-service/docs/streams.md — published/consumed event payload contracts (its
training.assignments.expandedandprogress.init_requiredentries are stale, see §8). - assignment-service/docs/manual-reminders-fe-integration.md — the frontend contract for manual reminders, the notifications log, activity feed and assignee deliverability block; the strongest evidence that the admin assignment routes have a live caller.
- assignment-service/docs/assignment-details-api-gaps.md — what the detail screen shipped vs what was deliberately not built (reports have no CSV/PDF/email).
- assignment-service/docs/question-retry-settings.md — the admin → assignment →
/v1/employee/assignments→ mobile path for quiz overrides. - assignment-service/README.md — original K1-K5 phase framing, auth modes, health endpoints.
- bug-hunt-reports/assignment-service.md — 24 findings; #14 (expansion never ran) and #7/#19 (event worker never committed) are fixed and their fixes are reflected here; #9, #20, #24 cited above. #8 is stale: it was rejected on the grounds that nothing publishes
training.progress.*, but micro-learning does publishtraining.progress.assignment_progress(app/events/events.py:125, fromapp/routes/progress_mobile_routes.py:1344), and_handle_progress_eventnow commits — so that path is live, not dead (§3, §5, §8). - to-be-reviewed/assignment-service.md + to-be-reviewed/README.md — the notification-engine review. Of its three broken notification paths, two are now fixed in code (A1 completion push via the
training.module.completedconsumer; escalation consumer added, plus thecriticalpriority constraint widened) and the third, the disablednotification_event_worker, remains deliberately off. Two of its claims are superseded: AS-A12 (the in-pod expansion worker is dead) — the standalone worker was deleted and this one is now started; and AS-A10/AS-A8 (thetraining.progress.>consumer has no producer) — only itslesson_completed/assignment_completedbranches do. global-configs/k8s/users-auth-ingress.yaml— the single app ingress; decides which of these routes the outside world can reach at all.assignment-service/k8s/deployment.yaml,k8s/configmap.yaml,alembic/versions/*(15 revisions),app/main.py,app/routes/*,app/workers/*— read directly for every claim above.