- Shipped
- August 19, 2026 at 6:16 AM UTC
- Author
- kamo
- Commit
- db71d1e
The tab was two AG Grids under a tab bar: accurate and mute. It now opens on the org's whole training obligation and lets an admin act on the gaps. - A masthead whose cohort rail IS the assignment population — passed, overdue, in progress, not started, excused — sized in proportion and drawn on load. Every segment deep-links into Assignments pre-filtered, so the chart is the way in to the work rather than a picture of it. - The version ledger: one solid tick per published version with the live one lit, continuing into a dashed stub when the draft has drifted past it. A course publishes as versions and learners are pinned to the one they were sent — that was previously a line of text read one row at a time. Carried by both the gallery and the table, so the two views share one vocabulary. - Courses become a card gallery with search, status chips, sort and cover art; the grid stays behind a toggle for dense scanning. Both read from shapeCatalog, so they cannot disagree about what matches. - Assignments gain search, multi-select with bulk remind/extend/excuse/ cancel, and CSV export. Each bulk button states how many of the selection it can actually touch — a Remind 40 that mails 31 is a lie the admin then acts on. - A third sub-tab, Insights: pass-rate and average-score rings, the chasing queue, member standings, recent activity. All of it comes from the training block of /api/docs/hr/summary, which the HR Overview already calls; DocsService is untouched. - coverImgId has been on the DTO and in the save payload since the module shipped with nothing that could set it and nothing that read it. The editor gets the picker that makes the field real. Four defects fixed along the way: - Publish was disabled on every catalog row and always had been. publishBlockedReason walks course.items, but list() maps rows through toDto(course, false), which never sets them — so the item walk saw an empty list and returned 'empty' for every course, including ones that publish perfectly from the editor. catalogPublishBlockedReason gates on the counts the list does send; the per-question refusals stay in the editor, where the questions exist. - The delete confirmation's message was the courses empty-state body, pasted, so it read 'Build a course from slides and multiple-choice tests, then assign it to your team.' - Assignment status chips rendered status.replaceAll('_',' ') — the raw database enum, in English, in all 22 locales. - Two window.prompt() calls collected a compliance deadline and a waiver reason in an unstyled OS box. Both are real dialogs now; the waiver reason is what an auditor reads. Rules live in app/lib/trainingConsole.ts under 36 tests: vitest here runs in the node environment and collects only .test.ts, so a rule written inside a .tsx is a rule with no test. Strings landed first in kamo-translation-dictionary b5ed84af.