Five defects found by exercising the running app rather than reading it:
- Backspace inside the sidebar rename field deleted the trendline instead of
a character. The window keydown handler never checked what was focused, so
correcting a typo in a line's name destroyed the line.
- playAlert() built a new AudioContext per alert and never closed it. Browsers
cap a document at roughly six, after which alerts stop making any sound.
One shared context now, with nodes released on end and a resume() for the
autoplay policy.
- Stored layer preferences were used verbatim, so any key added to
defaultPrefs later would be missing for existing visitors. A missing
enabled.ma is a crash rather than a cosmetic gap. They are now deep-merged
onto the defaults, and unparseable state falls back instead of throwing.
- The alert log keyed rows on a second-resolution timestamp, so two alerts in
the same second collided.
- Clusters embedded whole Level objects, including a moving average's entire
point history — hundreds of entries reaching back years. Because clusters
are re-sent on every closed 1m bar, this shipped the whole levels payload
once a minute. Members are now compact summaries and the client joins on
id; Cluster.to_dict() also stops round-tripping through asdict(), which was
deep-copying those arrays before discarding them.
/api/confluence drops from 61,838 to 1,245 bytes with five clusters live.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>