Don't quietly write over a save that couldn't be read #78

Closed
jheaps wants to merge 1 commits from fix/casino-holdem-unreadable-save into fix/casino-holdem-say-when-not-saving
Owner

#77 made a failing save say so. This is the other direction, and it turned out to be worse.

load() is well defended — try/catch, a shape check, sensible fallbacks — and every one of those paths ends in return this.createDefault(). So an unreadable save becomes an empty table. Driven with a save truncated to 70% of its length, the way an interrupted write leaves one:

1762 bytes stored, 1233 after truncating
loaded: 0 named seats, hand #1 instead of #34
toast: none.  warning: none.  anything on the page: no.

A dealer mid-session sees an empty felt and can't tell whether they mis-tapped Reset, whether the browser lost the night, or what.

And then the first edit destroys the evidence. I typed a name into a seat and storage went from the 1233 unreadable bytes to 2549 bytes of fresh default state. Whatever a person could have picked out of that JSON — names, buy-ins, standings — was gone one tap after the tool decided it couldn't read it.

What it does now

The unreadable value is moved aside to its own key before the fallback, and the table says what happened:

toast: The saved table couldn't be read, so this one started empty.

panel: The saved table couldn't be read, so this one started empty. The unreadable copy has been kept rather than written over.

It's deliberately not offered as a restore. Half a JSON document isn't a table, and pretending otherwise would be worse than an empty one. It just stops being destroyed.

The two warnings share the Session panel line and both can show at once — a browser having trouble with storage tends to have it in both directions — with the load message first, because "why is my table empty" is the question the dealer is standing there asking.

Validation

8 tests (263 total): nothing stored is a new table rather than a failure, a good save loads clean, a truncated one is reported, valid JSON of the wrong shape counts too, the copy is kept, a later save doesn't touch it, the listener fires once across three reads, and storage with no room to keep the copy isn't a crash — which is exactly the storage most likely to hand back half a value in the first place.

Driven through the UI: warning and toast on a corrupted save, the set-aside copy still 1233 bytes intact after editing a seat, and nothing shown at all on a healthy table.

One audit correction rides along

The collapsed-column check measured inside hidden elements, so the standings header — hidden with its table until somebody buys in — was reported as three collapsed columns on every empty table. It skips hidden subtrees now, and still catches a real one: squeezing a visible column reports the header and all three rows.

#77 made a failing *save* say so. This is the other direction, and it turned out to be worse. `load()` is well defended — try/catch, a shape check, sensible fallbacks — and every one of those paths ends in `return this.createDefault()`. So an unreadable save becomes an empty table. Driven with a save truncated to 70% of its length, the way an interrupted write leaves one: ``` 1762 bytes stored, 1233 after truncating loaded: 0 named seats, hand #1 instead of #34 toast: none. warning: none. anything on the page: no. ``` A dealer mid-session sees an empty felt and can't tell whether they mis-tapped Reset, whether the browser lost the night, or what. **And then the first edit destroys the evidence.** I typed a name into a seat and storage went from the 1233 unreadable bytes to 2549 bytes of fresh default state. Whatever a person could have picked out of that JSON — names, buy-ins, standings — was gone one tap after the tool decided it couldn't read it. ## What it does now The unreadable value is moved aside to its own key before the fallback, and the table says what happened: > **toast:** The saved table couldn't be read, so this one started empty. > > **panel:** The saved table couldn't be read, so this one started empty. The unreadable copy has been kept rather than written over. It's deliberately **not** offered as a restore. Half a JSON document isn't a table, and pretending otherwise would be worse than an empty one. It just stops being destroyed. The two warnings share the Session panel line and both can show at once — a browser having trouble with storage tends to have it in both directions — with the load message first, because "why is my table empty" is the question the dealer is standing there asking. ## Validation **8 tests (263 total)**: nothing stored is a new table rather than a failure, a good save loads clean, a truncated one is reported, valid JSON of the wrong shape counts too, the copy is kept, a later save doesn't touch it, the listener fires once across three reads, and storage with no room to keep the copy isn't a crash — which is exactly the storage most likely to hand back half a value in the first place. Driven through the UI: warning and toast on a corrupted save, the set-aside copy still 1233 bytes intact after editing a seat, and nothing shown at all on a healthy table. ## One audit correction rides along The collapsed-column check measured inside hidden elements, so the standings header — hidden with its table until somebody buys in — was reported as three collapsed columns on every empty table. It skips hidden subtrees now, and still catches a real one: squeezing a visible column reports the header and all three rows.
jheaps added 1 commit 2026-09-01 18:22:57 -06:00
Last change made a failing save say so. This is the other direction, and it turned out
to be worse.

load() is well defended -- try/catch, a shape check, sensible fallbacks -- and every
one of those paths ends in `return this.createDefault()`. So an unreadable save becomes
an empty table. Driven with a save truncated to 70% of its length, the way an
interrupted write leaves one:

  1762 bytes stored, 1233 after truncating
  loaded: 0 named seats, hand #1 instead of #34
  toast: none.  warning: none.  anything on the page: no.

A dealer mid-session sees an empty felt and cannot tell whether they mis-tapped Reset,
whether the browser lost the night, or what.

And then the first edit destroys the evidence. I typed a name into a seat and storage
went from the 1233 unreadable bytes to 2549 bytes of fresh default state. Whatever a
person could have picked out of that JSON -- the names, the buy-ins, the standings --
was gone one tap after the tool decided it could not read it.

Now the unreadable value is moved aside to its own key before the fallback, and the
table says what happened:

  toast:  "The saved table couldn't be read, so this one started empty."
  panel:  "The saved table couldn't be read, so this one started empty. The
           unreadable copy has been kept rather than written over."

It is deliberately not offered as a restore. Half a JSON document is not a table, and
pretending otherwise would be worse than an empty one. It just stops being destroyed.

The two warnings share the Session panel line and both can show at once -- a browser
having trouble with storage tends to have it in both directions -- with the load
message first, because "why is my table empty" is the question the dealer is standing
there asking.

8 tests (263 total): nothing stored is a new table rather than a failure, a good save
loads clean, a truncated one is reported, valid JSON of the wrong shape counts too, the
copy is kept, a later save does not touch it, the listener fires once across three
reads, and storage with no room to keep the copy is not a crash -- which is exactly the
storage most likely to hand back half a value in the first place.

Driven through the UI: warning and toast on a corrupted save, the set-aside copy still
1233 bytes intact after editing a seat, and nothing shown at all on a healthy table.

One audit correction rides along. The collapsed-column check measured inside hidden
elements, so the standings header -- hidden with its table until somebody buys in --
was reported as three collapsed columns on every empty table. It skips hidden subtrees
now, and still catches a real one: squeezing a visible column reports the header and
all three rows.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps closed this pull request 2026-09-02 09:57:54 -06:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jheaps/JoshHeaps.Net#78