Merge pull request 'docs: M7 Part 2 landed — embedded ntfy distributor + push.ntfyUrl' (#22) from docs/android-m7-part2-landed into main

Reviewed-on: #22
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
2026-07-20 20:29:43 +00:00
2 changed files with 47 additions and 8 deletions

View File

@@ -1,6 +1,6 @@
# Android App — Plan
Status: **M0M6 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: **M0M7 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 `<ntfy>/<topic>/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#<pr>`; bumps `versionCode`/`versionName` for a post-v1 release
Ships as `RunicGateway/Android-app#15`; bumps `versionCode`/`versionName` for a post-v1 release
(§10). Like M1M4 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 (M6M8) 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.

View File

@@ -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/`.