← Commit history

0.5.54: the clock ticks on the minute; no polling while a token is expired

John Lauer ·cdeb9a7cdc · ·parent b092718
4 files changed +20−9
bin/hydrogen-widget-clock+1−1
@@ -9,4 +9,4 @@ def data():     now = datetime.datetime.now()     return {'time': now.strftime('%H:%M'), 'date': now.strftime('%a %d %b')} RENDER = r"""(`<span class="big">${d.time}</span><span class="lbl">${d.date}</span>`)"""-lib.serve('Clock', data, RENDER, 15000)+lib.serve('Clock', data, RENDER, 60000, align_ms=60000)
bin/hydrogen-widget-lib.py+16−5
@@ -61,7 +61,8 @@ function postWidth(){try{const w=document.querySelector('.w');if(!w)return;   document.body.appendChild(c);const total=Math.ceil(c.getBoundingClientRect().width)+2;c.remove();   if(total>0&&window.parent!==window)window.parent.postMessage({hdWidget:{width:total}},'*');}catch(e){}} async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});const d=await r.json();window.__last=d;render(d);}catch(e){document.getElementById('w').innerHTML='<span class="err">no data</span>';}}-let tm=null;function arm(){if(tm)return;tm=setInterval(tick,R);}function disarm(){if(tm){clearInterval(tm);tm=null;}}+/* align_ms: fire on the boundary (a clock ticks once per minute, on the minute) instead of every R ms. */+const A=%(align_ms)d;let tm=null;function arm(){if(tm)return;if(A){const ms=A-(Date.now()%%A)+50;tm=setTimeout(()=>{tm=null;tick();arm();},ms);}else tm=setInterval(tick,R);}function disarm(){if(tm){if(A)clearTimeout(tm);else clearInterval(tm);tm=null;}} document.addEventListener('visibilitychange',()=>{if(document.hidden)disarm();else{tick();arm();}}); tick();if(!document.hidden)arm(); /* The slot resizes when the Title Bar goes compact or tall; the iframe's own window gets the resize event and the@@ -69,16 +70,23 @@ tick();if(!document.hidden)arm(); let rz=null;window.addEventListener('resize',()=>{clearTimeout(rz);rz=setTimeout(()=>{if(window.__last)render(window.__last);},80);}); </script></body>""" -def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):+def serve(title, data_fn, render_js, refresh_ms=60000, mark=None, align_ms=0):     mark = mark or {'svg': '', 'color': 'currentColor'}     port = int(os.environ.get('HYDROGEN_WIDGET_PORT') or (sys.argv[sys.argv.index('--port')+1] if '--port' in sys.argv else 8930))     # One upstream call per interval at most, however often the page asks; on an upstream     # error the interval doubles up to 15 minutes and returns to normal on the next success.     cache = {'t': 0, 'v': {'error': 'starting'}, 'wait': refresh_ms/1000, 'good': None}     lock = threading.Lock()+    def _creds_mtime():+        try: return max(os.stat(os.path.expanduser(f)).st_mtime for f in ('~/.claude/.credentials.json', '~/.codex/auth.json') if os.path.exists(os.path.expanduser(f)))+        except Exception: return 0     def data():         with lock:-            if time.time() - cache['t'] >= cache['wait'] - 1:+            due = time.time() - cache['t'] >= cache['wait'] - 1+            if cache.get('expired_mtime') is not None and not due and _creds_mtime() != cache['expired_mtime']:+                due = True  # the sign-in file changed: try now+            if due:+                cache['expired_mtime'] = None                 try:                     cache['v'] = data_fn(); cache['wait'] = refresh_ms/1000                     if not cache['v'].get('error'): cache['good'] = cache['v']@@ -92,7 +100,10 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):                     code = getattr(e, 'code', None)                     if code in (401, 403):                         msg = '%s token expired, waiting for %s to refresh it' % (title.replace(' usage', ''), 'Claude Code' if 'Claude' in title else 'Codex')-                        cache['wait'] = 60+                        # No polling while expired (John 2026-09-14: "stop that loop when it is not+                        # needed"): the next call happens when the sign-in file changes, or after+                        # fifteen minutes as a safety net. A stat per page poll is the whole cost.+                        cache['wait'] = 900; cache['expired_mtime'] = _creds_mtime()                     else:                         msg = str(e)[:120]; cache['wait'] = min(900, max(refresh_ms/1000, cache['wait']) * 2)                     cache['v'] = {'error': msg, 'stale': cache['good']} if cache['good'] else {'error': msg}@@ -123,7 +134,7 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):                         'const f=document.getElementById("w");f.dataset.w=m.width;if(!fixed)f.style.width=Math.max(48,Math.min(640,m.width))+"px";});</script></body>') % (title, w, h, 'true' if fixed else 'false')                 self._send(200, 'text/html; charset=utf-8', page.encode())             elif self.path.startswith('/health'): self._send(200, 'application/json', b'{"ok":true}')-            else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'render_js': render_js, 'mark_html': ('<span class="mark" style="color:%s"><svg viewBox="0 0 16 16">%s</svg></span>' % (mark['color'], mark['svg'])) if (mark and mark.get('svg')) else ''}).encode())+            else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'align_ms': int(align_ms), 'render_js': render_js, 'mark_html': ('<span class="mark" style="color:%s"><svg viewBox="0 0 16 16">%s</svg></span>' % (mark['color'], mark['svg'])) if (mark and mark.get('svg')) else ''}).encode())     # Bind with retries: a previous instance Hydrogen just stopped may still be releasing the     # port for a moment. Ten tries over five seconds, then give up loudly (the log says why).     ThreadingHTTPServer.allow_reuse_address = True
package.json+1−1
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.5.53",+  "version": "0.5.54",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "tags": [
page.json+2−2
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.5.53",+  "version": "0.5.54",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "tags": [@@ -49,4 +49,4 @@     "hydrogen editor configuration",     "where do hydrogen skills come from"   ]-}\ No newline at end of file+}