From 71cb181152f6352e1c084d807a2ac1e3db545e49 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 20 Jul 2026 15:27:18 -0500 Subject: [PATCH] =?UTF-8?q?docs(android):=20M7=20Part=202=20landed=20?= =?UTF-8?q?=E2=80=94=20embedded=20ntfy=20distributor=20+=20push.ntfyUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flip M7 Part 2 to landed (Android-app#15) and record the two implementation decisions: the direct-ntfy embedded distributor (no UnifiedPush library — the plan's stated likely path; foreground-service SSE, no second app, no Google Play Services), and the small additive push.ntfyUrl settings field the app needs to discover the relay (website#79). Document push.ntfyUrl + NTFY_PUBLIC_URL in BACKEND_DESIGN.md. Co-Authored-By: Claude --- android/PLAN.md | 46 +++++++++++++++++++++++++++++++++------ website/BACKEND_DESIGN.md | 9 +++++++- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/android/PLAN.md b/android/PLAN.md index 05ed8ea..c6f3305 100644 --- a/android/PLAN.md +++ b/android/PLAN.md @@ -1,6 +1,6 @@ # Android App — Plan -Status: **M0–M6 landed; M7 (push notifications) Part 1 — backend + docs — landed (website#78 merged 2026-07-20). Remaining: cut the v1 tag, then M7 Part 2 (the app's UnifiedPush integration — planned below).** This document is the +Status: **M0–M7 landed; M7 (push notifications) both parts done — Part 1 backend (website#78) and Part 2 app (Android-app#15) plus a small `push.ntfyUrl` settings addition (website#79). Remaining: set the shard's `NTFY_*` deploy config so push lights up, and cut the v1 tag.** This document is the design contract for the `RunicGateway/Android-app` repo. It was written before implementation so the API changes it depends on could be landed in `website/` and `docs/` first. The authoritative API reference is the committed OpenAPI spec at `website/server/swagger/swagger-output.json` (regenerated @@ -173,7 +173,37 @@ none (keeps §11's zero-interaction promise). reverse proxy; internal-only for the publisher), anonymous read-write to unguessable topics (no per-user accounts — safe because tickles are content-free). -**Part 2 — the Android app (next pass; planned here).** UnifiedPush receiver + device registration +**Part 2 — the Android app — ✅ LANDED** (2026-07-20, `RunicGateway/Android-app#15` + a small +`RunicGateway/website#79` settings addition + this docs PR). Built exactly to the plan below, with +two recorded implementation decisions: +- **Direct-ntfy embedded distributor, no UnifiedPush library (deviation from §2's "UnifiedPush + connector" wording — the plan's stated likely path, work item 1).** The app talks straight to ntfy + over its own topic rather than pulling in `org.unifiedpush.android:connector` + an external + distributor: a foreground `PushService` holds an OkHttp-SSE connection to `//sse` + (reusing the M2 `ShardStreamClient` reconnect pattern) on a **bare** client, `PushManager` + orchestrates topic mint / device register / start-stop keyed to the session, and `PushNotifier` + posts a per-stream notification whose tap deep-links via `MainActivity` intent extras. No new Gradle + dependency; a `PushResult`/transport seam keeps the future FCM Play flavor cheap. Reasons: the + UnifiedPush distributor model assumes a *separate* app (exactly what the user vetoed), we already own + the SSE machinery, and this keeps the APK Google-free and dependency-light. New code lives in + `core/push/` + `ui/notifications/` + a `NotificationsApi`/`NotificationsRepository`; no existing + screen's data flow changed. +- **One small additive backend field was required after all (`push.ntfyUrl`).** The embedded + distributor must know the shard's client-facing ntfy URL to build its topic endpoint, and Part 1 + never surfaced it (the `NTFY_*` vars are server-only). So `/public/settings` now carries + `push: { ntfyUrl }` (from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS`; never the internal + `NTFY_BASE_URL`), null when unconfigured → the app shows push as unavailable for that shard. This is + the "no backend work in Part 2" caveat corrected: it is additive, non-sensitive, and forward-compatible + (an older backend omitting it just decodes to null). **Deploy dependency stands:** push only delivers + once the shard sets `NTFY_PUBLIC_URL`/`NTFY_ALLOWED_ORIGINS` (§13). + +Verified green: `:app:testDebugUnitTest` (18 new JVM tests — notifications DTO decode, ntfy tickle +parse incl. malformed, topic/URL building, stream→route map + personal gating) + `:app:lintDebug` + +`:app:assembleDebug`; backend 250 tests (+3 for `push.ntfyUrl`) and `npm run swagger` clean. The +foreground-service tradeoff (§11) and the POST_NOTIFICATIONS runtime permission are implemented as +planned; an on-device delivery pass against a live ntfy is the one open QA item. + +**Part 2 (original plan) — the Android app.** UnifiedPush receiver + device registration against the merged Part-1 contract, a Notifications settings screen, and notification-tap deep-links. The app is architected for push from M0 (§11), so this is **additive** — a new feature slice (`core/push` + `ui/notifications` + a `DevicesApi`/`NotificationsApi` pair) that touches no existing @@ -283,7 +313,7 @@ hands the app an endpoint on *that* origin and the backend rejects a register wh allow-listed. This is deployment config, not code, but Part 2 can't be end-to-end tested until it's pinned. No `website`/`link`/`servuo-plugins` code change is expected in Part 2. -Ships as `RunicGateway/Android-app#`; bumps `versionCode`/`versionName` for a post-v1 release +Ships as `RunicGateway/Android-app#15`; bumps `versionCode`/`versionName` for a post-v1 release (§10). Like M1–M4 it records itself in the §9 build-progress block on landing. **Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset @@ -666,10 +696,12 @@ push, and Play (M6–M8) follow the designed app. `website/docker-compose.yml` (declarative, zero-interaction config), UnifiedPush integration in the app, device registration, the subscriptions UI, and the content-free-tickle backend fan-out (see §11). The app is built with room for this from M0 but it does not gate the first release. - ✅ **Part 1 (backend + docs) landed** 2026-07-20 (`RunicGateway/website#78` merged + docs#20) — see - the "M7 plan" build-progress block above and §8 item 3. **Part 2 (the app: UnifiedPush receiver, - device registration, subscriptions UI, notification-tap deep-links) is planned in the "M7 plan" - block and is next.** + ✅ **Both parts landed** 2026-07-20 — Part 1 backend (`RunicGateway/website#78` merged + docs#20), + Part 2 app (`RunicGateway/Android-app#15`) + a small `push.ntfyUrl` settings addition + (`RunicGateway/website#79`). The app embeds its own ntfy distributor (a foreground-service SSE + connection, no second app, no Google Play Services, no UnifiedPush library); see the "M7 plan" + Part 2 block for the recorded transport + backend-field decisions. Push delivers once the shard sets + its `NTFY_*` deploy config (§13). 9. **M8 — Google Play**: Play Console listing, signing/upload key, and (optionally) an FCM build flavor — after the direct-APK release is stable. diff --git a/website/BACKEND_DESIGN.md b/website/BACKEND_DESIGN.md index e36a65b..ce9aba2 100644 --- a/website/BACKEND_DESIGN.md +++ b/website/BACKEND_DESIGN.md @@ -241,7 +241,7 @@ create/publish-post path for `news.post`. The stream catalog + event→stream ma ### /public (public.routes.js → public.controller.js) — all GET, no auth | Method | Path | Notes | |---|---|---| -| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, and the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard. Asset fields may be site-relative paths (resolve against the base URL). | +| GET | `/settings` | whitelisted public keys, derived `registration`/`gameAccountSignup` flags, the per-shard **`brand`** block (name, `accent` color, logo/hero/favicon) a client themes itself from — one image runs as any shard, asset fields may be site-relative paths (resolve against the base URL) — and a **`push`** block `{ ntfyUrl }` (M7): the client-facing ntfy relay URL the app's embedded distributor registers its device topic against, from `NTFY_PUBLIC_URL` / first `NTFY_ALLOWED_ORIGINS` (never the internal `NTFY_BASE_URL`); `null` when push isn't configured for the shard. | | GET | `/status` | status message + current mode, **plus a `version` block** (`{ service:'runic-gateway', api, server }`) so a client first-run probe recognizes the backend and can run a version-mismatch guard | | GET | `/version` | lightweight, **DB-free** backend identity/version (`{ service, api, server }`) — the canonical target for the version guard and a cheap liveness check | | GET | `/posts/:category` | published only; `category` ∈ news\|five-on-friday\|newsletter\|screenshots | @@ -381,6 +381,13 @@ CLIENT_ORIGIN=http://localhost:5173 # Push (M7): the ntfy relay URL — also the backend's SSRF allow-set for device # endpoints. NTFY_ALLOWED_ORIGINS / NTFY_PUBLISH_TOKEN are optional. NTFY_BASE_URL=https://ntfy.example.com +# The client-facing ntfy URL surfaced to the app via /public/settings.push.ntfyUrl +# (the app registers its topic endpoint here). Defaults to the first +# NTFY_ALLOWED_ORIGINS entry; set explicitly when the public URL differs from the +# internal NTFY_BASE_URL. Without it (and without NTFY_ALLOWED_ORIGINS) the app +# shows push as unavailable for the shard. +NTFY_PUBLIC_URL=https://ntfy.example.com +NTFY_ALLOWED_ORIGINS=https://ntfy.example.com ``` `.gitignore`: `node_modules/`, `.env`, `_reference/`, `client/dist/`, `uploads/`.