Reorganize into What is OOTT?, Getting started (Docker + mobile apps),
Deeper dive (Nix flake, config options, HTTPS), and Things to keep in
mind (storage, privileges/ports). Inline a minimal oott.toml and the
compose file in the Docker quickstart, and document the missing
web_server.* options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a repo-root release.sh that prompts for the version, bumps it in
Cargo.toml/pubspec.yaml and the About release date, refreshes lockfiles,
runs tests, commits/pushes, tags, creates the GitHub release, and builds
the Docker image, pausing for confirmation between sections.
Release notes live in RELEASE_NOTES.md (newest first, one section per
version); the script extracts the section for the version being cut.
Seeds the file with the v0.1.0 notes and adds gh to the Nix dev shell.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
When the same scanner sees the same device (same MAC and IPv4) again within
a configurable window (default 1 minute), only one device_events row is
recorded, keeping the events table from filling with near-identical rows.
Device last_seen updates and notifications are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The five scanners (ARP, SNMP, mDNS, SSDP, DHCP) duplicated their
persist-and-notify pipeline, device enrichment, and status state
machines across same-family files. Extract the shared logic so a change
lands in one place instead of three to five.
- scanners/common/pipeline.rs: single record_sighting() persist+notify
path, replacing the per-scanner match blocks. ARP/SNMP now use the
same merge rules as the passive scanners (keep a stored hostname,
never overwrite a known IP with an empty one).
- scanners/common/enrichment.rs: build_device() for vendor/device-type
lookup with the privacy-MAC service fallback.
- scanners/common/{active,passive}_status.rs: the two status state
machines plus their tests, written once. Each scanner status.rs is now
a thin wrapper over its own static.
- utils/network::format_mac(): replaces three identical copies.
- web_server/scanner_status.rs: Active/Passive response types and two
handler helpers, replacing five near-identical structs+handlers. JSON
field names are unchanged so the frontend is unaffected; only OpenAPI
schema names change.
27 files changed, ~900 lines net removed. Build, clippy and all 113
tests (4 new) pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
Add an ArpScanner Default impl (30m/1m/10m, enabled) and mark the
arp_scanner field with serde default, so the [arp_scanner] section can
now be omitted entirely and fall back to code defaults — matching the
pattern used by the SNMP scanner. Previously these three durations were
mandatory and the backend would not start without them.
Reconcile the documentation to the canonical 30m/1m/10m: fix the README
NixOS example (was 15m/20m/30m) and options table (was 15m), and note
that the section is optional in both the README and sample TOML. The
NixOS module and sample TOML already used these values.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A typical router keeps active devices' ARP entries fresh continuously,
so a 10-minute poll interval stays well within common ARP cache TTLs
while keeping device-event churn modest (each poll records a DeviceSeen
event per device). A 5s per-request timeout adds margin for a busy
agent or large ARP table at no cost on the happy path.
Updates the default in settings.rs and the sample TOML, README and
NixOS module. TODO timing-review item narrowed to the ARP scanner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Build the Flutter web app at release time (nix/frontend.nix) and bundle
it next to the binary at $out/share/oott/web; resolve it at runtime
relative to the executable. Remove the prebuilt backend/web from git.
- Add web_server.{ip_address,port,api_key} options to the NixOS module so
the generated config deserializes (was missing, causing a startup panic).
- Docker image: set SSL_CERT_FILE for outbound TLS, drop the heavy
nixos/nix base image, trim contents to [oott cacert], and ensure /tmp
exists.
- Remove the unused "nix" flake input and commit flake.lock.
- Provide Swagger UI to utoipa-swagger-ui offline via a pinned fetchurl so
the package builds in the Nix sandbox; skip the redundant check phase
(tests run via backend/run_tests.sh).
- sample_oott.toml: set database.path to /db/oott.db for the Docker image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Each scanner (ARP, mDNS, SSDP/UPnP, DHCP) can now be turned off via an
`enabled` flag in its config section, defaulting to true so existing
deployments are unchanged. A disabled scanner's entry function returns
early and never starts.
Documented in the README options table (also fixing the stale `timings.*`
key names to the actual `arp_scanner.*` keys), and added to the TOML
samples and the NixOS module.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Listen for DHCP DISCOVER/REQUEST broadcasts on UDP 67 to catch devices as
early as possible — a device must request an address before doing almost
anything else, often before it has an IP.
Follows the mDNS/SSDP scanner pattern (finder/scanner/status modules) and
feeds the shared devices/events/notifications pipeline. The client MAC is
taken directly from the packet's chaddr, so no ARP probe is needed; a
DISCOVER with no assigned IP reuses any previously recorded address rather
than clobbering it. Exposes GET /api/dhcp_scanner/status, wired into the
OpenAPI generation, and adds a Dhcp variant to DeviceEventScanner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Add a Network ports section noting that the mDNS (UDP 5353) and SSDP
(UDP 1900) ports are fixed by their protocols and must be available on
the host, alongside the configurable web server port.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose the ssdp_scanner.probe_timeout option (default 2s, mirroring the
mDNS scanner) in the sample config and the NixOS service module.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a new SSDP/UPnP network scanner alongside the existing ARP and
mDNS scanners:
- New scanner module under scanners/ssdp (finder, scanner, status)
- Wire the scanner into the main scan loop
- Add Ssdp variant to DeviceEventScanner
- Add SsdpScanner settings with a configurable probe timeout
- Expose /api/ssdp_scanner/status and wire it into OpenAPI generation
- Add a lint.sh helper and point CLAUDE.md at it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Make each Pushover alert triageable from a lock-screen preview: titles now
carry the device's identity (hostname, vendor, or MAC suffix) and a concise
verb, and bodies are sectioned (Device / Status / Activity or Changes) with
the registration line and device type so a user can decide whether to act
without opening the app. Adds a security hint on new-unregistered devices
and on vendor-change-on-same-MAC (a MAC-spoofing tell); IP-only changes
stay quiet since DHCP rotation is normal. Rendering moves into pure helpers
covered by unit tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
So that consumers of the events API can tell whether a sighting came
from the ARP scanner or the mDNS listener. Adds a DeviceEventScanner
enum (Arp / Mdns) plumbed from each scanner's call site through to a
new scanner column on device_events, exposed via the existing events
endpoint and OpenAPI schema. Pre-existing rows are backfilled with
'ARP' since that was the only scanner before mDNS.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>