Initial commit
This commit is contained in:
commit
38c24fc9dc
166 files changed
+22002
No files matched your search
@@ -0,0 +1,129 @@
|
||||
# 05 — PWA & Web Push Design
|
||||
|
||||
## Manifest (`manifest.webmanifest`, generated via `vite-plugin-pwa`)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Notizarr",
|
||||
"short_name": "Notizarr",
|
||||
"description": "Self-hosted notification hub",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0b0d12",
|
||||
"theme_color": "#0b0d12",
|
||||
"orientation": "portrait",
|
||||
"categories": ["utilities"],
|
||||
"icons": [
|
||||
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`start_url` carries `?source=pwa` purely so the client can distinguish a
|
||||
standalone launch from a browser tab for analytics/diagnostics — not used
|
||||
for auth or routing logic.
|
||||
|
||||
## Service worker (Workbox via `vite-plugin-pwa`, `injectManifest` strategy — we need custom push/notificationclick handlers, so `generateSW` alone isn't enough)
|
||||
|
||||
- **Precache:** app shell (JS/CSS bundles, `index.html`, icon set) for
|
||||
offline load of the UI shell.
|
||||
- **Runtime caching:** `NetworkFirst` for `GET /api/v1/*` with a short
|
||||
(e.g. 1 min) fallback cache, so a flaky connection still shows the last
|
||||
known notification list instead of a blank screen. Mutating requests are
|
||||
never cached and always network-only.
|
||||
- **`push` event:** payload is small JSON:
|
||||
```json
|
||||
{ "notificationId": "...", "topicSlug": "homeassistant", "title": "...", "body": "...", "icon": "/icons/icon-192.png" }
|
||||
```
|
||||
Handler calls `self.registration.showNotification(title, { body, icon,
|
||||
badge, data: { deepLink: "/topics/homeassistant/messages/<id>" }, tag:
|
||||
notificationId })`. `tag` collapses rapid duplicate pushes for the same
|
||||
notification into one OS notification.
|
||||
- **`notificationclick` event:** `event.notification.close()`, then either
|
||||
focus an existing client and `postMessage({ type: "navigate", path })`, or
|
||||
`clients.openWindow(deepLink)` if none is open.
|
||||
|
||||
Payload is deliberately minimal (Web Push payloads are capped around 4 KB by
|
||||
most push services) — the client fetches full notification detail
|
||||
(`GET /api/v1/notifications/:id`) once it navigates, rather than trying to
|
||||
cram the full message into the push payload. This also avoids storing
|
||||
potentially sensitive message content in the push service's transit path
|
||||
longer than necessary.
|
||||
|
||||
## VAPID
|
||||
|
||||
Generated once on first boot if `system_settings.vapid_keys` is absent
|
||||
(using the `web-push` library's `generateVAPIDKeys()`), stored in Postgres —
|
||||
durable via the existing `postgres/` volume, covered automatically by
|
||||
`pg_dump` backups. `VAPID_SUBJECT` (a `mailto:` or `https:` contact) comes
|
||||
from an env var. The public key is exposed via
|
||||
`GET /api/v1/push/vapid-public-key` for the client to use with
|
||||
`PushManager.subscribe({ applicationServerKey })`.
|
||||
|
||||
## Subscription flow
|
||||
|
||||
1. User logs in (session cookie set).
|
||||
2. Client checks `navigator.serviceWorker` support and registers the worker
|
||||
on app load (idempotent).
|
||||
3. User taps "Enable notifications" (never auto-prompted on load — iOS
|
||||
Safari requires this to be a direct user gesture, and prompting
|
||||
unprompted is bad UX anyway).
|
||||
4. `Notification.requestPermission()` → if `granted`, fetch the VAPID public
|
||||
key, call `registration.pushManager.subscribe({ userVisibleOnly: true,
|
||||
applicationServerKey })`.
|
||||
5. `POST /api/v1/push/subscribe` with the resulting `PushSubscription` JSON
|
||||
→ stored as a `push_subscriptions` row tied to the current user.
|
||||
6. User can toggle push per topic from Topic Settings (`PATCH
|
||||
/api/v1/topics/:slug/subscription`).
|
||||
7–10. As described in `01-architecture.md`'s publish flow: new notification
|
||||
→ stored always → push sent if the topic's per-user setting is on → tapping
|
||||
the OS notification opens the deep link in the installed PWA (or a browser
|
||||
tab, if not installed).
|
||||
|
||||
## iOS / Safari specifics
|
||||
|
||||
- Web Push on iOS requires **iOS 16.4+** and the PWA must be **installed to
|
||||
the Home Screen** first — Safari does not support Push for ordinary
|
||||
browser tabs. This is documented prominently in the in-app onboarding and
|
||||
in the diagnostics screen (see below), since it's the single biggest
|
||||
source of "why isn't it working" confusion.
|
||||
- `<meta name="apple-mobile-web-app-capable" content="yes">`,
|
||||
`<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">`,
|
||||
`<link rel="apple-touch-icon" ...>` for the home-screen icon.
|
||||
- `<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`
|
||||
plus CSS `env(safe-area-inset-*)` padding on the app shell, bottom nav,
|
||||
and any fixed headers, so content clears the notch/Dynamic Island and the
|
||||
home indicator.
|
||||
- Standalone detection: `window.matchMedia('(display-mode: standalone)').matches
|
||||
|| window.navigator.standalone === true`.
|
||||
|
||||
## Diagnostics screen (`/settings/diagnostics`)
|
||||
|
||||
Client-derived:
|
||||
- PWA installed (standalone display-mode detected): yes/no
|
||||
- Service worker: registered / active / not supported
|
||||
- Notification permission: `Notification.permission` (granted/denied/default)
|
||||
- Push subscription: `pushManager.getSubscription()` present/absent
|
||||
|
||||
Server-derived (`GET /api/v1/push/diagnostics`), scoped to the current
|
||||
user's devices:
|
||||
- Push endpoint status per device (active / disabled)
|
||||
- Last successful delivery attempt (timestamp) per device
|
||||
- Last failed delivery attempt (timestamp + sanitized reason) per device
|
||||
- VAPID configuration status (configured / missing — admin-only detail also
|
||||
surfaced on `/admin/push`)
|
||||
|
||||
The screen explicitly states what Web Push **cannot** guarantee (delivery
|
||||
confirmation of on-screen display, iOS version/PWA-install prerequisites)
|
||||
rather than implying certainty it can't back up.
|
||||
|
||||
## Deep-linking contract
|
||||
|
||||
Every push notification's `deepLink` is always
|
||||
`${PUBLIC_BASE_URL}/topics/{topicSlug}/messages/{notificationId}`. The SPA
|
||||
router resolves this route regardless of entry point (cold start from a
|
||||
terminated PWA, warm start, or plain browser tab), fetching the notification
|
||||
detail and marking it read on open.
|
||||
Reference in new issue
Block a user