#4353

feat(gui): service worker that shows pushes and routes taps

Openjfcastro92wants to merge intofrom

A service worker so the browser can show pushes while Puter is closed, plus tap handling and local banners in the GUI. Part of PUT-2171. Stacks on #4352. Replaces #4323.

What

Service worker (src/gui/src/sw.js, served at GET /sw.js with Service-Worker-Allowed: / and no-cache)

  • Every push shows a notification inside the push handler. A missing or unreadable payload, or a failed show, falls back to a generic "New notification" with Puter's icon.
  • Payload, documented in the worker as the contract for whatever sends pushes: JSON with the message under a top-level puter key, or under data.puter for providers that only carry string data: { uid, title, body?, icon?, group?, target?: { app?, path? }, data? }, as an object or a JSON string.
  • On tap it posts the tap to an open Puter window and waits for an acknowledgement; without one within 600 ms it opens /?puter.notification=<uid>. It only ever opens or focuses its own origin.
  • Extensions add provider client code with PuterHomepageService.registerServiceWorkerScript(path) (same-origin paths), each loaded in its own try.

pushEnabled. Core doesn't set this gui param. An extension that provides push sets it with setGuiParam('pushEnabled', true). Without one, the GUI doesn't register the worker, and it leaves an existing registration alone: unregistering it would end the browser's subscription for servers that do have push. Turning push off ends the subscription through the push client instead.

GUI

  • Hidden tab: a live, unacknowledged, non-silent notification shows a local banner through the worker, only when permission was already granted and no Puter window is visible.
  • One tap router for toasts, panel rows, worker taps and the URL parameter. The URL parameter is only a notification uid, acted on only after it's found in the user's own list. app.message opens the app on the stored row and sends it the desktop's notificationClick broadcast.
  • App message icons (https) show in toasts and the panel. The notification driver's client rows include app_uid.

Subscription changes. On pushsubscriptionchange the worker posts { type: 'puter-push-subscription-changed' } to every Puter window on its origin, so a push client can register the new subscription while signed in. The worker has no provider code or session, so it only passes the news on. Test: same-origin windows get the message, other origins don't.

Tests

backend (homepage service and controller, notification driver): 111 passed
GUI full suite: 1281 passed

The homepage test now checks that pushEnabled is absent by default and passes through when an extension sets it.

Review follow-ups

  • A tap from the worker is handed over as the notification uid only; the page routes from the stored row for the signed-in user, the same way the ?puter.notification= path does. A row the user can't read opens the panel (dashboard) or nothing (desktop).
  • Local banners for app messages are tagged with the app, so two apps sharing a group name don't replace each other's banner.
  • A tap delivered twice within ten seconds routes once.
  • The worker is registered with updateViaCache: 'none', so imported scripts aren't reused from the HTTP cache on update checks.

Verified on staging (2026-10-26)

In Google Chrome on macOS against a deployment with a push service:

  • Turning push on registered this browser, and the test push showed "Push is on for this browser".
  • With every Puter tab closed, a share notification arrived as a system banner, and tapping it opened a new tab on Files > Shared with the share toast.

Both banners: the test push from Turn on, and the share with every Puter tab closed.

Tapping the share banner opens a new tab on Files > Shared with the share toast.

Not yet checked: the acknowledgement round trip into an already open window, and app.message taps opening an app.

Activity

11
github-actions[bot]4d ago

Coverage Report

StatusCategoryPercentageCovered / Total
🔵Lines95.04%
⬆️ +0.02 pp
39389 / 41442
🔵Statements93.23%
⬆️ +0.03 pp
43268 / 46409
🔵Functions92.6%
⬆️ +0.02 pp
7446 / 8041
🔵Branches84.58%
⬆️ +0.04 pp
30308 / 35831
File Coverage
FileStmtsBranchesFunctionsLinesUncovered Lines
Changed Files
src/backend/controllers/homepage/HomepageController.ts92.75%
⬆️ +1.09 pp
68.75%
⬆️ +1.36 pp
94.11%
⬆️ +0.36 pp
92.98%
⬆️ +1.32 pp
23, 120-124
src/backend/drivers/notification/NotificationDriver.ts96.12%
⬆️ +1.03 pp
91.5%
⬇️ -0.36 pp
100%
🟰 ±0 pp
96.63%
⬆️ +0.85 pp
259-260, 391-393, 409-411, 505
src/backend/services/homepage/PuterHomepageService.ts99%
⬆️ +0.24 pp
97%
⬇️ -0.50 pp
100%
🟰 ±0 pp
98.91%
⬆️ +0.27 pp
501
Generated in workflow #3055 for commit 6f9719f by the Vitest Coverage Report Action