Commit Graph
107 Commits
Author SHA1 Message Date
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 52533f447c Make phone device rows cards matching the notifications list
Wrap compact (phone) device rows in a Card instead of a bare
Material+Divider so they get the same rounded, spaced look as the
notifications list. Drop the row divider on narrow layouts and reduce
the phone page size to 5 to account for the taller card rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:27:04 -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 7b8a33606c Remove run_android.sh, fold its boot logic into run_android_emulator.sh
run_android_emulator.sh already boots the emulator and launches the app; it
delegated the boot step to run_android.sh. Inline that logic as a
boot_emulator() helper and drop the now-redundant script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:47:23 -04:00
rzuastiandClaude Opus 4.8 51dc5495ec Fold scroll-to-top into _fetchPage
Replace the _goToPage wrapper with an optional scrollToTop flag on
_fetchPage, so there is a single fetch entry point. The pagination bar
passes scrollToTop: true; all other callers keep the current behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:12:36 -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 fbd9ffd834 Add OOTT-branded native splash for Android and iOS
Replace the vanilla Flutter splash with the OOTT wordmark (Barlow
Condensed Bold) on the Gruvbox-dark background, matching the in-app
AppBar badge. Generated via flutter_native_splash, covering legacy
Android, the Android 12+ SplashScreen API, and iOS (light + dark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:50:45 -04:00
rzuastiandClaude Opus 4.8 c7d948abac Make Gruvbox Dark the default theme
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:38:23 -04:00
rzuastiandClaude Opus 4.8 2817fc7cab Fix Android emulator app launch
Three issues prevented `run_android_emulator.sh` from launching the app:

- gradlew's shebang was a hardcoded Nix-store bash path (copied verbatim
  from the nixpkgs Flutter SDK template) that breaks once that store path
  is garbage-collected, surfacing as a misleading "ProcessException: No
  such file or directory". The dev-shell now normalizes the gitignored
  android/gradlew shebang to the portable "#!/usr/bin/env sh" on entry.
- `flutter run -d android` never matched: -d resolves a device by id/name,
  not platform. Resolve the concrete id (e.g. emulator-5554) from
  `flutter devices --machine` instead.
- Install raced the boot ("device is still booting"); wait for
  sys.boot_completed=1 before launching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:35:46 -04:00
rzuastiandClaude Opus 4.8 080222f121 Rename frontend run scripts and add Android emulator runner
Rename android-emulator.sh to run_android.sh and run.sh to run_web.sh,
add run_android_emulator.sh to launch the app on the emulator, and
update CLAUDE.md commands accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 09:05:44 -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
rzuastiandClaude Opus 4.8 4924146075 Pin Android SDK components and aapt2 override for Flutter builds
The Android SDK lives in the read-only Nix store, so the Gradle build
cannot auto-install missing components. Pin everything the Flutter
Android build needs in the dev shell's composeAndroidPackages:
platforms 33-36 (app targets 36; plugins pin 34 and 35), build-tools
35.0.0 (AGP 8.11.1), NDK 28.2.13676358 (flutter.ndkVersion), and
cmake 3.22.1 (plugin native builds).

Also redirect the Android Gradle Plugin to the Nix-patched aapt2 from
the SDK via android.aapt2FromMavenOverride, written to the user-global
~/.gradle/gradle.properties on shell entry (the project's tracked
gradle.properties must not contain a machine-specific store path).
AGP's Maven-downloaded aapt2 cannot run on NixOS.

Ignore the generated frontend/android/build/ directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 20:49:28 -04:00
rzuastiandClaude Opus 4.8 ada989c11b Add reproducible Android emulator to the dev shell
Pin the Android SDK composition (platform 36 + google_apis x86_64 system
image + emulator) via composeAndroidPackages so the emulator image is
always available and reproducible across machines. Auto-create the
"oott_api36" AVD idempotently on dev-shell entry, and add
frontend/android-emulator.sh to launch it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:23:33 -04:00
rzuastiandClaude Opus 4.8 49544f0a20 Make the status screen scrollable to avoid bottom overflow
The status screen was a non-scrolling Column, so it overflowed
whenever the offline banner consumed vertical space or the viewport
was too short to fit all scanner cards. Wrap it in a
SingleChildScrollView, matching the other screens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:58:25 -04:00
rzuastiandClaude Opus 4.8 4a1fe60ec3 Derive the version from the Cargo/pubspec manifests
The version string was duplicated across six places. Collapse it to two
ecosystem sources of truth and derive the rest:

- backend/src/web_server.rs: omit the OpenAPI info.version so utoipa fills
  it from CARGO_PKG_VERSION (backend/Cargo.toml); add a test pinning this.
- nix/package.nix: read the version from backend/Cargo.toml via fromTOML.
- nix/frontend.nix: read the version from frontend/pubspec.yaml by splitting
  into lines (a whole-file regex triggers catastrophic backtracking in Nix's
  regex engine).
- frontend/lib/about/about.dart: read the version at runtime via
  package_info_plus instead of a hardcoded constant.

Also drop frontend/pubspec.lock.json: it is unreferenced (Nix's
autoPubspecLock generates its own JSON from pubspec.lock) and was going stale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:49:52 -04:00
rzuastiandClaude Opus 4.8 c9e79e028f Split oott_api.dart into per-domain modules
Break the 372-line frontend API client into focused files under
lib/utils/api/: error mapping (api_error.dart), Dio setup behind a
buildDio factory (dio_config.dart), and device/scanner/notification
endpoints as extensions in part files. Collapse the five near-identical
scanner-status methods via a generic _getModel helper and de-duplicate
the pagination logic via a shared _paginate helper.

The BackendAPI singleton and dioErrorToUserMessage remain importable
from oott_api.dart unchanged, so no call sites are affected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:24:37 -04:00
rzuastiandClaude Opus 4.8 72dbbe5e9a Count passive scanner devices seen in the last hour
Replace the lifetime "devices seen since start" counter in the mDNS, SSDP
and DHCP scanners with a rolling count of distinct devices (deduped by
MAC) seen within the last hour.

Each scanner's status now tracks a MAC -> last-seen-time map; the snapshot
prunes entries older than 60 minutes and reports the remaining count. The
API field name (devices_seen) is unchanged, so only its meaning and the
frontend labels ("N devices in the last hour") are updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:57:01 -04:00
rzuastiandClaude Opus 4.8 d15411a889 Report devices seen on last scan in ARP/SNMP status
The ARP and SNMP scanner status endpoints and front-end cards now expose
the number of devices found by the most recent successful scan, following
the existing mDNS device-count pattern. The count persists across the
running/waiting transitions, and for SNMP a failed poll keeps the last
good count rather than overwriting it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:45:45 -04:00
rzuastiandClaude Opus 4.8 2987f66363 Add SNMP scanner that polls a gateway's ARP table
Introduce a sixth scanner that periodically queries an SNMP agent
(typically the router/firewall) for its ARP/neighbour cache via
SNMPv2c and feeds discovered devices into the shared devices, events
and notifications pipeline. Unlike the ARP scanner it generates no
traffic on the local segment and can surface devices across all
subnets the agent routes.

Scope is intentionally minimal: SNMPv2c only, a single target, and the
ipNetToMediaTable (ARP) only. SNMPv3 and switch MAC/forwarding-table
polling are left as follow-ups in TODO.md.

- backend: csnmp dependency; SnmpScanner config (opt-in, off unless a
  [snmp_scanner] section is present); DeviceEventScanner::Snmp;
  scanners/snmp/{finder,scanner,status}; main.rs wiring; status API
  endpoint wired into OpenAPI
- frontend: SNMP scanner status model, card, API method, status screen
  and summary card rows, and device-event label
- docs/config: sample TOML, README options, NixOS module option, and
  setup notes for enabling SNMP on pfSense/OPNsense

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:03:20 -04:00
rzuastiandClaude Opus 4.8 a90bcc360d Document backend connection options and allow iOS local networking
Add NSAllowsLocalNetworking ATS exception so the iOS app can reach a
backend over HTTP on the local network, and document the supported
connection paths (direct private-IP HTTP vs. reverse proxy with HTTPS)
in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:38:43 -04:00
rzuastiandClaude Opus 4.8 abeba88fec Change scanner status label "Not yet started" to "Not started"
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:25:26 -04:00
rzuastiandClaude Opus 4.8 4e975598f9 Add DHCP scanner to frontend status displays
Add a DHCP scanner card to the Status screen and a DHCP row to the
consolidated Status card on the Home screen, mirroring the existing
mDNS/SSDP implementations. Also label DHCP-discovered device events as
"DHCP" instead of the raw "Dhcp".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:45:38 -04:00
rzuastiandClaude Opus 4.8 440959b10d Add SSDP/UPnP scanner to frontend status displays
Surface the SSDP/UPnP scanner alongside ARP and mDNS: a dedicated card on
the Status screen and a row in the consolidated Status card on Home. Mirrors
the existing mDNS implementation and reuses the generic ScannerStatusCard and
PolledValue infrastructure. Also maps the 'Ssdp' device-event scanner to the
"SSDP/UPnP" label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:13:33 -04:00
rzuastiandClaude Sonnet 4.6 29a5b7cf3e Expand notification cards in-place instead of navigating on tap
Tapping a notification card now toggles an expanded view showing the
full body text and M3 action buttons (Mark as read/unread, View device),
replacing the former popup menu and direct navigation on tap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:18:40 -04:00
rzuastiandClaude Opus 4.7 55a6b29d1c Show scanner source in device event tooltip
Surface the originating scanner ("ARP" or "mDNS") in the event timeline
tooltip so users can tell which scanner reported each sighting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 08:19:49 -04:00
rzuastiandClaude Opus 4.7 d94cc2183d Route scanner status colors through the active theme
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 07:55:34 -04:00
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 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
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