- Add screenshots to README "What is OOTT?" section
- Document the "push" notification method and notifications.push.relay_url
- Fix misleading retention duration example (m = minutes, not months)
- Add web_server.api_key to the Nix service example
- Make sample_oott.toml a complete, functional baseline: every option
present, mutually-exclusive notification provider sections commented out
- Trim completed items from TODO.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
512x512 app icon (downscaled from the icon master) and a 1024x500
feature graphic (logo on the Catppuccin Mocha base background), generated
for the Google Play store listing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
navigation.dart imports package:flutter/cupertino.dart for
CupertinoPageTransition, which made the asset bundler warn that the
CupertinoIcons font was expected but missing. Declaring cupertino_icons
provides the font and clears the warning; it tree-shakes away in release
builds since no Cupertino icons are actually rendered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Release builds were signed with the debug key, which Play rejects. Load
an upload keystore from android/key.properties (gitignored) and use it
for the release signing config, falling back to the debug key when the
file is absent so `flutter run --release` still works without a keystore.
Add key.properties.example documenting the keytool setup and a
build_android_release.sh helper that builds the signed AAB and refuses
to run when key.properties is missing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
record_sighting only carried the stored name and IP onto the reconciled
sighting, leaving is_registered, owner, and device_type at their bare
defaults. The change handed to the notification layer therefore rendered a
registered, typed device as "Not registered" with type "-". Carry those
fields over too, mirroring how db::devices::seen() preserves them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the standalone welcome card from the Settings screen and show the
greeting inside the non-dismissible first-run configuration dialog,
noting that OOTT cannot function without a backend installed in the
network.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The AlertDialog actions were stacked onto two lines on narrow phones
(e.g. iOS) because the default OverflowBar wraps once the buttons'
combined width exceeds the dialog width. Lay them out in a single Row
wrapped in a FittedBox(scaleDown) so they stay on one line, scaling
down uniformly on too-narrow screens rather than wrapping or overflowing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The action button row used a fixed Spacer that couldn't shrink, causing a
horizontal overflow on phone widths. Keep the spread-out Row on wide screens
and fall back to a Wrap on narrow ones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add five new selectable themes alongside the existing Catppuccin Mocha
and Gruvbox Dark, each built through the shared buildAppTheme so they
stay visually consistent. Palettes are taken from each project's
official source; a few intermediate surface shades are interpolated
where the canonical palette omits them (noted in the theme files).
Register the new themes in main.dart and add their dropdown entries in
settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scale the adaptive foreground content 145% so the dark wordmark reads
larger on the home screen while staying within the adaptive safe zone.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Backend:
- Add db::devices::delete to erase a device and its events atomically
- Expose DELETE /api/devices/{mac}/permanently, wired to OpenAPI
- Cover the new db method and endpoint with tests
Frontend:
- Delete action for not-registered devices (detail screen + list row)
and an opt-in "permanently delete" checkbox in the Forget dialog
- Navigate to the devices list after deleting from the detail screen
- Refine button emphasis to M3: single filled primary, error-colored
text buttons for destructive actions, Test demoted to filled-tonal
- Flash the backend-config Test button red on a failed connection test
- Render snackbars through a top-level ScaffoldMessenger host so they
show above dialogs; keep the built-in SnackBar (with an Overlay host)
Docs:
- CLAUDE.md: rustfmt edition 2024, don't revert formatter-only changes,
prefer built-in Flutter components, follow existing patterns + M3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mobile apps move to a one-time paid purchase on the App Store and
Google Play instead of a subscription. Document this in the README and
make clear the source stays fully open under the AGPL-3.0 for anyone who
prefers to build the apps themselves.
Add a Trademark section (and a matching note above the AGPL text in
LICENSE) reserving the "OOTT" name and logo, so third-party builds must
rebrand. The AGPL body itself is left verbatim and unmodified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iOS suppresses an incoming push banner while the app is foregrounded
unless the app opts in. _ensureForegroundDisplay now calls
setForegroundNotificationPresentationOptions on iOS so the OS presents
the push directly, avoiding a double notification / delegate clash with
flutter_local_notifications. Android keeps the onMessage local-notification
path, which it still requires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/notifications/test previously returned a blanket 200 even when there
were no registered devices, so a test that reached nobody looked like a success.
push::send now returns the number of devices the relay confirmed delivery to, and
the endpoint returns it as {"delivered": N}. Settings shows "sent to N device(s)"
on success and an explicit "No devices are registered..." warning when N is 0,
which is the case that previously masqueraded as success.
Backend, API and widget tests updated; clippy and dart analyze clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a cold restart the app considered push enabled (from prefs) but never
re-registered its FCM token, so a rotated token (or a backend that lost its token
store) left the backend delivering to nothing while the test button still
reported success. Re-register the current token at launch so the backend
converges to the live token on every start, and re-attach the foreground display
handler then too (it was previously only wired during enable()).
Also tidy push_service.dart: a single initPushOnLaunch() startup entry point so
main.dart needs no push internals, one owner for the persisted push-enabled flag
(pushEnabledOnThisDevice / setPushEnabledOnThisDevice) instead of a hard-coded key
in four places, private internal helpers, deduped platform checks, and section
grouping. enable() now reuses the shared token-registration path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Enabling push still fails on iOS with getAPNSToken() returning null, so capture
the APNs registration result natively to find out why. AppDelegate now overrides
didRegister/didFailToRegister, logs the outcome, and exposes it over a
oott/push_diagnostics method channel (super still calls through so Firebase
swizzling is unaffected).
apnsRegistrationStatus() reads that channel, and the settings push toggle now
shows Apple's actual rejection reason in the error message when enabling fails,
so it can be diagnosed without a Mac to read the device console.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firebase was only initialized lazily when the user toggled push on. On iOS the
firebase_messaging plugin installs its APNs swizzling at app launch, but the
delivered device token can only be forwarded to FCM if a FirebaseApp is already
configured at that point. With lazy init there was none, so getAPNSToken() never
resolved and enabling push failed with "Could not enable push".
Add initFirebaseForPush() (mobile-only, idempotent) and call it from main() at
launch. Extract a shared pushSupportedOnThisPlatform getter so isSupported and
the initializer stay in sync; _ensureFirebase() now delegates to it as a safety
net.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On iOS, FirebaseMessaging.getToken() throws apns-token-not-set when called
before Apple has asynchronously delivered the APNs token, which happens after
the permission prompt. This surfaced as "Failed to update push settings" with
the toggle stuck off. Poll getAPNSToken() with a short bounded wait first, and
return false (toggle stays off) rather than throwing if it never arrives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tier-1 dependency refresh (lockfiles only, no manifest/version-range
changes):
- frontend/pubspec.lock: dio, go_router, shared_preferences and others
to their latest in-range versions.
- backend/Cargo.lock: tokio 1.49->1.52.3, clap 4.5->4.6, tower-http,
serde_json and a batch of transitive crates.
All frontend (152) and backend (165) tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings screen now splits responsibilities:
- Backend URL + API key move into a Test/Save popup dialog
(backend_config_dialog.dart), reachable via a "Re-configure" action.
Per M3 the Test button is left-aligned (neutral) with Cancel/Save
grouped trailing. On first run the dialog auto-opens non-dismissible.
- The screen shows the connection read-only (URL plain, key masked with
reveal) in a "Backend connection" card, and groups Theme + Push into an
"App settings" card. Theme and push apply immediately, no Save button.
- Align all card titles to titleLarge across home/status/settings.
Adds a test seam (dioBuilderForTesting) so reconfigure keeps the mock Dio
in widget tests instead of issuing real requests.
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>
- Rename Android package com.example.frontend -> net.oottsecurity.app
(namespace, applicationId, MainActivity), and enable core library
desugaring required by flutter_local_notifications
- Configure Firebase from a committed lib/firebase_options.dart (client
identifiers, not secrets) instead of native config files; initialize the
push service with explicit options
- iOS: add Runner.entitlements (aps-environment) wired via CODE_SIGN_ENTITLEMENTS
for the Runner target, and UIBackgroundModes remote-notification in Info.plist
- gitignore the unused native Firebase config files defensively
dart analyze clean; 145 frontend tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- flake.nix: add nodejs_22, firebase-tools, and google-cloud-sdk to the dev
shell so the relay can be tested, built, deployed, and administered locally
- backend settings: point default_relay_url at the deployed relay
- rename the relay liveness route /healthz -> /health: Google Front End
reserves /healthz and returns its own 404 before the request reaches Cloud
Run, so the probe was unreachable (verified live; /v1/push and the FCM path
work end-to-end)
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>
- Drop all private data from push payloads: send only the already-sanitized
title/body, no data field, no MAC/IP, and no deep-link (tap just opens the app)
- Correct backend integration points: sender at src/notifications/push.rs and a
"push" arm in deliver() (not events.rs); add module registrations and an HTTP
client (reqwest) dependency
- Unify nomenclature on "push": method value "push", relay under push_relay/,
config section [notifications.push], sender module push.rs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
formatSeconds previously capped at minutes; extend it to dynamically
show the two largest relevant units up through months for the passive
scanners' "Listening for ..." line and other duration displays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Routed screens are transparent fragments rendered into the shell's single
Scaffold. A screen pushed on top (e.g. device detail) was therefore
see-through, leaving the screen beneath visible as it slid in, and the fade
page ignored secondaryAnimation so the covered screen sat frozen instead of
parallaxing out.
- Paint each routed page opaque (theme surface) so pushes cover cleanly.
- Drill into detail screens with a CupertinoPageTransition slide; the covered
page parallaxes out via the same route animation, so both move in lockstep
on the native iOS curve. Tabs keep their crossfade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without an explicit directive ServeDir let browsers heuristically cache
the Flutter bundle, and the service worker kept serving stale assets, so
new icons/images did not appear after an upgrade (even on hard reload).
Force revalidation so users always get the latest assets; unchanged files
still return a cheap 304.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>