Wraps navigator.serviceWorker.ready with a 10s timeout so subscribe,
unsubscribe, and existing-subscription checks surface a recoverable
error instead of hanging indefinitely when SW registration fails.
Virtual sections (__appearance__, __notifications__) now respect
includeSections/excludeSections filters so they only appear in
their intended tabs. Push initialization re-registers the local
subscription with the gateway to handle state-dir loss scenarios.
- Use base-path-relative manifest URL (no leading slash) so PWA works
under non-root base paths
- Classify push.web.* methods in WRITE_SCOPE so they work for operator
sessions without admin scope
- Refresh webPushPermission in the finally block of handleWebPushSubscribe
so denied permission is immediately reflected in the UI
- Check OPENCLAW_VAPID_PUBLIC_KEY/PRIVATE_KEY env vars before falling
back to persisted keys, so operators can share a stable VAPID identity
across multiple gateway instances
- Wrap VAPID key bootstrap in async lock to prevent concurrent first-run
callers from generating different keypairs
- Add test for env var precedence
Make the Control UI installable as a Progressive Web App and add Web Push
notification infrastructure so the gateway can send browser notifications
to subscribed clients.
PWA shell:
- Add manifest.webmanifest with app name, icons, standalone display
- Add service worker with cache-first for hashed assets, network-first
for HTML/API, push event handler, and notification click focus
- Register service worker from main.ts
- Serve .webmanifest with correct content type from gateway
- Add worker-src 'self' to CSP
Web Push server (src/infra/push-web.ts):
- VAPID key auto-generation and persistence to state dir
- Subscription CRUD with async-locked atomic JSON storage
- Send and broadcast via web-push library
- Mirrors push-apns.ts patterns
Gateway RPC (4 new methods):
- push.web.vapidPublicKey: returns VAPID public key for PushManager
- push.web.subscribe: registers browser push subscription
- push.web.unsubscribe: removes subscription by endpoint
- push.web.test: broadcasts test notification to all subscribers
Client-side push manager (ui/src/ui/push-subscription.ts):
- Permission request, VAPID key fetch, PushManager subscribe/unsubscribe
- Gateway RPC wiring for registration and test
Settings UI:
- New "Notifications" virtual section in Communications tab
- Shows browser support, permission state, subscription status
- Subscribe/unsubscribe/test buttons
Includes 10 unit tests for push-web subscription CRUD and VAPID keys.
Preserve Google Chat reply text when typing indicator cleanup or update fails.
- Extract Google Chat reply delivery into a focused module
- Retry the failed first text chunk as a new message after placeholder update failure
- Cover media caption and chunk fallback regressions
Thanks @colin-lgtm.
Summary:
- Show full date and time in Control UI chat message footers.
- Collapse assistant model/token/context metadata behind an explicit Context disclosure.
- Update changelog attribution guidance to allow multi-author credited entries.
Validation:
- OPENCLAW_LOCAL_CHECK=0 pnpm test ui/src/ui/chat/grouped-render.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm test src/commands/gateway-status/helpers.test.ts
- OPENCLAW_LOCAL_CHECK=0 pnpm check:changed
- GitHub CI passed on f071a38177
eleven_v3 already works end-to-end (model_id passes through to the API
without validation), but was missing from ELEVENLABS_TTS_MODELS so it
never appeared in the in-product model picker or catalog metadata.
* fix(heartbeat): clamp scheduler delay to Node setTimeout cap (#71414)
When `agents.defaults.heartbeat.every` resolves to >2_147_483_647 ms
(~24.85d), the previous scheduleNext() called setTimeout with the raw
delay. Node clamps any delay > 2^31-1 to 1 ms, fires the callback, and
the heartbeat re-arms with the same oversized value - a tight loop that
floods the log with TimeoutOverflowWarning and crashes the gateway with
exit code 1.
Clamp the computed delay to HEARTBEAT_MAX_TIMEOUT_MS (2_147_483_647)
before calling setTimeout. The worst case is now one heartbeat every
~24.85d instead of crash-loop. Warn once per process when clamping
fires, so a misconfigured "365d" remains visible without flooding.
This is a defense-in-depth fix at the scheduler layer; loadConfig-level
rejection is a broader change with more blast radius and a separate
question (some users may legitimately want "every: 365d" to mean
"effectively never"). The clamped behaviour is closer to that intent
than the crash is.
Test: new scheduler test sets heartbeat.every="365d" with fake timers,
advances 60s, and asserts runSpy was never called (with the bug, it
would be called ~60_000 times).
* style: format heartbeat scheduler clamp
* fix: share safe timeout delay clamp (#71478) (thanks @hclsys)
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Remove the misleading API Keys card from the quick settings page.
The card was hardcoded to a fixed env-var provider list and routed all actions to the broad Environment config section, which made the Add/Change affordances look more precise than they were. This removes the dead surface and keeps the quick settings grid focused on meaningful controls.
Verified:
- pnpm test ui/src/ui/views/config-quick.test.ts
- CI passed on PR #71496
Remove the startup persisted-offset getUpdates preflight so polling restarts do not self-conflict before the grammY runner starts.\n\nFixes #69304.\n\nThanks @chinar-amrutkar.