feat: initial tutorial-packs plugin (intro-bends + reading-the-highway)#1
Conversation
Adds slopsmith-plugin-tutorials: an interactive video-based tutorial system with Browse, Lesson, and Author modes. Fixes applied from Codex preflight: - Builtin pack sloppaks are now seeded into <DLC_DIR>/tutorials-builtin/ so playSong can resolve them via the core highway WS - pack.json exercise references updated to use DLC-relative paths - readLessonEditor uses NaN-safe _numDefault/_intDefault helpers instead of || so an explicitly authored 0 threshold is preserved - Hot-reload guard moved below const declarations to avoid TDZ crash - song:ended subscription guarded against duplicate on plugin reload - pendingRun no longer set when playSong is unavailable - Stale screen.html routing comment corrected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
There was a problem hiding this comment.
Pull request overview
This PR introduces the initial Tutorials plugin for Slopsmith, adding a video-and-exercise tutorial system with backend pack management, frontend browse/authoring UI, and bundled starter packs.
Changes:
- Adds plugin manifest, screen UI, SPA behavior, and FastAPI routes for packs, uploads, progress, and run recording.
- Ships builtin tutorial pack manifests, generators, covers/thumbnails, and sloppak assets.
- Adds README and agent guidance documenting architecture and usage.
Reviewed changes
Copilot reviewed 11 out of 39 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
plugin.json |
Declares the Tutorials plugin entry points and server file settings. |
screen.html |
Adds the plugin root layout and scoped inline styles. |
screen.js |
Implements browse, lesson, authoring, uploads, playback launch, and run submission UI. |
routes.py |
Adds backend pack CRUD, media serving/upload, builtin seeding, sloppak copy, and progress APIs. |
README.md |
Documents the plugin concept, API, generation flow, and architecture notes. |
CLAUDE.md |
Adds repository-specific architecture invariants and out-of-scope notes. |
builtin/intro-bends/README.md |
Documents the Intro to Bends starter pack asset checklist. |
builtin/intro-bends/pack.json |
Defines the 3-lesson Intro to Bends builtin pack. |
builtin/intro-bends/generate.py |
Adds generator for Intro to Bends sloppaks, cover, and thumbnails. |
builtin/reading-the-highway/pack.json |
Defines the 10-lesson Reading the Highway builtin pack. |
builtin/reading-the-highway/generate.py |
Adds generator for Reading the Highway sloppaks, cover, and thumbnails. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- routes.py: add type/range validation for pass/mastery thresholds in _validate_manifest so record_run never hits AttributeError on a bad PUT - screen.js: guard file-video branch with `video.src && typeof video.src === 'string'` so empty src falls through to "No video attached" state instead of rendering a broken <video> element - builtin/*/pack.json: clear video.src to "" (videos not yet recorded) and update sloppak refs to DLC-relative paths for playSong resolution - builtin/reading-the-highway/generate.py: replace hash() with hashlib.sha256-based seed for reproducible generation; fix manifest template to write DLC-relative sloppak paths and empty video.src - builtin/intro-bends/generate.py: correct string-index docstring (s=0 is LOW E, opposite of GP numbering) - builtin/intro-bends/README.md: update content checklist (sloppaks / cover / thumbs now marked committed; video files remain pending) - README.md: correct /runs API description; remove stale XML artifact - CLAUDE.md: correct XP architecture (frontend posts both independently; no server-side relay) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Addressing the two intentionally-deferred Copilot findings: screen.js:122 — stale screen.js:812 — |
- screen.js: add renderToken guard to prevent stale async renders; renderPackDetail and renderLesson bail if state.renderToken changed while awaiting /packs fetch (navigation-away race) - screen.js: add role/tabindex/onkeydown to pack card (article) and lesson row (div) so keyboard-only users can open them with Enter/Space - screen.js: rename "Locked" lesson state label to "Not started" — lessons are not actually gated so "Locked" was misleading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Round 2 deferred findings — intentional non-fixes: screen.js:1093 — stale screen.js:461 — |
- routes.py: tighten null-threshold validation in _validate_manifest; use `in` to detect explicit JSON null (vs missing key), and also reject bool values (bool is a subclass of int in Python) - screen.js: strip derived fields (cover_url, lesson.thumb_url) from manifest before PUT so response-only URLs are never persisted to pack.json and cannot become stale after the underlying file is removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Round 3 deferred finding: screen.js:1107 — stale |
- screen.js: guard onSongEnded against stale pendingRun — clear and return if activePackId/activeLessonId no longer match when song ends - screen.js: clamp packProgressPct to 100% to prevent over-count from stale progress entries on removed/recreated packs - screen.js: strip cover_url/thumb_url derived fields before PUT (done in round 3); no new changes here — companion for the HTML fix below - screen.html: replace role="tablist" + aria-selected on mode buttons with aria-label="Tutorials mode" + aria-pressed; update matching CSS selector from [aria-selected="true"] to [aria-pressed="true"] so the active-mode highlight is not lost - builtin/*/generate.py: copy sloppaks to tutorials-builtin/<pack>/ subdirectory instead of DLC root, matching the DLC-relative paths in the committed manifests (fixes immediate playability after regen) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tics The Author form previously said "arrangement id (optional)" which implied string IDs like "lead" would work. Core's playSong only accepts integer arrangement indices; string IDs are silently ignored. Update the placeholder to "arrangement index 0, 1, 2… (optional)". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Round 4 deferred findings: screen.js:1118 — stale screen.js:952 and screen.js:967 — cover upload/remove calls screen.js:475 — |
- routes.py: add range validation for accuracy thresholds (must be in [0, 1]) and mastery.speed (must be positive) in _validate_manifest - screen.js: add title attr to YouTube iframe for screen-reader users - screen.js: add aria-label="Remove tag <value>" to tag-chip remove buttons so assistive tech can identify which tag is being removed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Round 5 deferred findings: screen.js:1129 — stale screen.js:847 and screen.js:475 — screen.js:962 and screen.js:977 — cover upload/remove triggers full re-render screen.js:489 — minigames XP awarded before local progress recorded |
- screen.js: fix arrangement parsing to use String(ex.arrangement ?? '') before trim() so numeric JSON values (e.g. exercise.arrangement: 1) don't throw, and arrangement 0 isn't dropped by the || coercion; also use /^\d+$/ regex to reject partial-int strings like "1abc" - README.md: expand generator requirements section to list all needed tools: fluidsynth + soundfont, ffmpeg, mido, Pillow, PyYAML Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… in record_run - Move the stable song:ended wrapper registration out of init() and into the IIFE bottom section (after the singleton is created), so the wrapper is always registered on first load regardless of readyState. On hot-reload the _onSongEnded pointer is updated at the top of the hot-reload branch before init() is called, rather than inside init() where the old guard `if (window.slopsmithTutorials)` could be missed. - In record_run, use explicit `is not None` checks instead of dict.get() defaults for pass/mastery thresholds. dict.get(key, default) returns None when the key is present with a JSON null value, which would cause TypeError in the accuracy comparisons. The new pattern falls back to the safe default (0.7 / 0.9 / 1.0) only when the value is genuinely None. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e response Introduce refreshPacksOnly() that updates state.packs without calling render(), so the Browse-side pack card picks up a fresh cover_url on the next navigation without destroying any unsaved Author form state (lesson list, thresholds, titles) currently in the form. Cover upload and cover remove now both await refreshPacksOnly() instead of refreshAndRender(), preserving all unsaved Author edits. The local refreshPreview() call still fires immediately to update the preview element in place. Also: the cover delete handler now checks dr.ok and throws on non-2xx responses instead of silently updating the UI on a failed delete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The copy_sloppak endpoint previously allowed any file extension from
the DLC directory to be copied into a pack's sloppaks/ dir and served
via the GET /sloppaks/{filename} route. Only .sloppak files should be
copyable. Add a suffix check that returns 400 for any other extension.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rrangement ?? fix - delete_cover and delete_lesson_thumb now return 404 when the pack directory does not exist, matching the behavior of upload and other mutating endpoints. Previously glob on a missing directory silently returned an empty list, making the response indistinguishable from a successful delete of an already-absent file. - Thumbnail delete handler now checks dr.ok and throws on non-2xx responses, matching the cover delete fix from the prior commit. - arrangement input initial value: use `??` instead of `||` so a numeric 0 arrangement index is preserved in the Author form. The `||` operator treats 0 as falsy and blanks the field. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap window.playSong() in try/catch; clear state.pendingRun and alert on synchronous exception so a playSong error does not leave the run permanently pending. - Raise speed input max from 1.5 → 2.0 to match RunRecord.speed upper bound; lessons with mastery.speed thresholds above 1.5 were previously unreachable via the form. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@coderabbitai review |
What this adds
An interactive video-based tutorial system for Slopsmith. The plugin lives at `/api/plugins/tutorials/` and adds a top-level Tutorials nav entry that exposes three modes:
Two starter content packs ship in `builtin/`:
Why `plugin.json` has no `nav` block
The plugin deliberately omits the `nav` block. The standard `nav` entry would add Tutorials as a dropdown item inside the plugins menu. Instead, `screen.js:injectTopLevelNav()` inserts a sibling link next to Library / Favorites / Upload / Settings — the user-facing design decision being that tutorials are a first-class nav destination, not buried in a plugin submenu. The id guards on each injection (`tut-nav-top-link`, `tut-nav-top-link-mobile`) prevent duplicates on `loadPlugins()` re-runs.
Format conventions
Upload caps
Generators are reproducible
The `sloppaks/`, `cover.png`, and `thumbs/` in each builtin pack are committed so consumers get working content out-of-the-box. To regenerate:
Requires `fluidsynth` + `FluidR3_GM.sf2` (both present in the Slopsmith Docker image).
Codex preflight findings addressed