Commit Graph
214 Commits
Author SHA1 Message Date
rzuastiandClaude Opus 4.7 376eb3c367 Use grey dot when ARP scanner is waiting for next run
Amber implied a problem when the scanner is simply idle between runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:46:51 -04:00
rzuastiandClaude Opus 4.7 408585c056 Keep status pills inline and show tooltips only on overflow
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:42:43 -04:00
rzuastiandClaude Opus 4.7 0beca44496 Sort and look up text columns case-insensitively
Add COLLATE NOCASE on every text content column (names, owners, vendors,
device types, MAC addresses, notification titles/bodies/types, event
types) so DESC-by-name lists no longer order "iPad" before "Lutron".
Normalize MAC addresses to lowercase at the DB layer so the now
case-insensitive primary key cannot accept duplicate-looking rows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:33:37 -04:00
rzuastiandClaude Opus 4.7 0304c50efb Move home screen route from /notifications to /
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:17:41 -04:00
rzuastiandClaude Opus 4.7 6303951b3d Show online/offline status dot beside device last-seen
Highlights devices seen within the last 10 minutes with a green dot so
recent activity is scannable at a glance; older devices get a muted
outline-colored dot with an "Appears offline" tooltip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:15:30 -04:00
rzuastiandClaude Opus 4.7 17712c25f1 Collapse scanner-card and pagination duplication into shared widgets
Extract ScannerStatusCard<T> and PaginationBar so the ARP/mDNS cards and
the two paginated lists share one implementation each. Also fold the
four UISnackbars methods over a severity enum and fix Notification.toJson
serializing a method tear-off instead of the enum name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:09:24 -04:00
rzuastiandClaude Sonnet 4.6 9b521e0847 Stop interpolating raw exception into settings snackbar
Defense-in-depth: the persisted value is the (XOR-obfuscated) API key,
so a future SharedPreferences backend that surfaced the value in its
exception could leak it through the UI. Log the detail via debugPrint
and show a fixed message to the user instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 19:00:38 -04:00
rzuastiandClaude Sonnet 4.6 140ebab035 Catch uncaught async errors and surface response-shape failures
Wrap main in runZonedGuarded and install FlutterError.onError /
PlatformDispatcher.onError so background async failures get logged
instead of dying silently. Guard PrefUtil.init so a SharedPreferences
platform error no longer aborts startup. Recognize TypeError and
FormatException in dioErrorToUserMessage to give a clearer message
when the backend returns an unexpected JSON shape or bad timestamp.
Wrap Settings._save in try/catch so prefs write failures show a
snackbar instead of leaving the Save button silently dead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:55:35 -04:00
rzuastiandClaude Sonnet 4.6 e70676a193 Surface persistence and fetch errors instead of swallowing them
PrefUtil.setValue now returns the underlying SharedPreferences result
and throws on unsupported types; settings save reports failures rather
than always showing success. Device detail and event history report
the real Dio error message and skip cancellations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:43:46 -04:00
rzuastiandClaude Sonnet 4.6 f7f255e20f Compare parsed /test response instead of stringified Dio dump
Previously BackendAPI.test() called response.toString().contains('OOTT_API_OK'),
which scans the full stringified Response (status line, headers, body, request
path). A reverse proxy or captive portal returning HTML that happens to echo the
token would falsely pass the connectivity check. Compare response.data — the
JSON-decoded String the backend's /api/test handler returns — against the
expected value with exact equality instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:32:40 -04:00
rzuastiandClaude Opus 4.7 8e9e221048 Omit Authorization header when API key is empty
Send no Authorization header at all on a fresh install (rather than
"Bearer " with an empty token), so the backend can distinguish "no
credentials supplied" from "credentials supplied but invalid".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 18:26:00 -04:00
rzuastiandClaude Sonnet 4.6 b5da90be59 Pause polling and show a banner when the backend is unreachable
Adds a centralized BackendReachability singleton fed by a Dio
interceptor (connection-level failures only) and connectivity_plus.
PolledValue subscribes to it and pauses on offline / reloads
immediately on reconnect, replacing the previous per-card error
cascade. MainShell renders a global OfflineBanner with Retry and
Settings actions; cards downgrade error→stale when offline so the
banner is the only red element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 18:21:55 -04:00
rzuastiandClaude Sonnet 4.6 8be65f3ffa Fold pagination +1 trick into API methods, return typed records
Remove dead _pageSize from BackendAPI and centralise the
"fetch perPage+1, detect hasNextPage, trim" logic into
listDevices and listNotifications. Both methods now accept
page/perPage and return ({items, hasNextPage}) records,
eliminating the duplicated boilerplate at each call site.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 17:40:18 -04:00
rzuastiandClaude Sonnet 4.6 f75d1ce313 Pause polling when screens are off-route or app is backgrounded
PolledValue gains pause()/resume() methods. ScannersStatusCard and
NotificationsList now implement RouteAware (didPushNext/didPopNext)
and WidgetsBindingObserver to stop API calls and tick timers when
the widget is not visible, resuming with an immediate fetch on return.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 17:34:19 -04:00
rzuastiandClaude Sonnet 4.6 838b292456 Add dio_smart_retry for automatic GET request retries
Transient network hiccups and temporary backend errors (5xx, timeouts)
now retry transparently up to twice (1 s then 3 s backoff) before
surfacing an error to the user. Non-idempotent methods (POST, PUT,
DELETE), cancellations, and non-retryable status codes are excluded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 17:19:00 -04:00
rzuasti 7437b511ed Updated todo 2026-05-31 10:28:34 -04:00
rzuastiandClaude Opus 4.7 61f2579152 Make notification mark-as-read reliably update the list
Three intertwined fixes so that marking a notification as read from the
trailing menu, swipe-dismiss, and 'Mark all as read' all consistently
update the UI and surface backend failures.

- Guard the mark-read, mark-new, and mark-all calls with try/catch; on
  failure show a snackbar with the mapped error and leave the list
  untouched.
- Use the State's own `mounted` and `context` after the await instead of
  the per-card BuildContext, so the SliverList rebuild reliably runs
  even when the originating NotificationCard's element has been swapped
  out by the reconciliation. _setRead and _markAllAsRead no longer take
  a BuildContext.
- Replace `Dismissible(key: UniqueKey())` with `ValueKey(item.id)` in
  the notification card. UniqueKey was regenerated on every build,
  forcing the Dismissible/PopupMenuButton subtree to be disposed on any
  parent rebuild; the open menu's `mounted` check would then drop
  `onSelected` silently.
- Add `ValueKey(device.macAddress)` to the device list rows for the
  same reason, anchoring each row's Element (and its PopupMenuButton
  State) to its device identity instead of its list position.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 10:28:23 -04:00
rzuastiandClaude Opus 4.7 bb677aeebf Centralize status polling with last-known-good, stale, and error tiers
The four polling status cards (ARP, mDNS, scanners summary, devices
summary) each rolled their own Timer/_status/_error/_isLoading state and
flipped to a red 'Error' UI on the first failed poll, throwing away
last-known-good data that was still in memory. A momentary backend blip
made every card flash red.

Introduce PolledValue<T>, a ChangeNotifier that owns the periodic timer,
in-flight CancelToken, last value, last error, and a freshness
classification (initialLoading / fresh / stale / error). 'stale' kicks in
on the first failure but keeps the value visible; 'error' only takes over
after staleErrorAfter elapses without a success (30s for the 5s scanner
polls, 3min for the 1min device-summary poll). All four cards now branch
on freshness and show a small live-updating warning icon with a tooltip
('Last updated N ago — <error>') while stale, instead of flipping red.

Also threads CancelToken? through the three polled GET endpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 10:03:19 -04:00
rzuastiandClaude Opus 4.7 d7276c8383 Replace per-method debugPrints with a debug-only Dio LogInterceptor
Every BackendAPI method had hand-rolled 'About to call ...' and
'Received: ...' debug lines, and the constructor printed the base URL and
a masked API-key marker. Replace all of that with a single LogInterceptor
attached under `if (kDebugMode)` to both the singleton Dio and the
ephemeral Dio used by BackendAPI.test(). Headers and bodies are kept off
so the Authorization bearer never lands in the console. The interceptor
is tree-shaken from release builds.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:39:06 -04:00
rzuastiandClaude Opus 4.7 30df22df56 Cancel in-flight paginated fetches to prevent stale-write races
Rapidly switching filter chips, sort columns, or pagination on the devices
and notifications lists could overlap fetches, and an older response
landing after a newer one would silently overwrite the list with results
that no longer match the active query.

Thread an optional CancelToken into listDevices and listNotifications.
Each State now keeps one active token, cancels it on every new fetch and
on dispose, and bails from both success and catch branches when its token
has been superseded. DioExceptionType.cancel is silenced so internal
cancellations don't surface as user-visible errors. The notifications
list's prior `if (_isLoading) return;` guard is removed so the 1-min
periodic refresh is no longer skipped during user activity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:30:03 -04:00
rzuastiandClaude Opus 4.7 d3b6c64e8b Apply Settings changes to the live BackendAPI without restart
Settings._save wrote new base_url/api_key to prefs but the BackendAPI
singleton kept the Dio it built at first access, so URL/key changes only
took effect after an app restart. Expose reconfigureFromPrefs() on
BackendAPI and call it from Settings._save so subsequent requests pick up
the new config immediately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:17:15 -04:00
rzuastiandClaude Opus 4.7 7954a9c755 Add Dio timeouts and map backend errors to user-friendly messages
Set 5s/15s/15s connect/receive/send timeouts so calls fail fast instead of
hanging. Introduce dioErrorToUserMessage() and route DioException-throwing
call sites (device actions, devices list, notifications list, scanners
status card) through it, replacing raw e.toString() output. Notifications
list now surfaces load errors inline in the theme's error color, matching
the devices list pattern. Status dots show the mapped detail as a tooltip.
Stop logging the API key in cleartext.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:06:56 -04:00
rzuastiandClaude Opus 4.7 0f6296759d Add edit dialog for registered devices
Extend PUT /api/devices/{mac} to accept an optional name and surface an
Edit action from both the device detail screen and the per-row overflow
menu, letting users modify owner, device type, vendor and name without
forgetting and re-registering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:27:52 -04:00
rzuastiandClaude Opus 4.7 3a579f0321 Redesign devices list as a sortable, responsive headered list
GET /api/devices accepts sort_by/sort_order (whitelisted columns; default
last_seen DESC, stable secondary sort on mac_address), device registration
captures an optional hostname, and the Flutter list switches between a
wide headered layout and a compact ListTile under 600px. Per-row overflow
menu retains View details / Register / Forget actions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:09:55 -04:00
rzuasti 29f836f281 Updated CLAUDE.md to avoid aliases in imports in Rust 2026-05-30 10:00:13 -04:00
rzuastiandClaude Opus 4.7 d60e8a7e2e Address clippy warnings in test code
Apply clippy auto-fixes (filter().next() → find(), bool assert_eq →
assert!, len() >= 1 → !is_empty(), map_or(false, …) → is_some_and, etc.)
and refactor validate_device to take an expected Device instead of 8
positional args. Production code was already clean; all 60 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:59:44 -04:00
rzuastiandClaude Opus 4.7 f9ad90d015 Drop redundant use ... as ... aliases in scanner imports
The aliases re-encoded the module path into a name that no longer
matched the source identifier (e.g. `use super::finder as mdns`). Use
the actual module names at call sites; in the two web_server handlers
the local `status` fn collides with the module, so the single call site
uses the fully-qualified path instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:52:40 -04:00
rzuastiandClaude Opus 4.7 c12e700c27 Consolidate ARP/mDNS modules under scanners::{arp,mdns}::{finder,scanner,status}
Group each discovery protocol's primitive (finder), orchestration loop
(scanner), and status state under one module tree instead of splitting
them between device_finders/ and crate-root *_scanner / *_scanner_status
files. Also includes incidental rustfmt fixes to a few pre-existing
long lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:48:29 -04:00
rzuastiandClaude Opus 4.7 54211c57d7 Split db::devices update into seen/update and expose PUT endpoint
Scanners now call seen() (sighting semantics: stamp last_seen, preserve
registration, conditional vendor/device_type/name writes). update() is
the user-editable mutation (owner, device_type, vendor) wired to
PUT /api/devices/{mac_address}.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:27:44 -04:00
rzuasti 792a309136 Updated todo 2026-05-30 07:23:08 -04:00
rzuasti f02c9284d5 Updated todo 2026-05-30 07:21:57 -04:00
rzuastiandClaude Opus 4.7 926a902377 Refresh device and notification lists on return navigation
Subscribes the devices list and notifications list to a shared
RouteObserver so they refetch when a pushed route (e.g. device detail)
is popped, avoiding stale state after registering or updating a device.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:13:00 -04:00
rzuastiandClaude Opus 4.7 eb91804c97 Preserve existing device_type on scanner re-sighting
Why: a device manually registered as "Laptop" was being overwritten to
"Phone" when a later scan deduced a vendor that maps to a different type.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:06:21 -04:00
rzuastiandClaude Opus 4.7 2ba51769ec Show registered devices by owner's device type name
Display registered devices with a "John's Laptop" title and move their
IP address into the card body, instead of showing the owner separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:24:40 -04:00
rzuastiandClaude Opus 4.7 ef420ad360 Do not notify when a device's vendor is first deduced
Treat an empty->non-empty vendor transition as not a change, so first
deducing a vendor for a device that previously had none no longer raises
a "vendor changed" notification.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:20:20 -04:00
rzuastiandClaude Opus 4.7 561fbdb548 Split device registration out of the update path
Add dedicated db::devices::register/unregister methods so registration
state (is_registered, owner) is owned by the API endpoints, and make
update sighting-only. This stops scanner re-sightings from wiping a
device's registration. Also preserve an existing mDNS hostname instead
of overwriting it on re-sighting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:15:29 -04:00
rzuastiandClaude Opus 4.7 98a23aa7fd Deduce vendor from mDNS services for privacy MACs
Modern devices increasingly use randomized ("Private WiFi Address") MACs,
which are locally administered and have no real OUI, so the MAC-prefix
vendor lookup returns nothing. Such devices still advertise distinctive
mDNS service types, which we now use to deduce their vendor.

- Extend the mDNS parser to also capture PTR service types alongside
  A-record hostnames.
- Add a service-type -> vendor mapping (data/mdns-service-vendor.json) and
  a service_vendor_finder, using canonical vendor names so device-type
  resolution still chains.
- Add utils::network::is_locally_administered to detect randomized MACs.
- In the mDNS scanner, fall back to service-based deduction only when the
  OUI lookup is empty and the MAC is locally administered.
- Group the three finders under a new data module.
- Preserve a known vendor (and its derived device_type) when a later
  sighting cannot deduce one, and suppress the spurious "vendor changed"
  notification in that case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:56:39 -04:00
rzuastiandClaude Opus 4.7 7fb457ca4a Make device detail IP and info fields selectable
Use SelectableText for the IP address and all info-card values so users
can select and copy them (e.g. the MAC address).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:56:16 -04:00
rzuastiandClaude Opus 4.7 251c06277e Add pagination to the devices list screen
Mirror the notifications list paging pattern (offset/limit, fetch one
extra row to detect the next page, First/Prev/Next controls) across the
devices DB query, REST endpoint, API client and UI. Also add a First
page button to the notifications pagination row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:07:27 -04:00
rzuastiandClaude Sonnet 4.6 75f597bfe4 Add mDNS scanner status to Status and Home screens
Add an mDNS Scanner status card to the Status screen mirroring the ARP
card, and replace the ARP card on the Home screen with a combined Status
card that summarizes both scanners in one line each. Extract the shared
duration formatting helper into utils/duration_formatter.dart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 10:19:50 -04:00
rzuastiandClaude Sonnet 4.6 c6fdf9e181 Centralize networking helpers in utils::network module
Move select_interface and MAC-resolution logic (resolve_mac_address,
parse_proc_net_arp, probe_mac) out of the device_finders modules into a
single utils::network utility so future scanners can reuse them. Rename
resolve to resolve_mac_address and make parse_proc_net_arp private.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 09:58:45 -04:00
rzuastiandClaude Opus 4.7 ca83009944 Add passive mDNS/Bonjour discovery scanner
Introduce a second discovery module that passively listens for mDNS
multicast announcements (224.0.0.251:5353) and feeds discovered devices
into the existing devices/events/notifications pipeline, running in its
own task alongside the ARP scanner.

