Show progress: ladder, statistics, export and import - #17
Merged
Merged
Conversation
The drill could score a lesson but had nowhere to put the result, so stars were computed, shown once and thrown away, and a reload started the learner from nothing. This closes that loop and builds the three views around the drill. The ladder lists every lesson the layout produced, says which are unlocked and how many stars each has earned, and has a button on each unlocked rung so an earlier lesson can be returned to. Unlocking is one-way, so the ladder stays open behind a learner. A locked rung says what unlocks it and names the lesson, rather than only looking dimmer: stars are stated in words with the glyph row marked aria-hidden beside them. The statistics group weak keys by finger and by row, which is the thing this project exists to do. A list of missed characters is trivia; the same numbers gathered under the finger that presses them and the row it reaches for say "your left ring finger cannot find the upper row", and that is what a lesson can be built from. The grouping is a pure module, src/stats/keystats.ts, with no DOM, so the diagnosis is unit-tested without a browser; every level carries its own numbers, its own wording and a severity band named in words, with the tint and the little miss-rate bars as reinforcement only. Fingers that need work lead; the ones that are behaving sit in a disclosure. A key whose statistics the current layout does not bind is reported separately rather than dropped, so the totals are never quietly short. Progress is now actually persisted. Storage is probed rather than assumed, because localStorage throws in a private window, and when it is unavailable the page says plainly that progress will not outlive the tab. Export writes a dated JSON file, and import merges it back, taking the better of the file and this browser for every field so importing your own export can never cost you this morning's practice. A partly unreadable import reports what it dropped, item by item, instead of failing silently or refusing the whole file; only a file that is not JSON at all is refused, and then the message says so. Clearing saved progress takes two deliberate goes. The export is handed over as an object URL, not a relative href. The site deploys to GitHub Pages from /touchwright/, and a relative href resolves against the page's own path, so an export that worked at the root would have pointed at the wrong place one directory down. There is an end-to-end test that pushes a deeper path and exports from there. Regression 1 is kept, not undone: every load goes through ProgressStore, which thaws and validates, and the thawed copy is handed to applyLoadedProgress, which merges field by field. Nothing from a file or from storage becomes live state by reference. Clearing empties the session's own Progress object in place rather than replacing it. Two end-to-end fixmes are filled in: "keeps progress across a reload" and "exports progress, clears storage, and imports it back". The other four are left alone. nextLessonName is deliberately still null on the drill view. Issue #3 has not landed, so the result card would otherwise offer "Start <next lesson>" on a button that could only restart the same sample text. The lesson is chosen on the ladder instead, and showDrill already has the lesson's id, name and key set in hand, so wiring generated text in is one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The views around the drill, and the wiring that makes progress actually
persist. Stars were being computed, shown once, and thrown away; a reload
started the learner from nothing.
What is here
The ladder lists every lesson the layout produced, says which are unlocked
and how many stars each has earned, and puts a button on every unlocked rung so
an earlier lesson can be returned to. Unlocking is one-way, so the ladder stays
open behind a learner. Star counts are stated in words with the glyph row
aria-hiddenbeside them, and a locked rung says what unlocks it and names thelesson rather than only looking dimmer.
The statistics group weak keys by finger and by row, which is the thing
this project exists to do. A list of missed characters is trivia; the same
numbers gathered under the finger that presses them and the row it reaches for
say "your left ring finger cannot find the upper row", and that is what a lesson
can be built from. The grouping is a pure module,
src/stats/keystats.ts, withno DOM, so it is unit-tested without a browser. Every level carries its own
numbers, its own wording and a severity band named in words; the tint and the
miss-rate bars are reinforcement only, and the bars are
aria-hiddenwith thesame number printed beside them. Fingers that need work lead; the ones that are
behaving sit in a disclosure.
describeKeysupplies the wording for the part agroup heading does not already say — a sideways reach, or which arc of the thumb
cluster — so this view and the drill readout can never disagree about a
position. A key the current layout does not bind is reported separately rather
than dropped.
Export and import. Storage is probed rather than assumed, because
localStorage throws in a private window; when it is unavailable the page says
plainly that progress will not outlive the tab. Export writes a dated JSON file.
Import merges it back, taking the better of the file and this browser for every
field, so importing your own export can never cost you this morning's practice.
A partly unreadable import reports what it dropped, item by item, instead of
failing silently or refusing the whole file —
readProgress's warnings areprinted as a list. Only a file that is not JSON at all is refused, and then the
message says so. Clearing saved progress takes two deliberate goes.
Stars are written to progress.
createDrillViewnow takes alessonIdandrecords best-of stars plus experience when a drill is scored, then calls
onScoredso the caller can persist and refresh. Best-of, never last-of: a badrun on a lesson already passed does not take a rung away.
The subpath caveat
The export is handed over as an object URL, not a relative
href. The sitedeploys to GitHub Pages from
/touchwright/, and a relative href resolvesagainst the page's own path, so an export that worked at the root would have
pointed at the wrong place one directory down.
tests/e2e/progress.spec.tspushes a deeper path with
history.replaceStateand exports from there.Regression 1 is kept
Every load goes through
ProgressStore, which thaws and validates, and thethawed copy is handed to
session.applyLoadedProgress, which merges field byfield. Nothing from a file or from storage becomes live state by reference.
Clearing empties the session's own
Progressobject in place rather thanreplacing it, so the session keeps the one object it records keystrokes into.
tests/regression/prototype-failures.test.tsis untouched and still passes, andthere are functional tests that type a keystroke straight after an import and
after a clear.
Tests
tests/unit/keystats.test.ts— 19 tests over the grouping, thresholds,wording and boundary behaviour.
tests/functional/progress-view.test.ts— the views against the real markup.tests/functional/progress-view-wiring.test.ts— the whole loop: load alayout, type a drill, have the stars written down, find them again after a
second
wireUpover the same storage driver.tests/e2e/progress.spec.ts— the ladder, the statistics, downloads, a secondbrowser context, axe in both themes, target sizes, focus rings and reduced
motion.
tests/e2e/pending.spec.ts— keeps progress across a reload and exportsprogress, clears storage, and imports it back are filled in. The other four
fixmes are left alone.
npm run verifyandnpx playwright testboth pass: 443 vitest tests and 120Playwright tests across both themes.
Deliberately not done
nextLessonNameis still null on the drill view. Issue #3 has not landed, sothe result card would otherwise offer "Start <next lesson>" on a button
that could only restart the same sample text. The lesson is chosen on the ladder
instead.
showDrillalready has the lesson's id, name and key set in hand, sowiring generated text in is one line, marked with a seam comment.
Closes #9
🤖 Generated with Claude Code