[Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

Description

@radroid

What happened

Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

Why it happens

CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

  • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
  • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

Expected

The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

Suggested fix

Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

  • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
  • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
  • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

Surfaces

Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

Tests

updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

      Description

      @radroid

      What happened

      Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

      Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

      Why it happens

      CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

      • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
      • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

      selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

      Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

      Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

      The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

      Expected

      The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

      Suggested fix

      Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

      • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
      • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
      • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

      Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

      Surfaces

      Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

      Tests

      updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        bugSomething isn't working

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

          Description

          @radroid

          What happened

          Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

          Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

          Why it happens

          CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

          • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
          • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

          selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

          Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

          Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

          The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

          Expected

          The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

          Suggested fix

          Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

          • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
          • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
          • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

          Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

          Surfaces

          Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

          Tests

          updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            bugSomething isn't working

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

              Description

              @radroid

              What happened

              Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

              Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

              Why it happens

              CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

              • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
              • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

              selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

              Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

              Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

              The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

              Expected

              The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

              Suggested fix

              Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

              • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
              • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
              • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

              Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

              Surfaces

              Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

              Tests

              updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                bugSomething isn't working

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

                  Description

                  @radroid

                  What happened

                  Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

                  Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

                  Why it happens

                  CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

                  • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
                  • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

                  selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

                  Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

                  Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

                  The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

                  Expected

                  The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

                  Suggested fix

                  Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

                  • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
                  • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
                  • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

                  Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

                  Surfaces

                  Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

                  Tests

                  updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    bugSomething isn't working

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

                      Description

                      @radroid

                      What happened

                      Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

                      Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

                      Why it happens

                      CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

                      • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
                      • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

                      selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

                      Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

                      Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

                      The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

                      Expected

                      The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

                      Suggested fix

                      Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

                      • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
                      • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
                      • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

                      Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

                      Surfaces

                      Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

                      Tests

                      updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        bugSomething isn't working

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

                          Description

                          @radroid

                          What happened

                          Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

                          Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

                          Why it happens

                          CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

                          • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
                          • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

                          selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

                          Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

                          Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

                          The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

                          Expected

                          The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

                          Suggested fix

                          Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

                          • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
                          • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
                          • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

                          Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

                          Surfaces

                          Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

                          Tests

                          updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            bugSomething isn't working

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              [Bug]: update toast's "built …" age freezes at renderer mount — a build offered 8h ago still reads "built just now" #127

                              Description

                              @radroid

                              What happened

                              Left the app running overnight with a staged update. The toast said "built just now" when it appeared, and it still said "built just now" ~7–8 hours later, next to a "Restart to update T3 Coil." that had been sitting there the whole time.

                              Toast contents at the time of the screenshot: 20 changes ready to run / "Restart to update T3 Coil." / built just now in the top-right / Restart when idle + Restart.

                              Why it happens

                              CoilUpdateToast seeds its clock once and only ticks it while an auto-restart is armed.

                              • apps/web/src/components/coil/UpdateToast.tsx:65const [now, setNow] = useState(() => Date.now()), evaluated once at mount.
                              • apps/web/src/components/coil/UpdateToast.tsx:71 — the CEILING_TICK_MS interval bails out with if (autoRestart === undefined) return;. It exists to fire the two-hour arm ceiling, and the comment is explicit that an idle app should not wake up for it.

                              selectUpdateToastView threads that now into formatBuiltAgo(builtAt, now) (updateToast.logic.ts:307), so in the common case — toast is up, nothing armed — the age string is pinned to whenever the renderer happened to mount and never advances.

                              Worse, the value it pins to is usually wrong at birth, not merely stale. The app is normally launched before the build exists; the toast arrives hours later over bridge.onState, and builtAgo gets computed against a now that predates builtAt. That's a negative age, which formatBuiltAgo deliberately clamps to "just now" (updateToast.logic.ts:218-226) to avoid rendering "in 3 minutes". The clamp is right for clock skew; here it's silently laundering a stale clock into the single most reassuring string the label can produce.

                              Net effect: the field is worse than absent. "built just now" on a build that is hours old actively misinforms — and this label is the one thing telling a maintainer whether the staged bundle contains the commit they just merged.

                              The hours/days branches of formatBuiltAgo are, as far as I can tell, unreachable in practice today.

                              Expected

                              The age advances while the toast is on screen: just now12 min ago1h ago8h ago.

                              Suggested fix

                              Tick now whenever the toast is visible, not only while armed, and keep the cadence coarse enough to stay honest about the "no continuously repainting animations" rule in AGENTS.md:

                              • Run the interval when autoRestart !== undefinedorstate.status.kind === "ready".
                              • One tick a minute is already the right granularity for both consumers — the ceiling has two hours of slack, and formatBuiltAgo's finest bucket is a minute.
                              • Re-seed now on the ready transition too, so the first paint of a freshly-arrived build isn't computed against a mount-time clock.

                              Cost check: viewKey is JSON.stringify(view), so a tick that doesn't change the bucket produces no toastManager.update — a minute-ticking setNow costs one re-render of this component per minute while a toast is up, and a toast payload write only when the string actually changes. That's within budget, but the interval must still stop when the view is hidden or the app is back to waking once a minute forever for nothing.

                              Surfaces

                              Desktop only. The feature is gated on isElectron and there is no bundle to swap in a browser tab, so web and mobile are unaffected. All three desktop platforms show the label.

                              Tests

                              updateToast.logic.test.ts covers formatBuiltAgo at fixed now values and passes — the pure function is fine. The gap is entirely in the container's clock, which has no test. A regression test wants to assert the interval runs on ready without an arm.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                bugSomething isn't working

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions