The HR course editor, assignments grid and member course player

Featurekamo-internal
Shipped
August 11, 2026 at 5:32 PM UTC
Author
kamo
Commit
6fa89b8

/hr/training replaces its placeholder with the two sub-tabs the brief asked for, and the member's Assigned Training grid gains the action column it was holding back for want of a player. COURSES. A catalog grid with publish, assign, duplicate, archive and delete inline. Delete is disabled once anybody holds an assignment and the tooltip points at Archive instead — deleting a course somebody was assigned orphans their evidence, since the assignment pins a version whose parent would be gone. "Nothing has changed since version N" is shown as INFO rather than an error: the author pressed Publish on something already published, which is a success they misread. EDITOR. Outline · canvas · inspector as a full-page nested route rather than a dialog — a slide needs a canvas beside an outline and an inspector, authoring is multi-session work that has to survive a refresh, and hrTabMatches was written so a nested route keeps the parent tab lit and MANAGE_TRAINING-gated with no new registration. Saves are per-ITEM and debounced; a whole-course PUT with replace-set semantics turns two admins editing two different units into one silently losing their work. After each flush the course is re-read, because saveItemContent recreates child rows and a stale local copy would send phantom ids on the next save. Reordering is arrow buttons driven by moveInOrder, which CLAMPS rather than wrapping — wrapping silently reorders a course the author did not mean to reorder, and the order IS the reading order. MEDIA comes from /hr/resources through a new LibraryPickerDialog, generalised out of MaterialPickerDialog by lifting its two hardcoded constants into props. assocType is the ORDINAL (13), never the enum id (15): the enum skips id 6, and sending an id lands on a different collection's rules. Alt text is required on any media slide and blocks publish — the authoring tool is the only enforcement point that actually holds, because a review checklist is run once and alt text is added never. PLAYER. Resumes at the first GAP, never the furthest point reached, and every gate unions the server's record with THIS session — reading only the server's row makes Next unreachable the moment a tick has not round-tripped, which is the exact bug the legal wizard shipped. Video completion is measured as WATCHED COVERAGE, not currentTime. /api/imaging/stream honours HTTP Range end to end, so a learner can seek to the last second in one request and any position-based gate is defeated by a single drag; accumulate() treats a jump larger than the tick gap as a seek and opens a new interval instead of crediting the span jumped over. videoCoverage.test.ts asserts "seek to the end yields under 5%" as its first case. Coverage is media time, so 2x playback still has to cover the timeline — which is why allowing it is fair rather than a loophole. A visible escape hatch appears once the video's full length has elapsed in wall-clock time and records skipReason on the progress row, because a gate with no escape becomes a support ticket whose answer is always "we'll mark it complete for you" — the same outcome, untracked. The test runner carries no correctness data at all: the payload is built from PlayerQuestionDto, which has no such field, and the server decides. Answers autosave so a closed laptop is not a lost attempt, and blanks are allowed but must be confirmed. npm test: 3 guards clean, 102 files, 1257 tests. next build compiles.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing