Commit Graph
28 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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
rzuastiandClaude Opus 4.8 f0d939f685 Add API Docs link to the wide navigation rail
Add an "API Docs" navigation entry that opens /api/docs in a new tab.
It is shown only in the wide-mode navigation rail (not the compact
bottom bar) and sits just before About.

Generalises the navigation destination model so an entry can be an
in-app route or an external link, and can be restricted to wide layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:20:29 -04:00
rzuastiandClaude Opus 4.8 a51b9c06dc Display unknown values as a dash and centralise repeated literals
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>
2026-06-06 16:07:05 -04:00
rzuastiandClaude Opus 4.8 42310a36e9 Consolidate active-scan notifications and scrub private data
Active scanners (ARP, SNMP) now accumulate every change across a whole
scan and emit one notification per type via events::notify: a single
device produces the usual single-device notification (carrying its MAC),
while two or more produce one consolidated summary with an empty
mac_address. Device events are still recorded per device.

Notification bodies no longer include MAC or IP addresses; the title
MAC fallback is masked to the last two octets. Summaries list up to
three devices then "…and N more devices".

Split sighting handling so record_sighting persists + records the event
and returns Vec<DeviceChange>; passive listeners (mDNS, SSDP, DHCP) use
record_and_notify since they see one device per event.

Also fixes NotificationType::from_str never mapping "DeviceChanged",
which made those notifications round-trip from the DB as Other.

Frontend: the card already hides the device link when mac_address is
null; added widget tests for the present/absent link cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 11:31:13 -04:00
rzuastiandClaude Opus 4.8 6eaaff457d Add collapsible navigation rail in wide layout
Let users collapse the wide-mode NavigationRail to an icons-only compact
view to reclaim horizontal space. A bottom-pinned double-chevron toggle
sits centred when compact and slides to the rail's right side when
extended, animating in sync with the rail. The choice is persisted via
the nav_rail_extended preference (defaults to extended).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 10:56:25 -04:00
rzuasti 48d73fb207 Consolidate frontend scanner, pagination, and tick-timer duplication
Collapse the five scanner status models into two shared shapes,
ActiveScannerStatus and PassiveScannerStatus, mirroring the backend's
active/passive vocabulary. Replace the five near-identical per-scanner
detail card files with a single scanner_status_cards.dart (two shared
resolvers plus a config list), and rebuild the combined home card to
iterate a list of scanners with two shape resolvers instead of five
copy-pasted resolve methods.

Extract two reusable mixins:
- PeriodicRebuild: the shared once-a-second "rebuild to refresh elapsed
  text" timer used by the scanner cards and the stale indicator.
- PaginatedListState: the shared pagination state, page-size/page-count
  getters, cancel-token-aware fetch orchestration, and disposal used by
  the device and notification lists.

No behaviour change; ~900 lines removed. Tests and analyzer pass.
2026-06-05 21:57:58 -04:00
rzuastiandClaude Opus 4.8 844d7d189c Add "go to last page" and page count to notifications and devices lists
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>
2026-06-05 21:09:25 -04:00
rzuastiandClaude Opus 4.8 3cb104e364 Add sort-by-device-type to the devices list
Wide layout makes the leading device-type icon column header tappable to
sort; narrow layout gains a "Device Type" option in the sort sheet. The
backend already whitelisted device_type as a sort column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:37:38 -04:00
rzuastiandClaude Opus 4.8 b3a4e4cb0b Collapse chart time-range pills to a dropdown on phones
The device detail chart's time-range selector (Today / Last week / ...)
rendered as a SegmentedButton that overflowed on narrow phone layouts.
Reuse the responsive FilterSelector widget so the same control is used
for both the list filters and the chart: segmented pills on wide layouts,
a compact dropdown combo box on phones.

To keep the two controls consistent, FilterSelector now renders a
SegmentedButton (instead of ChoiceChips) on wide layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 19:29:38 -04:00
rzuastiandClaude Opus 4.8 f847b60c21 Animate notification list inserts, removals and arrivals
Replace the notifications list's wholesale redraw with a SliverAnimatedList
driven by a GlobalKey, keeping `_items` in lockstep with the animated state.

- Background refreshes (poll, pull-to-refresh, resume, route pop, mark-all)
  reconcile against the fetched page: departed rows slide out, newly fetched
  rows slide in at the top with a theme-coloured arrival highlight, and
  surviving rows stay put (with in-place read-state recolouring under "All").
- Filter/page changes and the initial load reset the list (fresh key) so the
  new dataset appears instantly without per-row animation.
- Read/unread removals are owned by the list: buttons play a slide/fade exit,
  while swipes let Dismissible animate and then reconcile, avoiding double
  animation and the disposed-widget race.

Add the arrival highlight overlay to NotificationCard and cover the new
behaviour with widget tests (swipe-out, flash-in, external removal, in-place
"All" mark, filter reset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:35:18 -04:00
rzuastiandClaude Opus 4.8 c949deedb6 Show a page-level progress bar while paginating lists
Changing pages in the notifications or devices list gave no cue that
the next page was loading. Render an indeterminate progress bar at the
shell level, pinned flush against the bottom of the page body (above
the nav bar on phones, the screen bottom on wide layouts), driven by a
shared paginationLoading notifier the lists set while fetching. The
pagination bar keeps disabling its buttons during the fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:45:08 -04:00
rzuastiandClaude Opus 4.8 f64b4d3559 Collapse list filters to a dropdown on phones
On narrow layouts the devices list's filter chips (Not registered /
Registered / All) competed for horizontal space with the Sort and Filter
icon buttons and overlapped. Introduce a reusable FilterSelector that
keeps the chips on wide layouts but collapses to a compact dropdown
button on phones, and use it for both the devices and notifications
lists for consistency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:22:01 -04:00
rzuastiandClaude Opus 4.8 0a899b7890 Shrink notification and device list page sizes on phones
On phone-width layouts (< Breakpoints.medium) the notification and device
lists now request fewer items per page so the list and its pagination bar
fit on screen together on common current phones. Notifications use 4 items
and devices 6 on phones; wider layouts keep 5 and 10 respectively. The
initial fetch is deferred to didChangeDependencies so the page size can read
the screen width from MediaQuery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:13:37 -04:00
rzuastiandClaude Opus 4.8 8b8fa5ce89 Add pull-to-refresh to the notifications list
The devices list already supported pull-to-refresh; mirror that on the
notifications list by wrapping its CustomScrollView in a RefreshIndicator
with AlwaysScrollableScrollPhysics. Keep the existing list visible during
a refresh (_isLoading = _items.isEmpty) instead of flashing the skeleton,
matching the devices list behaviour.

Add widget tests covering pull-to-refresh for both lists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:02:07 -04:00
rzuastiandClaude Opus 4.8 aa5f65de2b Scroll to top of list when changing pages
The notifications and devices lists kept their scroll offset when paging,
so a new page would open partway down. Attach a ScrollController to each
CustomScrollView and animate back to the top whenever the page changes
via the pagination bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:10:13 -04:00
rzuastiandClaude Opus 4.8 aa5a29271b Keep notification cards collapsed when a sibling is removed
SliverList recycles State objects by index, so when an expanded
notification was marked as read and dropped from the filtered list, the
next notification inherited the expanded state. Give each card a
ValueKey(id) so its expand/collapse state is matched by notification,
not by list position.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:02:57 -04:00
rzuastiandClaude Opus 4.8 2a6d480428 Collapse notification cards on tap anywhere
Tapping a notification's body already toggled expand/collapse, but the
action area below it wasn't tappable. Wrap the whole card in an InkWell
so tapping anywhere (body or action row) toggles, while the buttons
still handle their own taps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:57:20 -04:00
rzuastiandClaude Opus 4.8 9367c359d7 Polish frontend: design tokens, theming, AppBar titles, empty/loading states
Frontend usability and aesthetics pass across web and mobile:

- Add shared layout tokens (theme/dimens.dart: Insets + Breakpoints) and
  replace magic-number spacing and per-file breakpoint consts.
- Route both themes through buildAppTheme (theme/theme_builder.dart): explicit
  useMaterial3 and a shared branded textTheme (Barlow Condensed for
  display/headline/title styles); logo now reads its style from the theme.
- Move screen titles into the shared AppBar (route-derived) and drop the
  redundant in-body headers; upgrade Settings buttons to M3 FilledButton.
- Add reusable EmptyState and skeleton loaders; Devices empty state links to
  scanner status, notifications get filter-aware messages.
- Add a first-run welcome intro and Save-disabled helper text in Settings.
- Make device Filter/Sort adaptive: bottom sheet on phones, dialog on wide.
- Collapse the device-list filter chips and Sort/Filter buttons into one row;
  rename the home Scanners card title and align its style.

Tests: add EmptyState/skeleton widget tests and Settings first-run cases;
update tests for the FilledButton swap and relocated titles. 89 passing,
dart analyze clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:39:37 -04:00
rzuastiandClaude Opus 4.8 98b5fa267f Add automated test suite for the Flutter frontend
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>
2026-06-03 19:41:52 -04:00