diff --git a/README.md b/README.md index 83fbe8e3..dcaacc0c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ make test-headed # same tests in visible browser Open `http://localhost:10500`. +PDF export (DeepSearch reports, via weasyprint) needs the Pango text stack installed at system level. The production image installs it; on a development host install it once: + +```bash +sudo apt-get install -y libglib2.0-0 libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libfontconfig1 fonts-dejavu-core +``` + ## Stack | Layer | Technology | @@ -40,7 +46,7 @@ devplacepy/ avatar.py # Multiavatar generation, URL builder utils/ # Password hashing, session mgmt, time_ago, notification hook (package) models.py # Pydantic schemas - push.py # Web push crypto, VAPID keys, encrypt/send/register + push/ # Push delivery: provider protocol, Web Push, APNs, registrations routers/ # One file per domain (auth, feed, posts, push, ...) templates/ # Jinja2 HTML templates static/css/ # Page-specific CSS files @@ -811,9 +817,37 @@ calling itself. ## Push notifications & PWA -Authenticated users can receive native web push notifications, and the site is an +Authenticated users can receive native push notifications, and the site is an installable Progressive Web App. Push uses only standard libraries (`cryptography`, -`PyJWT`, `httpx`) against the Web Push Protocol - no third-party push wrapper. +`PyJWT`, `httpx`) against the Web Push Protocol and the Apple Push Notification service - +no third-party push wrapper. + +### Providers + +Delivery is split into providers behind one protocol (`devplacepy/push/providers/`). A user +receives a notification through every provider they hold a live subscription for. + +| Provider | Registration | Transport | +|----------|--------------|-----------| +| `webpush` | `PushSubscription` from the browser `PushManager` (endpoint + `p256dh`/`auth` keys) | Web Push Protocol, VAPID signed, `aesgcm` encrypted payload | +| `apns` | Hexadecimal device token | `POST https://api.push.apple.com/3/device/{token}` over HTTP/2, ES256 provider token | + +`POST /push.json` accepts a registration for any active provider; a body without a +`provider` field is a `webpush` body, so browsers need no change. `GET /push.json` returns +the VAPID public key plus the providers currently accepting registrations. A provider that +is disabled or not fully configured accepts no registrations and is skipped during +delivery, so an unconfigured provider is inert rather than an error. + +Every provider setting is edited at **`/admin/services/push`**: per provider an `Enabled` +toggle, the VAPID subject for `webpush`, and team id, key id, `.p8` auth key (stored as a +masked secret), topic and environment (production or sandbox) for `apns`. The same page +holds the shared delivery timeout and the retention window after which dead subscriptions +are removed. Push delivery does not depend on that service running; stopping it only stops +the pruning sweep. + +Adding a third provider is one file plus one registry entry: the registration route, the +delivery loop, the admin page, the audit record and the metrics are all written against the +provider protocol. ### Events @@ -836,9 +870,11 @@ in real time, bridged onto the in-process pub/sub bus by a lock-owner relay: `create_notification` schedules delivery as a fire-and-forget async task, so a dead subscription or push-service error never blocks the triggering request. Delivery -(`push.notify_user`) iterates a user's subscriptions, encrypts the payload -(legacy `aesgcm` content encoding), and POSTs to each endpoint; subscriptions that -return `404`/`410` are soft-deleted. +(`push.notify_user`) reads a user's subscriptions once, groups them by provider, builds +each provider's payload once, and sends over a single shared HTTP client. A subscription +the push service reports as gone (`404`/`410` for Web Push, `410` or an `Unregistered` +class reason for APNs) is soft-deleted; any other failure is logged and the subscription is +kept. A notification is also **marked read automatically when you open the page that shows its content** - viewing a post clears its comment, reply, upvote and mention notifications; @@ -895,7 +931,10 @@ offline. Installation requires a secure origin (HTTPS, or `localhost` for develo | File | Role | |------|------| -| `devplacepy/push.py` | VAPID keys, payload encryption, send, register | +| `devplacepy/push/providers/` | Provider protocol, Web Push (VAPID keys, payload encryption), APNs | +| `devplacepy/push/store.py` | `push_registration` reads and writes | +| `devplacepy/push/delivery.py` | `notify_user` - group by provider, deliver, reap dead subscriptions | +| `devplacepy/services/push/service.py` | Provider configuration at `/admin/services/push`, retention sweep, metrics | | `devplacepy/routers/push.py` | `/push.json`, `/service-worker.js`, `/manifest.json` | | `static/js/PushManager.js` | Service-worker registration + subscribe + opt-in UI | | `static/service-worker.js` | Receives push, shows notification, offline fallback | diff --git a/events.md b/events.md index fefadf7a..bfcc5ce5 100644 --- a/events.md +++ b/events.md @@ -408,6 +408,8 @@ Every state-changing action in DevPlace records one append-only row through `dev | `push.subscribe` | `routers/push.py` | | `push.update` | `routers/push.py` | +Both carry `metadata.provider` (`webpush`, `apns`, ...) plus `created`; `metadata.endpoint_host` is set for endpoint-based providers and `null` for token-based ones. + ## Gamification (`reward`) | Event key | Recorded in |