Skip to content

Pausing mid-edit no longer discards the Cluster Strength - #376

Open
lstein wants to merge 1 commit into
masterfrom
lstein/fix/mid-typing-cluster-strength
Open

Pausing mid-edit no longer discards the Cluster Strength#376
lstein wants to merge 1 commit into
masterfrom
lstein/fix/mid-typing-cluster-strength

Conversation

@lstein

@lstein lstein commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Rebased onto master now that #370 has landed, and re-scoped: #370 changed what an empty field means, which changes what this bug is.

The bug

An empty Cluster Strength field is now a deliberate signal — it means "go back to a derived strength". The trap is that <input type="number"> reports an empty value for anything it cannot parse yet:

"0.4" -> value="0.4"
"0."  -> value=""      <- indistinguishable from a cleared field
"-"   -> value=""
"1e"  -> value=""

So an empty field alone cannot be read as the user asking for a derived value. Pausing for a second while retyping looks exactly like it, and throws away the number the album was tuned to:

album tuned to 0.4; user selects the field and retypes
  "0"   -> parseable, save armed
  "0."  -> value becomes "", i.e. "clear it"
  ...user hesitates for one second...
  -> POST /set_umap_eps/ {eps: null}
  -> the album is back on a derived strength, "auto" badge and all

The existing guard cannot catch this. if (eps !== null && Number.isNaN(eps)) is dead code for a number input: the sanitized value is "", so readSpinnerEps returns null, never NaN.

The fix

validity.badInput is the discriminator. The browser sets it only while the input holds text it could not turn into a number, so it separates "the user cleared this" from "the browser cannot parse this yet" — which reading .value alone cannot do.

Two smaller things in the same handler:

  • The pending save is dropped before every early return, not after them. Otherwise a save armed by an earlier keystroke still fires a second later carrying a number the field no longer shows.
  • A non-positive number is not saved. DBSCAN refuses one, so resolve_cluster_eps floors it and the map ends up clustered at something other than what the spinner and the cluster-info modal report. (Refuse a Cluster Strength that DBSCAN cannot run with #375 refuses the same values server-side; this stops the UI producing them.)

Tests

5 tests in tests/frontend/umap-eps-debounce.test.js. Against master, three fail and two pass:

✕ does not clear the album's strength when the user pauses mid-number
✓ still treats a genuinely emptied field as a request to derive one
✓ saves the number once the user finishes typing it
✕ does not let a pending save land after the field goes unparseable
✕ refuses to save a strength DBSCAN cannot use

The two that already pass are there as guards: the deliberate-clear path is the feature #370 added, and it must keep working.

Honest caveat on the main one. jsdom sanitizes an unparseable value to "" but never sets badInput (verified — see the typeUnparseable helper, which stubs it). These tests pin this module's logic; that a browser really sets badInput for a half-typed number is a platform guarantee, not something this suite proves. Worth a manual check in a real browser before merging.

597 frontend tests pass; eslint and prettier clean.

🤖 Generated with Claude Code

An empty Cluster Strength field is a deliberate signal — it means "go back
to a derived strength". The trap is that `<input type="number">` reports an
empty value for anything it cannot parse *yet*: "0.", "-", "1e". So an
empty field alone cannot be read as the user asking for a derived value,
and pausing for a second while retyping looked exactly like it, throwing
away the number the album was tuned to.

`validity.badInput` separates the two: the browser sets it only while the
input holds text it could not turn into a number. `Number.isNaN` cannot do
this job — the sanitized value is "", never "NaN" — so the guard that was
there was dead code for this element.

Two smaller things in the same handler:

* The pending save is now dropped before *every* early return rather than
  after them. Otherwise a save armed by an earlier keystroke still fires a
  second later, carrying a number the field no longer shows.
* A non-positive number is not saved at all. DBSCAN refuses one, so
  `resolve_cluster_eps` floors it and the map ends up clustered at something
  other than the number the spinner and the cluster-info modal report.

Note on the tests: jsdom sanitizes an unparseable value to "" but never sets
`badInput`, so the flag is stubbed. These tests pin this module's logic; that
a browser really sets `badInput` for a half-typed number is a platform
guarantee, not something the suite proves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lstein
lstein force-pushed the lstein/fix/mid-typing-cluster-strength branch from 0afca9a to 2d779f2 Compare August 20, 2026 00:18
@lstein lstein changed the title Pausing mid-edit no longer overwrites the Cluster Strength Pausing mid-edit no longer discards the Cluster Strength Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant