Backend:
- Add db::devices::delete to erase a device and its events atomically
- Expose DELETE /api/devices/{mac}/permanently, wired to OpenAPI
- Cover the new db method and endpoint with tests
Frontend:
- Delete action for not-registered devices (detail screen + list row)
and an opt-in "permanently delete" checkbox in the Forget dialog
- Navigate to the devices list after deleting from the detail screen
- Refine button emphasis to M3: single filled primary, error-colored
text buttons for destructive actions, Test demoted to filled-tonal
- Flash the backend-config Test button red on a failed connection test
- Render snackbars through a top-level ScaffoldMessenger host so they
show above dialogs; keep the built-in SnackBar (with an Overlay host)
Docs:
- CLAUDE.md: rustfmt edition 2024, don't revert formatter-only changes,
prefer built-in Flutter components, follow existing patterns + M3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
Adds a responsive Home screen combining the notification list with a
device summary panel and ARP scanner status, visible side-by-side on
wide screens and stacked on narrow ones.
Backend:
- New GET /api/devices/summary endpoint returning registered device
counts and "seen in last 24h / 7 days" breakdowns by registration
status
- Migration 07 adds indexes on (is_registered) and (last_seen,
is_registered) so summary queries use index range scans instead of
full table scans
Frontend:
- HomeScreen replaces NotificationList, embedding notifications,
device summary card, and ArpScannerCard in a LayoutBuilder-driven
two-column (≥700 px) or single-column layout
- ArpScannerCard extracted to a shared public widget reused by both
HomeScreen and StatusScreen
- Nav rail entry renamed to "Home" with home icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows ARP scanner state (running/waiting/error) with a colored indicator
and time that ticks locally every second, refreshing from the API every 5s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the old frontend-only `router` type and adds the full set used
by the backend: network_appliance, home_security, home_appliance, watch,
pc, gaming_console. Adds an `apiName` getter to map camelCase enum values
to the snake_case strings the API expects. Updates the unknown device icon
to a question mark.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The device event history chart previously fetched all events and filtered
client-side. This wires the existing created_from API parameter to the
frontend so filtering happens in the backend.
Also fixes a bug where the Today filter returned no results: rusqlite was
storing datetimes with a space separator ("YYYY-MM-DD HH:MM:SS+00:00")
while SQL filters used RFC 3339 with a T separator, causing string
comparisons to fail for same-day events. All datetime storage across
device_events, devices, and notifications is now consistently RFC 3339.
A migration converts existing records.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Displays a scatter chart timeline of when the device was seen online,
with a time range selector (Today → Last Year). Tooltips show event
date/time, type, and highlight any IP or vendor changes relative to
the current device data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Owner filter uses a server-side substring match (LIKE '%VALUE%');
device type filter matches the stored value exactly, sending an
empty string for unknown/unclassified devices. Both filters combine
with the existing registration status chip via AND logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /devices/:macAddress route showing all device fields with Register/Forget actions
- Device list items are now tappable; popup menu gains a "View details" item
- Notification list items with a linked device are tappable; popup menu gains a "View device" item
- Backend: new DB migration adds optional mac_address column to notifications
- Device-related notifications (NewDeviceFound, DeviceOnlineAfterTime, DeviceChanged) now store the triggering device's MAC address
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Centralises all SnackBar calls behind UISnackbars.showError/showSuccess/
showWarning/showInfo, each themed via AppColorExtension. New warning and
info colour tokens added to both themes. Calling any method clears any
visible snackbar before showing the new one.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a context menu to each device card with contextual actions:
- Unregistered devices show a Register option (dialog with owner field
and device type dropdown)
- Registered devices show a Forget option (confirmation dialog)
Also fixes CORS to allow PUT and DELETE methods, adds device type
tooltips on the icon, and renames the "New" filter/badge to
"Not registered".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>