Newer
Older
navi-1 / docs / push.md

PWA: install, offline shell, web push

The webclient is a full Progressive Web App: it installs as an app on phones/desktops, opens from a service-worker cache when the network is down, and sends web-push notifications when Navi answers while no tab is watching.

Serving routes

navi/main.py (all inside the NAVI_WEBCLIENT_ENABLED gate):

Route Source Cache-Control
/ webclient/dist/index.html no-store
/manifest.webmanifest webclient/dist/manifest.webmanifest no-cache
/sw.js webclient/dist/sw.js no-store (mandatory — see versioning)
/assets/*, /images/* StaticFiles mounts (unchanged) default

There is intentionally no SPA fallback and no dist-root mount — mounting dist/ at / would shadow the API routes.

Service worker (webclient/public/sw.js)

Hand-rolled (~150 lines), no workbox, no build-time precache manifest.

  • Navigation requests — network-first (3 s race) with a cached-shell fallback and a background refresh. Deliberately not cache-first: after a deploy emptyOutDir deletes old hashed assets, and a stale cached shell would 404 on its entry chunks.
  • /assets/* — cache-first: Vite content-hashes filenames, so cached entries are immutable.
  • /images/* — cache-first, capped at 200 entries.
  • Pass-through (never cached): /api/, /ws/, /auth/, /push/, /content/, /content-viewers/, /debug, /admin, non-GET, cross-origin.

Build versioning

vite.config.js ships a tiny inline plugin (stamp-sw-version) that, in closeBundle(), hashes dist/index.html + the hashed dist/assets/* filenames into a 16-hex digest and rewrites __NAVI_BUILD_VERSION__ inside dist/sw.js. Because sw.js bytes change on every build and it is served no-store, the browser re-fetches it after each deploy; the new SW's activate evicts every navi-* cache from previous builds.

Registration is PROD-only (webclient/src/main.js) — dev never registers, so HMR is untouched.

Installability

public/manifest.webmanifest (copied verbatim into dist/): standalone display, theme/background #16161E, icons generated from logo.svg (icon-{192,512}.png regular + icon-maskable-{192,512}.png, committed under webclient/public/images/ — served by the existing /images mount). index.html carries the manifest link, theme-color, and apple-touch-icon.

Web push

Backend: navi/push/ (store.py asyncpg store, service.py fan-out), routes in navi/api/routes/push.py:

Route Purpose
GET /push/vapid-key public VAPID key for pushManager.subscribe (503 when push is not configured)
POST /push/subscribe upsert {endpoint, keys:{p256dh, auth}, user_agent} for the current user
DELETE /push/subscribe remove by endpoint (idempotent)

Subscriptions live in the push_subscriptions postgres table (created at startup next to the auth tables). Dead endpoints (HTTP 404/410 from the push service) are pruned on the next send.

Trigger rule

AgentSessionOrchestrator.run_agent (and run_recall) fire a push when a turn completes (StreamEnd content non-empty) and no WebSocket client is attached to that session — i.e. no tab is actively watching. One turn = one push (multi-tool turns yield a single StreamEnd); a per-session cooldown (NAVI_PUSH_COOLDOWN_SEC, default 30 s) guards overlapping recall + agent runs. Push is fully asynchronous (create_task, sends off-thread) and wrapped in try/except — it can never disturb the run. Sessions without a user (user_id is None) only push in auth-disabled deployments (the anonymous admin); otherwise they are skipped.

Payload: {"title": "Navi ответила", "body": ≤140-char markdown-stripped preview, "url": "/#<session_id>", "session_id": ...}. The SW's notificationclick focuses an existing window and navigates it to /#<session_id> (hash routing opens the right chat), or opens a new one.

Configuration

NAVI_VAPID_PUBLIC_KEY / NAVI_VAPID_PRIVATE_KEY / NAVI_VAPID_SUBJECT / NAVI_PUSH_COOLDOWN_SEC — see config.md. Generate the VAPID pair once with pip install py_vapid && python -m py_vapid gen. Empty keys = push disabled everywhere (client panel shows the toggle as unavailable).

Client side: settings → "Notifications" toggle (webclient/src/components/settings/NotificationSettingsPanel.vue, composable webclient/src/composables/usePush.js).

Offline indicator

useOnline composable + OfflineBanner.vue — an orange banner over the app shell when navigator.onLine goes false. (Server outages are already signalled by the WS reconnecting state.)

Security note

Service workers and push require a secure context (HTTPS or localhost). Navi is expected to sit behind TLS in any deployment where the PWA is used. The httpOnly session cookie rides along automatically on push subscriptions — the SW never touches credentials.