Adds POST /api/notifications/test, which delivers a canned test push to every
registered device through the existing relay path (not persisted to the
notifications list), wired into the router and OpenAPI. In Settings, a "Send test
notification" button appears under the push toggle, only when push is enabled on
this device, so the full backend -> relay -> FCM -> APNs -> device path can be
verified on demand without waiting for a real device event.
Backend, API and widget tests added; clippy and dart analyze clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a GET /api/config endpoint exposing the front-end-facing backend
configuration (currently the notification delivery method, grouped under
a nested "notifications" object so the shape can grow). The settings
screen fetches it on init and only shows the per-device push toggle when
the backend method is "push" (and the platform supports push, which keeps
it off the browser).
Also fold in related push-notifications cleanups: fix the Android app
label ("frontend" -> "OOTT") so the notification permission dialog reads
correctly, remove the completed push_notifications.md plan, and update
TODO.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend (Rust):
- push_tokens migration, model (PushToken/PushPlatform), and db layer
(upsert/list/delete/delete_many)
- PUT/DELETE /api/push_tokens endpoints wired into the router + OpenAPI
- "push" notification method: relay sender (reqwest) that forwards only the
sanitized title/body and prunes dead tokens, plus settings with a default
relay_url
- 164 tests pass, clippy clean
Relay (push_relay/, TypeScript Firebase Cloud Function):
- POST /v1/push (firebase-admin sendEach + per-token status mapping),
GET /healthz, payload validation, per-IP Firestore rate limiting
- Jest tests + README documenting the manual project-owned setup
Frontend (Flutter):
- oott_api_push.dart (register/unregister), push_service.dart behind a
PushService abstraction, and a per-device push toggle in settings
- firebase_core/firebase_messaging/flutter_local_notifications deps
- 145 tests pass, dart analyze clean
Dev shell:
- add nodejs_22 to the Nix dev shell so the relay tests/build run locally
Remaining (manual, project-owned): create the Firebase project, deploy the
relay and set the real default_relay_url, add native Firebase config, and test
on real devices.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend: notifications now show a plain "-" for an absent name, vendor, or
device type (was empty string / "(unknown)" / "Unknown"), via a single
UNKNOWN_PLACEHOLDER constant.
Frontend:
- Empty/unknown values render as an em dash everywhere, centralised in a new
Placeholders.emptyValue constant (replaces inline '—' and '(unknown)').
- Route paths moved to a new Routes class, used by the router and every
navigation call site.
- Device event type modelled as a DeviceEventType enum mirroring the backend
(NewDevice/DeviceSeen) instead of bare string comparisons.
- Hardcoded EdgeInsets/SizedBox spacing replaced with existing Insets tokens.
Tests and formatting updated; all backend and frontend tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The list endpoints now return a total count alongside the page so the
front-end can show how many pages exist and offer a last-page jump.
Backend: add count(is_new) and count_devices(...) (sharing a WHERE-builder
with list_devices so page and count can't drift), wrap both list responses
in {items, total_count} structs, and register them with utoipa.
Front-end: parse the wrapper shape (dropping the fetch-one-extra trick),
add a Last-page button and a responsive "Page X of Y" / "X / Y" label to
the shared PaginationBar, and track the total in both lists. Notifications
re-sync the count on every fetch and decrement it locally on mark-read/
unread removals so the count stays accurate without a re-fetch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a headless `flutter test` suite (83 tests) covering models,
utilities, every API endpoint, and the five screens, with shared helpers
and fixtures so new tests stay terse.
API endpoint methods are statically-dispatched extensions on the
BackendAPI singleton, so they cannot be mocked via `implements`. Mock at
the Dio HTTP-adapter layer (http_mock_adapter) instead, enabled by two
small @visibleForTesting seams: BackendAPI.dioForTesting swaps the
singleton's Dio, and BackendReachability.forceOnlineForTesting() forces a
deterministic online state so polling widgets load.
Add frontend/run_tests.sh and document it in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>