Commit Graph
350 Commits
Author SHA1 Message Date
rzuastiandClaude Opus 4.8 8c7e98ae5d Show foreground push notifications on iOS
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>
2026-06-09 14:44:44 -04:00
rzuasti 7241a964ef Release v0.2.1 v0.2.1 2026-06-09 14:11:04 -04:00
rzuasti 6a27f81a4f Updated release notes 2026-06-09 14:10:15 -04:00
rzuastiandClaude Opus 4.8 475edcb2cf Report delivered device count from the test-notification endpoint
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>
2026-06-09 14:06:56 -04:00
rzuastiandClaude Opus 4.8 0075072196 Re-register push token at launch; refactor push_service
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>
2026-06-09 13:56:13 -04:00
rzuastiandClaude Opus 4.8 817ec5872c Add a "Send test notification" button to verify push delivery
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>
2026-06-09 12:42:20 -04:00
rzuastiandClaude Opus 4.8 c7c5b45ae8 Surface native iOS APNs registration outcome for diagnostics
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>
2026-06-09 12:06:11 -04:00
rzuastiandClaude Opus 4.8 3d190e847e Initialize Firebase at startup so iOS APNs token is captured
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>
2026-06-09 11:51:28 -04:00
rzuastiandClaude Opus 4.8 7d7f0197cd Wait for iOS APNs token before requesting FCM token
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>
2026-06-09 11:19:46 -04:00
rzuasti 5fbee67a24 Release v0.2.0 v0.2.0 2026-06-09 10:50:02 -04:00
rzuasti 4192626690 Updated release notes for 0.2.0 2026-06-09 10:47:49 -04:00
rzuastiandClaude Opus 4.8 122624baf1 Bump frontend and backend dependencies within constraints
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>
2026-06-09 10:30:07 -04:00
rzuastiandClaude Opus 4.8 67b94b89a1 Move backend config to a dialog; group settings into cards
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>
2026-06-08 18:15:10 -04:00
rzuastiandClaude Opus 4.8 1d85ec6f83 Gate push toggle on backend notification method
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>
2026-06-08 17:26:53 -04:00
rzuastiandClaude Opus 4.8 bef1a2987d TODO: note Android push permission dialog shows the placeholder app label
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 15:01:08 -04:00
rzuastiandClaude Opus 4.8 44ec383744 Wire app-side Firebase push notifications
- 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>
2026-06-08 14:46:55 -04:00
rzuastiandClaude Opus 4.8 d81eba3b37 Deploy push relay: dev-shell tooling, real relay URL, /health route
- 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>
2026-06-08 14:00:09 -04:00
rzuastiandClaude Opus 4.8 4f3fe10332 Implement push notifications Phase 1 (FCM + project relay)
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>
2026-06-08 12:28:03 -04:00
rzuastiandClaude Opus 4.8 fa54361198 Refine push notifications plan: privacy, paths, and naming
- 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>
2026-06-08 11:33:33 -04:00
rzuastiandClaude Opus 4.8 ed04a9bc89 Scale duration formatting up to months
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>
2026-06-08 11:15:20 -04:00
rzuastiandClaude Opus 4.8 dedfe1c838 Fix ghosting in native screen transitions
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>
2026-06-07 18:29:57 -04:00
rzuastiandClaude Opus 4.8 80f0f54785 Serve front-end assets with Cache-Control: no-cache
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>
2026-06-07 18:15:52 -04:00
rzuastiandClaude Opus 4.8 a12ff62fbd Document webhook prerequisite in codemagic.yaml
Cutting v0.1.3 pushed the tag but no build started: the repo had no
webhook delivering events to Codemagic, so the tag trigger never fired.
Note that the triggering block alone is not enough and explain the two
ways to wire delivery (GitHub App or a manual repo webhook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:36:35 -04:00
rzuasti 4cf8759888 Release v0.1.3 v0.1.3 2026-06-07 16:14:03 -04:00
rzuasti 645cd2c543 Updated release notes 2026-06-07 16:13:02 -04:00
rzuastiandClaude Opus 4.8 f3d845ce37 Add device identification guide dialog for unregistered devices
Adds a "How to identify this device" button next to Register Device that
opens a dialog with personalized clues (vendor/type) and steps to track
down the physical device on the network.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:10:50 -04:00
rzuastiandClaude Opus 4.8 3d7c4a1323 Shrink normal device-seen dots in event history chart
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:49:44 -04:00
rzuastiandClaude Opus 4.8 4a19ab34e2 Crossfade between top-level tabs instead of iOS slide
Switching between the top-level destinations went through the default
CupertinoPage slide, which is meant for forward pushes. For a peer
tab-switch it slid the incoming screen in over the outgoing one without
animating the old screen away, leaving it visible in the background.

Give the top-level routes a CustomTransitionPage crossfade; keep the
device-detail route on the default slide since it is a genuine drill-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:44:48 -04:00
rzuastiandClaude Opus 4.8 55eb878590 Reword empty new-notifications message
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:37:30 -04:00
rzuastiandClaude Opus 4.8 d4e00515fc Remove Notifications title from home page body
Match the structure of other pages by dropping the standalone title and
placing the filter selector and "mark all as read" action in one row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:36:05 -04:00
rzuasti 9099d0f6a7 Updated todo 2026-06-07 15:33:21 -04:00
rzuastiandClaude Opus 4.8 40a7f3a432 Request iOS local-network permission at app launch
The iOS local-network permission prompt fires on the first local-network
access. That used to be the user's "Test" tap in settings, so the prompt
appeared mid-request and the first connection attempt always failed.

Provoke the prompt at launch instead via an mDNS multicast datagram, so the
permission is settled before the user reaches settings. Platform exclusion is
handled with a conditional import: web gets a no-op stub (keeping dart:io out
of web builds) and the native path no-ops off iOS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:33:06 -04:00
rzuastiandClaude Opus 4.8 87af782985 Use the OOTT brand icon as the app launcher icon on all platforms
The app shipped Flutter's default icon on every platform. Add
flutter_launcher_icons (mirroring the existing flutter_native_splash
setup) with an OOTT wordmark on the brand orange (#fe8019) and
regenerate the native icon sets for iOS, Android, macOS, Windows and
web.

Android uses an adaptive icon (orange background + inset transparent
foreground) so the mark survives any launcher mask; the square
full-bleed source is used elsewhere where the platform applies its own
corner mask. Source images live under assets/icon/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:24:38 -04:00
rzuasti 032923cdae Updated todo 2026-06-07 12:40:48 -04:00
rzuasti af3faa2786 Updated todo 2026-06-07 12:40:31 -04:00
rzuastiandClaude Opus 4.8 a81e510cde Note the TestFlight build trigger in release.sh output
Pushing a vX.Y.Z tag kicks off the Codemagic iOS build automatically;
surface that in the release output so the operator knows it's running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:40:18 -04:00
rzuastiandClaude Opus 4.8 5a2872e602 Declare app as exempt from export encryption rules
The app's only encryption is HTTPS/TLS (accessed via iOS) plus trivial
local XOR obfuscation, so set ITSAppUsesNonExemptEncryption to false.
This is the correct export-compliance classification and stops App Store
Connect from prompting on every upload.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:36:05 -04:00
rzuastiandClaude Opus 4.8 a685d9abfa Use a persistent signing key for iOS code signing
Codemagic build VMs are ephemeral, so fetch-signing-files without a
persistent private key creates a distribution certificate that can't be
reused on later builds ("Cannot save Signing Certificates without
certificate private key"). Pass a CERTIFICATE_PRIVATE_KEY secret so the
certificate is created/reused with a key Codemagic owns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:58:50 -04:00
rzuastiandClaude Opus 4.8 59872bde1f Point CI at the "Codemagic" App Store Connect integration
Match the App Store Connect API key name registered in the Codemagic UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:48:38 -04:00
rzuastiandClaude Opus 4.8 8dee23d18c Make TestFlight build number safe for the first build
get-latest-testflight-build-number errors when the app has no builds
yet, which broke the first CI run. Fall back to 0 so the initial build
number is 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:43:22 -04:00
rzuastiandClaude Opus 4.8 d485f4b335 Add iOS TestFlight CI via Codemagic
Set up automated cloud iOS builds and TestFlight distribution without a
local Mac:
- Add codemagic.yaml: iOS->TestFlight workflow with automatic signing,
  build-number auto-increment, triggered by the vX.Y.Z release tags.
- Set the iOS bundle identifier to net.oott-security.app (was the
  placeholder com.example.frontend).
- Set the iOS display name to OOTT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:31:53 -04:00
rzuasti f547cf1279 Release v0.1.2 v0.1.2 2026-06-07 09:17:09 -04:00
rzuasti b1302a1eec Added release notes for 0.1.2 2026-06-07 09:16:11 -04:00
rzuastiandClaude Opus 4.8 09e7915547 Separate events and notifications into focused modules
The events.rs file mixed three domains: device-event recording, change
detection, and the entire notification pipeline (rendering + delivery +
sending). This made it long, gave functions side effects beyond their
stated goal (classify_* silently recorded events), and intertwined the
events and notifications logic.

Split along domain boundaries:
- model::device_events now owns DeviceChange, the shared contract.
- events records device events only (record_new_device/record_known_device);
  events/detection.rs holds pure change detection.
- new notifications module owns rendering, delivery, and sending
  (notifications.rs + delivery.rs + render.rs); pushover/error moved here.

Data now flows one way: events produces DeviceChange, notifications
consumes it, both depend only on model. Scanners/pipeline/main orchestrate.
classify_* renamed to record_* so the write is the stated goal; send and
send_notification collapsed into persist_and_deliver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 09:10:49 -04:00
rzuastiandClaude Opus 4.8 9efe84c099 Add DeviceChanged and DeviceBackOnline event types
Record what happened on each known-device sighting instead of only
"seen": a baseline DeviceSeen (history heartbeat, no notification) plus
DeviceChanged and DeviceBackOnline events, each deduplicated
independently so a recent routine sighting no longer suppresses a
genuine change or return notification. The frontend chart now trusts the
event type for its marker and tooltip rather than comparing each event's
snapshot against the device's current state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:56 -04:00
rzuastiandClaude Opus 4.8 27cb2e1d62 Apply event deduplication window to notifications and refine its key
Gate notification persistence and delivery on the device-event
deduplication window, not just the device_events table: record_event now
reports whether it recorded, and classify_new_device/classify_existing_device
suppress their changes when the sighting is a duplicate.

Treat an empty->value IP fill as not a change via a new ip_changed helper
(mirroring vendor_changed), so a device gaining its first address raises no
"changed" notification.

Key deduplication on (mac, scanner, event_type) instead of (mac, ip,
scanner), so repeated sightings of the same kind collapse regardless of the
reported address.

Also delete stale oott.db-wal/-shm in run_tests.sh to avoid init_db panics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:17:24 -04:00
rzuasti 2b808a0147 Updated todo 2026-06-06 16:51:01 -04:00
rzuastiandClaude Opus 4.8 b0222445fb Fix API docs link when app is served from the backend
The API docs nav entry uses the origin-relative path "/api/docs", which
Uri.parse left scheme- and host-less, so canLaunchUrl/launchUrl could not
open it (e.g. in Docker). Resolve external links against the current page
so they carry a scheme and host; absolute URLs pass through unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:50:43 -04:00
rzuasti 37075294e3 Release v0.1.1 v0.1.1 2026-06-06 16:31:59 -04:00
rzuastiandClaude Opus 4.8 a69d1aa91a Add v0.1.1 release notes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:29:48 -04:00