Since mDNS carries an IP and hostname but no MAC (the device key), the
module resolves IP->MAC via the OS ARP cache with a targeted ARP-probe
fallback. The advertised hostname is stored in a new optional `name`
column on devices (blank for ARP-only devices); the single `update`
writes `name` only when set so ARP rescans never clobber it. Device
names are included in event/notification messages.

Adds a GET /api/mdns_scanner/status endpoint (is_listening, devices_seen,
last_device_seen_seconds_ago) wired into OpenAPI, an optional
[mdns_scanner] config section, and the corresponding Nix module option.
Also aligns the Nix module's stale `timings` section with the current
`arp_scanner` schema.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 09:37:33 -04:00
rzuastiandClaude Sonnet 4.6 2fe850c070 Auto-focus owner field and submit on Enter in register device dialog
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 08:29:50 -04:00
rzuastiandClaude Sonnet 4.6 1da0306b61 Replace device detail popup menu with inline CTA buttons
Register and Forget Device actions are now always-visible buttons
(FilledButton and error-styled OutlinedButton) placed inline below
the device info card, following M3 guidelines for in-content actions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 08:25:11 -04:00
rzuastiandClaude Sonnet 4.6 39f382fd0d Extract NotificationsList widget from HomeScreen into its own file
HomeScreen is now a StatelessWidget responsible only for the responsive
layout structure. All notification state, pagination, filtering, and
mark-as-read logic lives in the new NotificationsList widget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 08:06:30 -04:00
rzuastiandClaude Sonnet 4.6 85b86026cb Replace infinite-scroll notifications with button-based pagination
- Swap PagingController/PagedSliverList for explicit page state and
  Previous/Next icon buttons below the list
- Fetch pageSize+1 items to detect the last page without a total-count
  API; pagination controls are hidden when there is only one page
- Remove unused infinite_scroll_pagination dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:56:03 -04:00
rzuastiandClaude Sonnet 4.6 2077f970f9 Update README to reflect optional networking.interface config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:36:31 -04:00
rzuastiandClaude Sonnet 4.6 08f2dc8340 Auto-detect network interface when not configured
If no interface is set in the config, the ARP scanner now picks the
first non-loopback, up, IPv4-enabled interface automatically. The
setting is optional in the TOML config, sample config, and Nix module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 07:35:25 -04:00
rzuasti 49adadf059 Updated todo 2026-05-28 21:18:20 -04:00
rzuastiandClaude Sonnet 4.6 e5eb9c777d Updated todo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 20:03:23 -04:00