# Rastrillo PWA

Add an installable app manifest, a public offline page and controlled worker
updates to a web app. This optional module has no runtime dependencies.
Web Push composes through [aviso](https://amadan.net/rastrillo/aviso).

The kit does not store pages, API responses, messages, keys or pending writes.
Offline navigation shows a public fallback. Offline reading and editing need
an application data model and synchronisation contract; see
[docs/offline.md](docs/offline.md).

## Try it

From this checkout:

```sh
cd examples/basic
go run .
```

Open `http://127.0.0.1:8080`, reload once after the worker has installed,
then disconnect and reload. The worker returns an offline page with a retry
link. Connect again and retry to return to the app. The sample serves valid
PNG icons as placeholders; replace them with your app's icons.

The sample imports aviso's worker helper and wires notification handlers.
It does not enrol subscriptions or send push messages. Follow aviso's own
skill to provision its server and browser enrolment.

## Add it to an app

Read [SKILL.md](SKILL.md). Install a reviewed version of
`amadan.net/rastrillo/pwa`. Create `pwa.Manifest` with a permanent ID,
name, start URL, scope ending in `/`, and your icons. Call `.Handler()` at
boot and handle its error. Serve the manifest and worker assets without
authentication redirects. Mount the returned handler at
`/manifest.webmanifest`. Mount assets with:

```go
mux.Handle("/pwa/", http.StripPrefix("/pwa", pwa.Assets()))
```

Add the manifest link, theme colour and Apple touch icon to the page head:

```html
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#234d45">
<link rel="apple-touch-icon" href="/static/icon-180.png">
```

Serve your own `/sw.js` with `Content-Type: text/javascript` and
`Cache-Control: no-cache`:

```js
importScripts("/pwa/worker.js");
RastrilloPWA.install();
```

Then register it from your page's JavaScript:

```js
import { register } from "/pwa/client.mjs";
await register({onUpdate: () => { updateNotice.hidden = false; }});
```

Define `updateNotice` in the app. Suggested text: “An update is ready. Save
your work in all tabs, then close and reopen the app.” The helper neither
reloads pages nor automatically activates a waiting worker. Browser support
is detected; registration resolves to `null` without service workers.

`activateUpdate(registration)` explicitly asks a waiting worker to activate
and returns whether there was one. Activation affects all tabs in its scope.
Use it only when the app has resolved unsaved work across those tabs; handle
`controllerchange` in the app if a reload is appropriate. Closing all tabs
allows normal browser activation without this helper.

## Worker contract

`RastrilloPWA.install({offlineHTML})` attaches navigation and update-message
handlers once. `offlineHTML` is optional, trusted build-time public HTML.
The default is a self-contained English page. Provide an app-owned public
translation if needed; neither account details nor keys belong in it.

Only same-origin, in-scope GET navigations are intercepted. A network
failure returns the fallback with status 503 and `Cache-Control: no-store`.
HTTP errors remain unchanged. API calls and mutations are untouched. The
fallback permits inline styles but no scripts, forms or external resources.

The worker and imported scripts are persisted by the browser's worker
installation; the kit never writes Cache Storage or IndexedDB. With no
`clients.claim`, the first page stays uncontrolled until its next navigation.
Removing the worker registration removes the fallback capability.

Use one worker registration per app scope. To add push, import aviso's
helper into this same `sw.js`, attach its push/click/subscription-change
handlers, and pass the same registration to its browser module. Read the
version-pinned aviso skill for its authentication and key-rotation contracts.
Keep notification payloads and encryption in the app.

Production needs HTTPS. Installation UI differs by browser; on iOS guide
the person to add the app to the Home Screen and sign in inside that copy
before enabling push. Registration alone does not prompt installation or
grant notification permission.

## Validation

`make ci` runs Go and JavaScript tests and a real Chromium/WebKit browser
drive. Install the matching Playwright browsers first with
`npx playwright install chromium webkit` after `npm ci`. Missing browsers
fail the gate. The nested example is built and tested by the gate too.

The browser drive covers navigation failures, preservation of HTTP errors,
API failure behaviour, empty caches and updates across two edited tabs.
Chromium uses its offline switch; WebKit uses a dropped network connection
because its automation switch can abort before calling the worker. Device
installation and real iOS push delivery remain manual checks; this gate
does not claim them.

MPL-2.0; see [LICENSE](LICENSE).
