Reflow the systemDevices.map(...) chain after the .where() removal so
`dart format --set-exit-if-changed .` is clean. No behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The scanner filtered advertisements by device-name keyword
(withKeywords: deviceNamePrefixes), so any MeshCore/Offband device
whose advertised name fell outside the hardcoded prefix list was
invisible to the scanner — even though the official MeshCore app,
which scans by the Nordic UART service UUID, found them. Switch
startScan to withServices: [Guid(service)] and drop the now-redundant
name filter on the already-service-scoped systemDevices fallback.
Device-tested on a Samsung S25: a previously-invisible node now
appears in the scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI's `dart format --set-exit-if-changed .` step is repo-wide. My A/B/C
commit hand-wrote the .catchError blocks without running the formatter,
and 2 settings views carried pre-existing format drift from #28. dart
format reflows all three (whitespace only, identical logic):
- lib/connector/meshcore_connector.dart (this PR's blocks)
- lib/screens/settings/app_settings_view.dart (pre-existing, #28)
- lib/screens/settings/message_settings_view.dart (pre-existing, #28)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The OffbandMesh org board (project #2) had no label->board sync, so issues never populated it (root cause of the 0-item board). Add sync-labels-to-board.yml (adapted from the DifferentWire/Unfocused canonical) wired to board #2's project + status/priority field and option IDs; runs on ubuntu-latest, uses repo secret PROJECT_PAT (classic PAT, project+repo scope). REQUIRES PROJECT_PAT set as a repo secret to function.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
settings_screen action handlers fire-and-forgot or awaited-without-catch several connector calls, and _loadVersionInfo called setState after an await with no mounted guard. Add a mounted guard to _loadVersionInfo; try/catch + error snackbar to the GPS-enable toggles (inline + _editLocation dialog) and getContacts; await syncTime in _syncTime so the 'synchronized' snackbar shows only on success (it previously fired before the await); and in _editLocation's save, capture the messenger before the pop and wrap the setCustomVar/setNodeLocation/refreshDeviceInfo awaits.
Verified non-issues: deleteAllPaths is void, _PrivacySection._apply() already has a try/catch, and exportGPX catches internally and returns gpxExportFailed. Completes #28; error strings English for now (separate l10n sweep).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings onChanged/onTap callbacks fire-and-forgot AppSettingsService Future<void> setters, so a persist failure rejected unhandled and was swallowed. Add a shared persistSetting(context, action) helper that awaits the setter in a try/catch and shows a dismissible error SnackBar, capturing the messenger before the await so it survives a picker-dialog pop. Wrap the 27 onChanged/onTap setter sites in app_settings_view + message_settings_view; setAppDebugLogEnabled and setNotificationsEnabled (which await + show their own confirmation) get an inline try/catch instead.
Covers the two onChanged panes; the settings_screen action flows (GPX export, location dialogs, version-info mounted guard) follow in a second pass, so #28 stays open. Error strings are English for now (separate settings l10n sweep).
Adds a persistSetting widget test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DirectRepeater.ranking added recencyScore (0..1.8M) to the SNR term (1000/dB), so recency dominated (~1 dB == 1 second), contradicting the comment that recency only breaks ties. Scale recency to a 0-999 bonus so SNR (1000/dB) is primary and recency only decides sub-dB ties.
Offset SNR by its floor (+32; SX126x SNR spans -32..+31.75 dB) so every live ranking stays >= 0, above the -1 stale sentinel; otherwise the scaled-down recency leaves live rankings negative and a stale repeater would outrank a live one.
Adds DirectRepeater.ranking tests: SNR-first, recency tie-break, stale=-1, live-always-outranks-stale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
disconnect() never resets the channel store scope (publicKeyHex), so a channel write in the post-disconnect async gap still persists -- to the last radio's scope, and into a different radio's scope after reconnecting (cross-radio bleed). Add if (!isConnected) return; to the UI-triggered persisters setChannelUnreadCount, markChannelRead, setChannelOrder. Chosen over clearing _channels, which would risk a racing mutator persisting an empty list and wiping stored channels.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PathHistoryService capped each contact's recentPaths at 100 but dropped any new distinct path once full, so the list froze on the first 100 ever seen and never refreshed with newer/better routes. Evict the lowest-_scorePathRecord entry to make room, keeping the strongest 100. Adds a regression test (full history evicts the weakest to admit a new path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Query FlutterBluePlus.systemDevices on every native platform (was Linux-only)
so a radio already connected to another central -- e.g. Liam's MeshCore app --
appears in the picker and we attach to the existing link. A connected BLE radio
stops advertising, so a scan alone never finds it.
Patch increment for the #37 compact-tile feature (the bump that was owed
inside PR #47). Build +18 stays ahead of rc.3 (+17) so a 1.1.1 build
installs over the sideloaded RC.
Parse a leading @[Name] in the message body and render it as a rounded
pill that sets the name apart from the rest, matching the reference
client. Handles a missing space after the mention. Lives in
TranslatedMessageContent so channel + direct chat both get it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Clock setting (System / 12h / 24h) in App Settings; chat times follow it + locale.
- Channel tile: always-on metadata row - time + hops on inbound, time + cell_tower
heard count + status on outbound - decoupled from the message-tracing toggle. The
via-path stays behind tracing.
- Drop the auto reply-to block and the most-recent-sender guess; @mentions now
render inline in the message text.
- Direct chat: same clock-aware time formatting.
Closes#37
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the typed text anywhere in the shortcode name (not just the prefix),
ranked exact > prefix > substring, so `:check` surfaces the checkmarks (✅ is
`white_check_mark`). Add check/checkmark/tick aliases for ✅ in a separate,
regen-safe map the chat + channel composers consume.
Closes#45
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more connector silent-failures from the review. requestRadioStats had an empty 'catch (_) {}' (now logs). setChannelUnreadCount fired unawaited(saveChannels) with no error path (now .catchError + logs). Verified: setContactUnreadCount calls a void debounced store method -- no call-site Future (Gemini false positive); the actual swallow is inside unread_store's debounce timer, flagged separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BLE onValueReceived.listen had no onError, while USB (L1557) and TCP (L1669) both attach one that logs and gracefully disconnects; an error emission on the BLE notify stream was unhandled. Add a matching onError. Also await sendMessageWithRetry in the reaction branch of sendMessage -- the normal path already awaits it; the reaction path's unawaited call swallowed failures, contradicting its own 'same as normal messages' comment.
Verified, not assumed: Gemini's 'disconnect() not try-caught' was a FALSE POSITIVE (already wrapped at L2484-2489); 'deleteAllPaths silent void' is a sync in-memory clear (no async failure path). flutter analyze clean; Gemini-reviewed with no critique of these two changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace inherited zjs81 9.1.0+14 with Offband's own line. Milestone catch-up: v1.0.0=rebrand (c002f90), v1.1.0=settings shell. Build number 14->15 (monotonic). Tags applied separately on dev.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Human testing found Privacy controls slow to respond. _PrivacySection._apply called refreshDeviceInfo() after every toggle/dropdown change -- a 5-frame device re-query (deviceQuery, appStart, battery, customVars, autoAddFlags) that does not even re-fetch telemetry. setTelemetryModeBase already updates the connector's local state and calls notifyListeners(), so the refresh was pure overhead. Removed it; apply is now a single setOtherParams frame, matching the auto-add section's responsiveness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring radio config up a level: render the form directly in the Radio Settings pane with a full-width Save button, instead of a tile that opens a dialog. _RadioSettingsDialog -> _RadioSettingsForm; build returns an inline Column of the (verbatim) fields + Save; _saveSettings drops the Navigator.pop (no dialog), which also resolves the unguarded-pop-after-await finding. Removed _showRadioSettings. Field widgets, controllers, preset matching, and off-grid snapshot logic unchanged.
Key the form on the device's current radio values (freq/bw/sf/cr/txpower/clientRepeat) so a device-reported change re-seeds it -- the dialog got this for free by reopening; the persistent inline form needs the key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the Privacy save/cancel dialog with an inline _PrivacySection in the pane: advert-location + multi-ACKs toggles and the three telemetry-mode dropdowns now apply on change. All five commit through one setTelemetryModeBase call, so each control sends the full current state (coalesced via an apply guard) and reverts to the device's values on failure with an error snackbar -- folding in the #28 error handling for this pane. Removes the category->tile->dialog hop; drops _privacySettings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flatten two more redundant layers in the settings IA. Radio Stats: extract CompanionRadioStatsScreen's body into an embeddable CompanionRadioStatsBody (carrying the 1s-polling acquire/release lifecycle), add a top-level Radio Stats category that renders it directly, and remove the nested Radio Stats tile from the Radio Settings pane (the screen is kept; still pushed from radio_stats_entry.dart). Rename the Contact Settings category to Contacts (reusing contacts_title) with a contacts icon; rename _messagesContactsPane to _contactsPane.
Also fixes a pre-existing chart-freeze surfaced by review: _NoiseChartPainter compared only sample-list length, so the noise chart stopped repainting once the 120-sample buffer filled. Now compares contents via listEquals.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Split the monolithic AppSettingsScreen (reached via a redundant tile->push from the App Settings category) into two embeddable views rendered directly in the shell panes: AppSettingsView (appearance, translation, battery, map, Cyr2Lat, debug) and MessageSettingsView (notifications + message handling, a new top-level 'Message Settings' category after Contacts). Removes the category->tile->separate-screen layer; each category now lands directly on its settings. Deletes app_settings_screen.dart (no remaining refs). Adds settings_messageSettings(+Subtitle) l10n keys (en template; other locales fall back pending the l10n sweep). Card bodies moved verbatim -- behavior preserved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the Contact-settings tile->modal dialog with an inline _AutoAddSection: five SwitchListTiles that apply on toggle (send config frame + refetch flags). Apply path has a coalescing guard (re-applies the latest state instead of overlapping sends) plus inline error handling (revert to device flags + snackbar). _PathHashSizeTile onChanged wrapped in try/catch + error snackbar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run dart format across the repo to clean drift in 5 files (channel_chat_screen, settings_screen, settings_shell, gif_picker, mention_autocomplete) from earlier straight-to-dev commits that never hit the format CI. No logic change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the monolithic card+dialog settings screen with a responsive master-detail shell (rail+pane when wide, list+push when narrow; layout chosen by width via LayoutBuilder). Regroup the existing settings into intent categories and surface Path Hash Size (1/2/3-byte) in Radio and range, the first device-config-parity gap (previously repeater/CLI-only).
No settings lost: every existing control stays reachable. Phase 0 of #7.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allowlist the 6 gitleaks findings (all upstream/already-public: 5 generic-api-key
false positives in app_pl.arb, 1 Giphy public beta key in gif_picker.dart, since
replaced by --dart-define). gitleaks now reports 0. See #13 for the scan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gif_picker.dart reads GIPHY_API_KEY from String.fromEnvironment instead of a
hardcoded constant - key is supplied at build time (--dart-define-from-file=
dart_defines.json, gitignored), never in source or history. Adds
dart_defines.example.json template + .gitignore entry; picker shows a hint
when no key is configured.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repo moved Strycher/meshcore-open -> OffbandMesh org and renamed to
meshcore-client; update README (clone URL + cd + issues), CONTRIBUTING,
and TESTFLIGHT_GUIDE links. zjs81 upstream attribution links unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repo is going public, so the tracked dev-facing CLAUDE.md is rebranded with
NO private infra:
- Title + intro -> "Offband Meshcore"; applicationId -> app.offband.meshcore.
- Added attribution: fork of MeshCore Open by zjs81 (MIT).
- Refreshed version reference -> 9.1.0+14.
Private governance (toolchain/build paths, signing, device, Citadel/board IDs,
DW hooks) stays in the untracked, gitignored CLAUDE.local.md - never in tracked
files (git history is permanent once public).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sweep user-facing docs from "MeshCore Open" to "Offband Meshcore":
README, docs/PRIVACY_POLICY, documentation/*, CONTRIBUTING, TESTFLIGHT_GUIDE.
- Prominent README attribution (fork of MeshCore Open by zjs81, MIT) + a
Credits section. LICENSE left untouched (zjs81 copyright retained).
- Removed the upstream donation block (Solana/Monero/BTC) per owner.
- Removed upstream-only pointers that would misdirect (zjs81 Obtainium badge,
meshcoreopen.org); pointed clone/issues at Strycher/meshcore-open; fixed the
stale iOS bundle id -> app.offband.meshcore.
CLAUDE.md (dev-facing instructions) intentionally left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integrates the Offband rebrand (chore/offband-rebrand, 98a7728) on top of
dev, which already carries Batch-1 UX (#2) + emoji :shortcode: (#5) via
fast-forward of feat/batch1-ux.
pubspec version conflict resolved to 9.1.0+14 (emoji MINOR over rebrand
PATCH 9.0.1; build kept at the higher +14).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The maintainer's keyboard can't type emoji, so the composer now accepts
:shortcode: input with an inline autocomplete:
- Type `:` to open a bottom-anchored picker; arrows/Tab/click select, Esc
dismisses. Best/exact match sits at the bottom, nearest the caret.
- Enter completes an unambiguous emoji (single OR exact match) then sends,
so `:rofl` + Enter ships the glyph, not the literal text. Ambiguous
matches still send raw; Tab picks one deliberately.
- A completed `:name:` (closing colon) auto-converts in place.
- 1913 gemoji shortcodes bundled (lib/helpers/emoji_shortcodes.dart, MIT).
- Byte-count aware via the existing ByteCountedTextField; shares the
MentionAutocompleteField with @-mention input (both composers wired).
Version 9.0.0+12 -> 9.1.0+14 (MINOR feature; build kept monotonic after
the rebrand's +13 on the parallel chore/offband-rebrand branch).
Grid/tap picker deferred (#5 follow-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Freeze permanent applicationId/bundle id to app.offband.meshcore
(three-segment so app.offband.* reserves the brand family; a future
app.offband.meshtastic won't collide).
Display name "Offband Meshcore" across: Android android:label, iOS
CFBundleDisplayName, Windows + Linux window titles, MaterialApp title,
Windows VERSIONINFO (ProductName/FileDescription/CompanyName), pubspec
description, GPX creator. iOS/macOS bundle ids + Linux APPLICATION_ID
set to app.offband.meshcore.
Version 9.0.0+12 -> 9.0.1+13 (PATCH milestone + build).
Deferred (unchanged): internal Dart package name (meshcore_open imports),
Android namespace + MainActivity package, exe/bundle filenames, docs prose
(README/privacy/documentation still say MeshCore Open - separate sweep).
MIT attribution to zjs81/MeshCore preserved (LICENSE untouched).
Verified: flutter analyze clean; Android debug APK built green;
aapt2 dump packagename = app.offband.meshcore; Windows compiled clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Typing @ at the start of a word opens a bottom-anchored dropdown above
the composer, filtered live by the text after @. Recent channel senders
sort to the bottom (most recent closest to the input, auto-highlighted);
other contacts stack above in alpha order (A nearest the input), with a
Recent divider between. Up/Down move the highlight, Tab or click selects
(inserting @[ExactName] at the cursor), Esc dismisses, Enter sends.
- New MentionAutocompleteField wrapping ByteCountedTextField with an
Overlay dropdown and FocusNode.onKeyEvent key handling
- _buildMentionCandidates sources recent senders from the channel
history plus contacts, deduped (recent wins)
Verified on desktop, including wrapped/multi-line input.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The firmware path-length byte packs hop count (low 6 bits) and hash
width (high 2 bits) into one byte. The UI used it raw, so a 4-hop/2-byte
path showed as "68 hops" and the path list rendered per byte (8 entries
for 4 hops), and a 2-byte region mismatched a 1-byte one.
- Add pathHopCount/pathHashSizeBytes decoders (meshcore_protocol.dart)
- Add ChannelMessage.hopCount / pathHashSize getters; decode at display
and leave the stored pathLength untouched so the duplicate-merge in
the connector stays self-consistent
- Add an always-visible per-message hop-count badge in the channel chat
- Group path bytes into hash-width hashes in the chat "via" line and the
Message Details screen, using each packet's own width so mixed
1/2/3-byte feeds render correctly side by side
Verified against live BLE traffic (mixed 1-byte and 2-byte senders).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- channel_chat: always-visible HH:MM timestamps (theme-aware dark-mode contrast)
- channel_chat: Today/Yesterday/date day dividers between message days
- contacts: pin favorites to the top, preserving the active sort within each group
Fork of zjs81/meshcore-open (MIT). Tracking #2 (migrated from Strycher/LoRa#343).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>