aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-18 08:05:53 -0700
committerAdam Soltys <[email protected]>2026-08-18 08:05:53 -0700
commit4465314218b95b8a4962a76f420867b9b670ec12 (patch)
tree64b37ad8389ce0bed5ba0d5178b912b25913fe39
parent82165619f9a6a7090f97d15cff4e1b1dd48b8dee (diff)
downloadcoinosv3-4465314218b95b8a4962a76f420867b9b670ec12.tar.gz
coinosv3-4465314218b95b8a4962a76f420867b9b670ec12.zip
A crashed render confesses instead of playing dead: the error goes on screen with a Reload button, ending the silent every-tap-is-a-no-op brick
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JtaatFfAbx91Nh3xqKVqKz
-rw-r--r--src/app.js26
1 files changed, 26 insertions, 0 deletions
diff --git a/src/app.js b/src/app.js
index 85274ae..fd536e1 100644
--- a/src/app.js
+++ b/src/app.js
@@ -409,6 +409,32 @@ function animatedAmount(key, sat) {
}
function render() {
+ // A render that throws must never brick the page silently: stale DOM keeps
+ // its handlers, every one of them calls render again, and every tap turns
+ // into a no-op. Show the error and offer the way out instead.
+ try { renderInner(); } catch (err) { renderCrash(err); }
+}
+function renderCrash(err) {
+ console.error('render failed:', err);
+ try {
+ const div = document.createElement('div');
+ div.className = 'card col';
+ div.style.cssText = 'margin:20px;gap:10px';
+ const h3 = document.createElement('h3');
+ h3.textContent = 'Something broke';
+ const pre = document.createElement('div');
+ pre.className = 'small muted';
+ pre.style.cssText = 'word-break:break-word;white-space:pre-wrap';
+ pre.textContent = String((err && (err.stack || err.message)) || err).slice(0, 600);
+ const btn = document.createElement('button');
+ btn.className = 'btn-primary btn-block';
+ btn.textContent = 'Reload';
+ btn.onclick = () => location.reload();
+ div.append(h3, pre, btn);
+ root.replaceChildren(div);
+ } catch {}
+}
+function renderInner() {
// A direct render subsumes any coalesced one queued by a background emit, so
// drop the pending one (a user action must not trigger a second rebuild).
if (_renderRaf) { clearTimeout(_renderRaf); _renderRaf = 0; }