A repeater reply that arrived after its command's window closed hit
`if (commandId.isEmpty) return;` in RepeaterCommandService.handleResponse
and was dropped with no log and no UI. Since the command timeout can be
shorter than the app's own message-retrieval budget, that made "the
command ran but the response was never reported" the normal outcome
rather than an edge case, and it affected every repeater screen, not
just the CLI one.
RepeaterCommandService now remembers a timed-out command's prefix for two
minutes, so a reply arriving afterwards can be attributed to the request
it answers. Replies that reach no waiting command are handed to a new
onUnmatchedResponse sink and logged through appLogger. No path through
handleResponse returns without either completing a command, surfacing the
payload, or logging why it could not.
The CLI screen renders these as a distinct history entry naming the
original command and how late it was. The settings screen applies the
value if it is a `get` reply and tells the user it arrived late.
repeater_status_screen already parsed responses independently of the
service, so it had no silent-loss path to fix.
Part of epic #473. Does not change the timeout window itself (#529),
the reported duration (#531), or the stale-prefix fallback (#532).
Adds three tappable rows to the About page opening in the external browser via
url_launcher: offband.org, the Google Play listing (app.offband.meshcore), and
offband.org/donate (Ko-fi + GitHub Sponsors options, live-verified). Failure
surfaces a snackbar. Completes epic #525.
Agent: QuietSnow (session 31eaba02)
About moves out of the Debug pane into a top-level 'About' category (its own
pane, not the stock dialog). Keeps the marketing version display, adds a
description + refreshed legalese (Offband, zjs81/MeshCore MIT attribution kept),
and a View licenses row (showLicensePage). Build identity stays in Device Info
(#553). #527 adds the outbound links here next. Part of epic #525.
Agent: QuietSnow (session 31eaba02)
Hiding the section left enabled features running invisibly. The action is now
'Disable experimental features': disableExperimental() turns off every
experimental flag (CoreScope observer counts, future ones) AND re-locks the
section in one atomic write. Also fixes the subtitle to say 'Build row' (the
#553 anchor), not 'version row'. Part of the #509/#553 experimental section.
Agent: QuietSnow (session 31eaba02)
Badge stays observers-primary; tooltip/long-press now read 'N observers . M
observations' so they reconcile with CoreScope's 'Observations (N)' feed (the
two are distinct metrics: 15 distinct observers vs 34 total sightings). Refresh
feedback moved from a bottom snackbar to an auto-dismissed top MaterialBanner,
out of the way of the composer. Part of #524.
Agent: QuietSnow (session 31eaba02)
Owner testing: the inline badge needed pixel-accurate taps, the refresh gave no
feedback, and there was no refresh in the message action sheet. Now the badge is
a padded InkWell (real tap target), the long-press panel has a 'Refresh CoreScope
observers' action, both show a snackbar with the fetched count, and the auto-poll
is more patient (adds 90s tail steps, stabilises after 3 flat checks) so it climbs
closer to the total before you need to tap. Part of #524.
Agent: QuietSnow (session 31eaba02)
After a channel send, when firmware advertises 0xC6 (cap2 0x08 + ver>=22) AND
the owner enabled the feature, the connector queries the packet hash, then
CoreScope for observer_count, and stamps it on the message (background,
best-effort). Adds the coreScopeObserverCountEnabled AppSettings flag, a gated
switch in the #509 Experimental section (disabled until the radio supports the
capability), and a distinct cloud badge next to the radio-heard count. All
inert until the firmware PR (#611) lands. Completes the client side of #524.
Agent: QuietSnow (session 31eaba02)
Owner feedback: per-tap snackbars appeared over the version/About row and
blocked the next tap; and a 'Hide' switch sitting ON read backwards.
- Tap progress + unlock/already-unlocked now render inline under the
version number (2s auto-clear timer), so nothing overlays the tap target
or the app bottom.
- Experimental 'Hide' is now a plain action row, not a switch.
Agent: QuietSnow (session 31eaba02)
7 taps on the About version row (rolling ~3s window) reveal a neutral
'Experimental' settings category; persisted on AppSettings, survives
restart, re-hidden from a switch inside the section. Empty of toggles
for now (Fast Sync #118, CoreScope #524 land into it later).
Countdown snackbars after tap 4; already-unlocked feedback. Version-text
tap is absorbed so it counts without opening the About dialog.
Agent: QuietSnow (session 31eaba02)
Channel messages had no resend path: unlike DMs (auto-retried by
MessageRetryService), a channel send that never gets its generic ack/repeat-back
goes to failed with no recovery. Adds a "Send Again" action to the channel
long-press sheet for outgoing messages that are not yet acked (status != sent,
i.e. pending or failed), re-sending via sendChannelMessage.
Also renames the existing direct-chat "Retry" action to "Send Again" (matching
stock MeshCore's "Send Again" for cross-client familiarity); DM gating unchanged
(offered at failed, after auto-retry is exhausted). New l10n strings
message_sendAgain + chat_sendingAgain. Build of epic #256.
Ben's style rule: no em-dash character in copy, docs, or code comments
(it reads as an AI tell), and the repo is going public. Replaced the
em-dashes in code comments and English UI strings with commas, colons,
or periods, whichever reads best. User-facing strings were hand-tuned
for natural punctuation rather than a blanket comma.
Preserved the lone "no data" glyph placeholders (a standalone dash used
as a not-available indicator in status displays); those are a design
element, not prose.
Regenerated app_localizations*.dart from app_en.arb (the English
fallback for untranslated keys propagates to every locale's generated
file). Non-English ARB translations left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gemini flagged two unhandled PlatformException paths (SAFELANE §6):
- isIgnoringBatteryOptimizations could throw -> banner silently never shows;
now caught + logged, leaving the banner hidden on an unknown status.
- openIgnoreBatteryOptimizationSettings could throw -> button did nothing with
no feedback; now caught + logged + a snackbar tells the user it failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On aggressive OEMs (Samsung One UI) a backgrounded app that is not exempt
from battery optimization is slept on screen-off and drops the radio
connection, with no warning. Add a persistent banner on the channels screen
that appears (Android only) when the app is not exempt, explains the risk,
and deep-links to the battery settings via
openIgnoreBatteryOptimizationSettings(). Re-checks on resume so it clears
once the user applies the change; dismissible for the session.
No restricted permission: reads own status and opens the settings screen
(ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); the Play-restricted
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS path is deliberately avoided.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Smaz is a client-side text convention with no MeshCore protocol or firmware
support (findings: GH-315). When enabled it compressed ordinary prose into
`s:`+base64, which any non-lineage client renders as garbage — the same
cross-client interop failure as the old `g:<code>` GIF token.
Phase 1 of the Smaz-removal epic (GH-314): stop sending Smaz and remove the
user-facing surface, while KEEPING decode so messages from lineage peers still
on Smaz, and any legacy compressed rows, keep rendering. Decode removal is
deferred to Phase 2 (GH-421).
Removed: the Smaz branch in prepareContact/ChannelOutboundText (Cyr2Lat branch
and the structured-payload guard preserved); connector state/API (the enabled
maps, is*/set*SmazEnabled, ensureContactSmazSettingLoaded, the warm-up and
channel loaders); the per-channel and per-contact toggles plus their
mutual-exclusion; loadSmazEnabled/saveSmazEnabled and the `*_smaz_` key prefixes
(stores kept, they also hold Cyr2Lat); l10n `channels_smazCompression` and the
orphaned `chat_compressOutgoingMessages` across 18 locales (+ regenerated
app_localizations).
Retained for Phase 2: the 5 Smaz.tryDecodePrefixed decode sites and
helpers/smaz.dart.
Safety (verified): no storage migration, the app already persists plaintext
(receive decodes before store; send stores the pre-compression text), so the
`s:` form was wire-only. ACK matching is unaffected, the expected hash derives
from prepare*'s output, so dropping compression keeps both sides hashing
plaintext.
Behavior change: the composer byte-counter now reflects raw size, so anyone who
had Smaz on can type slightly fewer chars per message.
Tests: gif_url_outbound_guard_test rewritten to pin plaintext passthrough and
decode retention. Full suite green, analyze clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tapping a DM opens the Path screen; it now shows the RF details the app
was already receiving but discarding:
- SNR: captured from the v3 contact-msg-recv frame (was skipBytes(1)).
Firmware sends (int8)(snr_dB*4), so dB = byte/4.0 (MyMesh.cpp:512) —
the old commented-out code multiplied by 4, which was 16x wrong.
- Path type: firmware sends path_len 0xFF for a direct/routed frame and
the hop count for a flood (MyMesh.cpp:545). The app collapsed 0xFF->0,
colliding "direct" with "flood, 0 hops". Capture the distinction into
Message.isFloodRoute so the Path row reads "direct (routed)" vs
"flood, N hops".
- RSSI: row wired to Message.rssi but left null — the wire byte is a
hardcoded reserved 0 today; populated once firmware ships it (#439).
Message gains snr/rssi/isFloodRoute (nullable, additive, persisted like
rxTime). 10 tests cover scaling, the 0xFF discriminator, and copyWith.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The share helper only invoked the OS share sheet on mobile; on all desktop it
opened the containing folder, which for a temp-dir capture dumped the user into
TEMP amid unrelated files. Now:
- Android/iOS: OS share sheet (unchanged).
- Windows/macOS/Linux: native Save As dialog (file_selector) writing the file
to a user-chosen location.
- Web: browser download of the log text (no on-disk file on web).
Adds file_selector; web download via a js_interop helper behind a conditional
import. Folds in #427 (localized share strings). LogExport.shareFile keeps a
compatible signature for the serial-capture screen (#430).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route LogExport's Share tooltip, share subject, and file-logging-unavailable
snackbar through context.l10n instead of hardcoded English. Adds four keys to
app_en.arb (debugLog_shareLog, debugLog_openLogsFolder, debugLog_shareSubject,
debugLog_fileLoggingUnavailable) and regenerates all locales (English fallback
until translated). Resolves the deferred Gemini finding from #393.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No in-app signal of which binary is running has caused repeated confusion
(a debug-signed APK silently not installing over the release app, old layout
persisting with nothing to indicate the new build never landed).
BuildInfo reads GIT_SHA/GIT_BRANCH/BUILD_TIME from --dart-define with dev
fallbacks, so every build carries its own identity independent of the pubspec
version. Surfaced as its own copyable Build row in Device Info.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stores are keyed by the first 10 hex of the connected radio's public key, so
connecting a different radio silently swaps which contacts, channels, and
history you are viewing with nothing in the UI saying so. Device Info showed
the full public key but never tied it to storage.
Adds a Data scope row with the key actually in effect, plus a one-line
explanation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Advertisement Notifications" reads as ads. Retitled to "New node discovered"
/ "Notify when new repeaters or contacts are heard" across all 18 locales so
the overnight-ping setting is findable without mesh jargon.
Coverage verified, no gap found: showAdvertNotification is the only discovery
notification entry point and all three of its callers
(meshcore_connector.dart:4961, 5047, 7226) are guarded by
notificationsEnabled && notifyOnNewAdvert && !isBlocked. The batch summary is
fed solely from enqueued adverts, so it cannot fire with the toggle off.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a KeepScreenAwake controller that holds a wakelock_plus lock while the
setting is on and the app is foregrounded, releasing on background, on toggle
off, and on dispose. Toggle lives in App Settings > Battery (power tradeoff),
defaults OFF, persisted as keep_screen_awake. Strings added for all 18 locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Notification settings moved to the Messages category, but the App Settings
tile subtitle still advertised them. Retitled across all 18 locales to match
what the pane actually renders (appearance, translation, battery, map, Cyr2Lat).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the database can't open (e.g. the native sqlite library fails to load),
every read/write silently failed and the app opened to an empty, normal-looking
screen — the user thinks their history was wiped (SAFELANE §6 violation).
Probe the storage layer in main() before any store reads
(BlobStore.verifyReadWrite). On failure, a StorageHealthService one-way latch
records it, and a persistent, non-dismissable banner is shown above the whole
app: history is not lost, storage is unavailable, messages are NOT being saved,
restart after fixing. Cross-platform (probe goes through drift, covers web too).
Tests: service state/latch + banner show/hide widget test. Gemini-reviewed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner hardware test on an S25 FE and Windows turned up five issues. Three of
them shared one root cause.
**Map: always show names.** New mapAlwaysShowNames setting, toggled from the
map panel. Names were shown only past zoom 14; the override wins at any zoom.
Evaluated at build rather than only on camera movement, so flipping the switch
repaints immediately instead of waiting for the next pan.
**Panel footer on every screen.** Disconnect and Settings now also appear
inside a channel. The footer is app-level, so it belongs anywhere the panel
is, not just the primary views.
**Back button (three reported symptoms, one cause).** Channels, Contacts and
Map each wrapped themselves in `PopScope(canPop: !isConnected)`, so while
connected the system back press was swallowed whole. That explains all three:
an open drawer would not close, Android would not background the app, and on
Windows the pinned layout has no Scaffold drawer, so the app bar auto-implied
a back arrow whose press PopScope then discarded - a button that visibly did
nothing.
AppShell now owns back handling for every screen that uses it, in order:
close an open drawer, else pop the route (a pushed chat returns to its list),
else hand back to the OS via SystemNavigator.pop() so Android returns to the
home screen or the previous app. The three per-screen PopScope wrappers are
removed. AppShell becomes stateful to hold the scaffold key this needs.
flutter analyze clean, dart format clean, 469 tests pass. Not yet re-tested on
hardware.
A capability-gated control that doesn't appear is indistinguishable from
a broken one. Diagnosing "the FEM LNA toggle is missing" on real hardware
required a rebuild, because the caps byte was never surfaced and the app
debug log records nothing in a release build unless logging is enabled.
Adds an "Offband capabilities" row to Device Info showing the raw caps
byte, firmware version code, and which gated features it grants, e.g.
"0x02 (v16) - block". That turned an unexplained missing toggle into a
one-look answer: firmware v16 running, FEM LNA bit clear, client correct.
Also logs the same values at device-info parse for anyone who does have
logging on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the client half against the firmware as-built (#298):
- Device-info offset 83 carries the FEM LNA state on v16+, appended
unconditionally (reads 0 on non-capable boards). parseFemLnaState is
length-guarded and returns null pre-v16. Byte presence signals firmware
version, not capability — the cap BIT gates the UI.
- 0xC3 reply handler adopts the reported value verbatim: firmware returns
post-apply hardware state, not an echo, so a refused write surfaces as
truth rather than a lie.
- setFemLna / requestFemLnaState no-op unless the capability bit is set,
so a non-capable radio never sees 0xC3 traffic.
- Radio Settings toggle rendered only when the bit is set, driven by
connector state via ListenableBuilder rather than local optimistic
state, so it always shows what the radio reports.
- errCodeUnsupportedCmd documented: a mis-gated request draws [0x01][0x01],
not [0x01][0x06]. Neither is 0xC3-prefixed, so no new error path.
Gating is on the bit alone, never model or version: firmware derives it
from a runtime FEM probe, so two Heltec V4s can legitimately disagree and
rak3401 reports false by design (its SKY66122 gates LNA and PA together).
13 tests. Not hardware-validated — firmware 0xC3 is still on a branch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sensors were the one advert type with no filter. advTypeSensor (4) already
existed in the protocol and Contact.typeLabelRaw already returned 'Sensor',
but ContactTypeFilter stopped at rooms, so sensor contacts could not be
isolated in either the contacts list or discovery.
- ContactTypeFilter gains `sensors`. Both exhaustive switches over it were
found by the analyzer rather than by hand: the contacts list predicate and
the search-hint text.
- discovery_screen's predicate has a `default: return false`, so it compiled
without a sensors case but would have silently shown an empty list. Added
explicitly.
- New l10n strings contacts_searchSensors and listFilter_sensors, regenerated
across all 18 locales. The 17 non-English locales fall back to the English
text and are recorded in untranslated.json, matching how existing strings
are handled.
- Persisted round-trip verified: UiViewStateService stores the filter by name
with orElse -> ContactTypeFilter.all, so older persisted values cannot throw
and `sensors` persists like the rest.
Groundwork for the Contacts filter rail (#307), but useful on its own: the
existing filter menu now offers Sensors.
flutter analyze clean, dart format clean, 445 tests pass.
The channel long-press sheet's binary Mute/Unmute tile becomes a
Notifications tile showing the current level, opening a picker with All
messages / Mentions only / Off.
The mode is read and written against the channel's PSK identity via
AppSettings.channelNotifyKey, so it survives a rename and does not follow a
reused slot (#259). Plain ListTiles with a check mark are used rather than
RadioListTile to stay clear of the Radio groupValue deprecation.
Adds channels_notifications / channels_notifyAll / channels_notifyMentionsOnly
/ channels_notifyOff to app_en.arb and regenerates l10n; the other 17 locales
fall back to English and are recorded in untranslated.json.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the DM composer with an "Unblock to message" notice bar when
the contact is blocked, so a block also stops outgoing DMs (previously
only incoming DMs/adverts were suppressed). Inline unblock restores the
composer. Adds block_composerNotice l10n string and documents the
outgoing-DM rule in block-contract-as-built.md §4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Settings showed the self public key truncated to 16 hex chars + "..." as
plain Text (settings_screen:262/:410), so it couldn't be shared. Show the
full key, make it selectable, and add a copy button beside it.
- _buildInfoRow gains an optional copyValue: renders the value as
SelectableText + a copy IconButton that copies the full key and shows a
"Public key copied" snackbar. Both self-pubkey rows pass the full key.
- New l10n string settings_publicKeyCopied (regenerated localizations +
untranslated.json).
Part of #234.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connector parses ADD/REMOVE/CLEAR replies; ADD ok=0 (node store full at 32) sets
blockOffloadStoreFull (local stays authoritative), cleared on reconcile. BlockedView
shows a firmware-offload status row when supportsOffbandBlock — 'active', or a
'radio block list full' warning. Adds block_offload* l10n.
Epic B #166 / B5 #180.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New BlockedView pane (Settings category, after Privacy): lists blocked pubkeys
(contact name when resolvable, else short key) and name-only channel blocks, each
with Unblock; empty state when nothing blocked. App-local; firmware-offload
indicator deferred to Epic B. Adds block_* settings l10n.
Epic A #165 / A6 #173. Design: docs/architecture/block-contract-as-built.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long-press a channel post -> Block sender: resolve the claimed name to known
pubkey(s) and block each (full block across DM+adverts+all channels, multi-device
covered); if unresolved, block the name globally across ALL channels (promotes to
pubkey later, #174). Adds block_* l10n strings. A5 channel surface;
contacts/chat/discovery + unblock still to come.
Epic A #165 / A5 #172. Design: docs/architecture/block-contract-as-built.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Add Channel "Scan QR Code" scanner now accepts channel share URIs
(meshcore://channel/add) alongside community QR JSON: it parses name +
secret, confirms, and adds the channel to the next free slot via
setChannel. Completes the channel-QR round-trip in-app, and interops with
the reference app's channel QRs.
Bumps build to +52 for the b52 combined test binary (path-hash + trace +
channel QR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel.toShareUri/fromShareUri/isValidShareUri encode the reference
MeshCore app's format: meshcore://channel/add?name=<url-encoded>&secret=<hex32>.
A "Share QR Code" action in the channel menu shows the QR via the existing
QrCodeShareDialog, so it round-trips with the reference app.
Encode/decode unit-tested against both real reference examples (#test, #echo).
Scan-to-add follows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Set Path" dialog parsed, built, and displayed hop prefixes as a fixed
1 byte (substring(0,2)), so on a 2-/3-byte path-hash mesh manual entry produced
wrong-width routing paths and the helper text claimed "1 byte". Make it width-
aware from connector.pathHashByteWidth.
- parsePathPrefixes(text, width, invalid): static + testable; each entry must be
EXACTLY width*2 hex chars (wrong-length / malformed go to invalid, never
silently truncated); width clamped >= 1.
- _updateTextFromContacts / display / maxLength group by the configured width;
the display substring is guarded against short keys.
- show() takes pathHashByteWidth; the chat + path-management callers pass
connector.pathHashByteWidth.
- l10n: reworded the path-entry helper / example / instructions to be width-
agnostic (dropped the "2 hex / 1 byte / first byte" claims). The 17 non-English
locales keep their prior strings, flagged for re-translation (English fixed).
analyze clean; 6 parsePathPrefixes tests (incl. exact-length + clamp). Gemini v1
BLOCKER (unguarded substring) + MAJOR (silent truncation) fixed; v2 MAJORs were
the pre-existing refresh button (out of scope), v2 MINOR (1-byte example) fixed.
Closes#155
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Offband 0xC1 GPS query: 1-byte request -> RESP 0xC1 ASCII reply
(enabled/detected/active/fix/baud/lat/lon/alt_cm/sats/time), parsed into
OffbandGpsStatus (lat/lon /1e6). New GPS-status section in the Location dialog
(live-fix vs stored banner + coords/sats/alt/time). Validated against RedCreek's
feat/gps-state-query test firmware.
#140: the per-minute GPS poll now uses the lightweight 0xC1 query instead of
CMD_APP_START (which re-walked channels + re-drained messages = the BLE re-init
flood); a live fix updates self-lat/lon. Validated on b42 (APP_START ~1/min ->
~0, 0xC1 polls clean every 60s, channel re-walk gone).
Gemini (fix-then-ship): atomic completer-claim in _handleOffbandGps to shrink
the stale-reply race; full elimination needs a protocol sequence id, but the
poll shares in-flight via the re-entrancy guard and the query button is disabled
during a request, so concurrent requests don't occur in practice. The hardcoded
snackbars Gemini flagged in _editLocation are pre-existing l10n debt, not
introduced here; all new strings are localized.
Closes#135Closes#140
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse the device-info reply's readable strings (build date / model / firmware
version, NUL-terminated after the 8-byte header) and show firmwareVersion +
deviceModel in Settings → Device Info. Raw strings are logged on connect.
End-anchored field mapping (deviceInfoFields) keeps the version correct on
partial frames.
Gemini (fix-then-ship): removed the unused deviceBuildDate field/getter (dead
state) — the build date stays in the connect log via the raw strings, and
deviceInfoFields still parses + tests it.
Closes#134
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Reply long-press drops the cursor into the composer (post-frame focus). (#131)
- "Reply with route" sends `@[sender] ↩ N hops · via <path>`, byte-truncated with `…`, Cyr2Lat-aware. (#106)
Gemini (fix-then-ship): the unawaited `sendChannelMessage` in `_sendRouteReply` mirrors the established `_sendMessage` convention — failure surfaces via the visible message's delivery status, not the returned Future; holistic channel-send error handling is tracked in #133 per maintainer decision. O(n^2) path truncation is negligible on the <=160-byte payload; the `w<1` guard is a defensive div-by-zero (pathHashByteWidth is 1-3).
Closes#131Closes#106
Part of #132
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>
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>
The telemetry parser has been expanded and should now support significantly more metrics. It has been ported from the Python implementation of meshcore_py.