Two changes to the Android wrapper, shipping together as app version 2.1
(versionCode 6).
Notifications stopped arriving a few minutes after the app was backgrounded,
and no setting on the phone could prevent it: Android freezes a process with
nothing in the foreground, and a frozen process runs no JavaScript and holds
no socket. MeshWatchService is a foreground service of type remoteMessaging
that does no work of its own - it exists so Android leaves the process alone.
Every notification is still built by the mc-webui page, so mutes, blocked
senders, channel names, translations and deep links stay in one place rather
than being duplicated in Kotlin.
The service follows mc-webui's own notification toggle, read from localStorage
by the shim rather than asked of the page, because the wrapper connects to
whichever server its user typed in - very often an older mc-webui. The shim
also wraps Storage.prototype.setItem, since localStorage fires no event in the
document that wrote it. Android's own switches have the last word, so a revoked
permission or a silenced channel stops the service too.
setRendererPriorityPolicy goes with it: the page renders in a process of its
own, and WebView waives that process's priority once the view stops being
visible, so a service saving only the host process would still lose the
renderer under memory pressure.
Separately, the address screen now lists the servers reached before, so running
more than one instance no longer means retyping the whole address to switch.
Only addresses that answered are listed, which keeps typos out.
Also replaces the deprecated setDecorFitsSystemWindows with enableEdgeToEdge,
which is what Play Console has been flagging twice. Its default navigation-bar
scrim is overridden, or it would paint over the strip the wrapper colours to
match the page below Android 10.
Docs, the privacy policy (both languages) and the app-vs-PWA comparison are
updated: three of them said the app ran nothing in the background.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wrapper was published to the Play Store today, so three places that told
readers it was not distributed there are now wrong: the Android guide, the
user guide and the README.
Each now leads with the Store and keeps the .apk as the deliberate second
route, with the fact that matters to existing users stated where they will
look for it: both builds carry the same signing key, so installing from Play
updates a repository build in place and keeps the server address.
Historical whatsnew entries are left as written; the change is announced in a
new Unreleased entry instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Expanding a Settings section fired one `get` per field, each a full mesh
round-trip. Seven questions to a repeater four hops away, answered one at
a time, to change the one field you came for -- and often a batch that
timed out halfway and left the section blank.
Nothing is read until asked for now:
- every field has its own read button beside the control (same row, so
no extra line per field), and `Read all fields` at the top of the
section still does the whole batch
- GET /settings takes an optional `fields=` subset so a single field is a
single round-trip; the section still defines the order
- reads are queued client-side. The device already serializes CLI traffic
behind one lock, so parallel requests would only tie up server threads
waiting for it
- values are remembered per repeater in localStorage, so a section opens
with the last readings instead of empty boxes. A remembered value is a
usable baseline -- Apply writes an absolute value, so editing from a
stale reading still sets what was typed -- and is marked with a clock
per field plus a note at the foot of the section saying when it was
read. Apply and unsupported replies update the cache too
- a failed read no longer wipes what is on screen: the remembered value
stays, still editable, still marked
- field hints moved from a text block under the control into an info icon
by the field name (the pattern the app's own Settings modal uses), and
every field has one now instead of the 9 that did
Verified against a live repeater from the local container: expanding all
8 sections makes 0 requests, a single-field read sends exactly
`?section=advanced&fields=rxdelay`, the value survives a reload from cache
with the stale note, and Apply still round-trips (dutycycle rewritten to
its own value).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The path list is a retry order, so the same route twice means retrying a
route that has just failed while a genuinely different one waits. Both
editors (contact info and My Repeaters) POST to the same endpoint, so the
guard sits there: adding or editing a path onto a route the contact
already has is refused with 409 duplicate_path, and the modal stays open.
Paths are now normalized to lowercase on write and existing duplicates are
collapsed on startup (keeping the primary copy), which lets a unique index
back the check. The index is created in the migration rather than
schema.sql, which runs before duplicates are removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the kebab menu and the detail dialog in the user guide, the two
new API entries in architecture.md, and a What's New entry under
Unreleased.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
user-guide gains two sections: how a received card behaves and what each
button state means, and how the "+" button shares something.
architecture gains "Share Tokens & Cards" — the three wire formats with
the byte arithmetic that explains why contacts use a compact token rather
than the URI, why parsing has to run before HTML escaping, the per-page
hooks the DM iframe needs, and two traps worth not rediscovering: never
compose an i18n key (a composed key hides a missing translation from
i18n_check.py — that is how share.card.private_channel shipped missing),
and cards belong in .message-content, never .message-meta.
whatsnew describes both halves for users, in the release voice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Firmware v1.17 changed no protocol constants and left FIRMWARE_VER_CODE at
13, so the companion protocol cannot tell a v1.16 node from a v1.17 one.
Capability is instead read from the CLI reply text, which self-describes and
works for any version and board without an extra round-trip:
`??: <field>` / `unknown config: <field>` → firmware has no such setting
`Error: unsupported` → hardware cannot do it
`> <value>` → supported
Both of the first two now yield an 'unsupported' status rather than a
failure, rendered as a muted badge with the control locked and excluded from
Apply. Without this, every field added below would read as a red error on
each repeater that predates it.
Applying to an unsupported field deliberately asserts nothing about the
stored value: v1.17 persists radio.rxgain before it learns the radio cannot
apply it, so the old value may or may not still hold. The baseline is
dropped instead of being reasserted, and only Refresh reveals the truth.
Also guards a pre-existing trap: up to v1.16 the `radio.rxgain` getter was
compiled out on non-SX126x radios, and `get radio.rxgain` fell through to
the `radio` branch (its memcmp has no trailing space), answering with the
radio parameters. That parsed as a valid reply and silently rendered the
switch as off. An on/off field that answers neither on nor off is now
treated as absent.
New fields: cad (v1.17+), flood.max.unscoped and flood.max.advert (both
present since v1.16, previously just undocumented upstream).
New action: poweroff (v1.17+). Unlike reboot it cannot be undone over the
mesh, so it takes a typed repeater name rather than a confirm(), checked
server-side as well as in the UI.
Telemetry: the MCU temperature v1.17 appends to channel 1 is labelled as
silicon rather than as an environmental reading. Absent stays absent —
boards without a usable sensor emit nothing, and a zero would be a lie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tapping the navbar channel selector focused a text input, so Android
raised the on-screen keyboard on every channel switch — half the screen
covered for a list that is four or five items long. The type-to-filter it
enabled solved a problem nobody has: the firmware caps how many channels
a device holds, so there are never enough to search.
#channelSelectorInput becomes #channelSelectorButton, a <button> carrying
the same .form-select chrome, and click toggles the dropdown. The list
itself is unchanged (previews, unread badges, favourites, muted styling),
so renderChannelDropdownItems() just loses its query parameter and the
channels.dropdown.no_matches key goes with it. Keyboard access is kept:
Enter/Space opens, arrows move, Enter selects, Escape closes — the
keydown handler now bails out while the dropdown is closed so Enter
performs a plain button activation instead of picking the first row.
Verified locally at 360px: focus lands on the button (never an editable
element), open/toggle/select/outside-click/Escape all behave, 0 console
errors, and the control renders identically in light and dark themes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The path dialog could send a node back to FLOOD or push a configured
path, but Direct — the firmware's third mode, zero hops — had no button;
getting there meant clearing the path from the Console.
A new POST /api/contacts/<key>/paths/set_direct sends an empty path hex,
which makes the firmware store out_path_len = 0, the state
_format_path_display() already renders as "Direct". Configured paths are
untouched, same as Restore FLOOD.
The button is wired in both places paths are managed: the DM contact info
dialog and the My Repeaters path modal. Both button rows get flex-wrap so
the third button wraps instead of overflowing at 360px.
Verified against the local test device: the endpoint moved a repeater
from 5E34→EE6C to Direct and back, and both dialogs render correctly in
en/pl at 1280px and 360px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restarting the node meant opening the Console and typing `reboot`, or
reaching the hardware. Device > Info now carries the same action as a
button, with a confirm that says what it costs: the device leaves the
mesh for a few seconds and anything addressed to you in that window can
be lost.
POST /api/device/reboot wraps the DeviceManager.reboot_device() the
console command already used. The firmware acknowledges nothing and the
companion link goes down with it, so a 200 means "command written", not
"device is back" - the reconnect is _on_disconnected()'s job, and it
takes about five seconds over TCP.
The button is rendered by loadDeviceInfo() rather than sitting in
base.html, so it only exists on the branch where the device answered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some faults cannot be diagnosed from a screenshot on a server we have no
access to. The one that prompted this: a sent channel message gets no
repeater badge, and nothing in the database distinguishes "nobody in
earshot repeated it" from "the repeat came back over the air but the
firmware dropped the RX-log frame before the app ever saw it".
Settings > Diagnostics records a bounded window into a zip: every RX-log
frame, the echo correlator's decision (with the pending-send list that
explains a non-match), what each send expected to hear back, the
application log at DEBUG, and the device's own packets.recv counter at
both ends of the window. That last comparison is the whole point - it
turns the firmware's silent 4-frame queue overflow into a measurement
instead of a hypothesis.
scripts/diag_report.py reads a capture offline and prints the verdict.
Captures can be downloaded, or uploaded to a Zipline instance once the
user pastes a token handed to them out-of-band. The URL ships as a
default; the token never does, because this repository is public.
- Hot path follows observer.py's contract: one deque append, no locks,
no I/O. The log handler uses a no-op lock object rather than
lock = None, which raises under Python 3.13's `with self.lock` in
Handler.handle() - latent on the 3.11 container, fatal on 3.13
- Caps: 5/15/30/60 min, 25 MB, 10 stored captures. All enforced by the
writer thread, so a user stop and an auto-stop finalise identically
- The capture contains the text of messages received while it runs; the
UI states this and confirms before any upload
Also drops a redundant function-local `from flask import current_app` in
get_status(): it shadowed the module-level import for the whole function
and broke /api/status as soon as anything referenced current_app above
it. Found by the capture tool, in its own change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also documents the two behaviours the release adds to Resend that only
whatsnew had covered: the stale-message confirmation, and that a resend
now repairs a message left with no delivery badge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab read fields the firmware never sends. get_stats_core/radio/packets
return uptime_secs, queue_len, tx_air_secs, rx_air_secs and recv; the tab
looked for uptime, queue_length, tx_air_time, rx_air_time and received, and
every one of those rows silently dropped out - leaving battery, errors and
Packets TX next to the database counters. The `stats` console command reads
the same payload with the right names, which is why it showed more.
Fix the names and add what was never shown at all: last RSSI, last SNR, noise
floor, the flood/direct split on both packet counters, RX errors, and channel
utilization. The rows now sit in four titled groups - System Information,
Radio Statistics, Packet Statistics, Database - reusing rptmgmt.status.* for
the labels the repeater status view already had, so the local device and a
remote repeater read the same way. Airtime becomes a duration ("8h 31m")
rather than minutes, for the same reason.
Only device.stats.database is a new key. The user guide's description of this
tab listed free memory and a forwarded counter, neither of which exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also corrects the app-vs-PWA table in the user guide: it claimed QR
scanning and file downloads were unavailable in the Android app, which
contradicted the Android App guide from the start - both have always
worked there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A thin WebView wrapper that opens a user's own mc-webui instance full
screen, without the browser address bar. It asks once for the server
address and remembers it; all logic stays on the server.
- android/mc-webui-wrapper.apk (1.0, it.wojtaszek.mc.wrapper, minSdk 21)
- docs/android-app.md: download + checksum, "unknown sources" permission,
the Play Protect notice, first connection, and the limitations that come
with a WebView (no notifications, no QR camera, no file downloads)
- README, user guide and whatsnew entries, incl. an app-vs-PWA comparison
- *.apk marked binary so the text=auto rule can never touch it
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Quote button produced `@[user] »text« ` — a shape mc-webui invented back
when it could not send newlines, and one that other MeshCore clients neither
write nor understand. It now writes the quote on its own line behind a '>'
and leaves the cursor underneath it, matching the plain-text convention some
users already type by hand.
processQuotes() styles both syntaxes, so messages sent by older builds keep
their formatting. The '>' match is anchored to a line start or a leading
@[mention] badge and looks for the escaped '>', so neither "5 > 3" in
prose nor the brackets of generated tags can trigger it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
user-guide: the Map view now opens showing only the selected route, with
the two opt-in checkboxes (All repeaters, Alternative paths) described
alongside how alternatives are coloured and why only their diverging
stretches are drawn. The Filters section gains a paragraph on settings
being remembered per browser, including the deep-link exception.
architecture: added map layer ordering, the segment de-duplication that
keeps alternatives visible, the no-refit re-render on toggle, and the
localStorage filter persistence contract (user-driven writes only, skip
restore on deep link).
whatsnew: three user-facing entries under the pending release section.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cover the two changes on dev since the last main merge (c6d2367):
- d8d3427: clicking a route under a channel message now opens the Path
Analyzer map deep-linked to that message + echo (user-guide new Group
Chat Message Routes section, Path Analyzer to-open note, whatsnew New
features, architecture deep-link URL contract).
- bb67c4d: exact sent-message echo matching fix (whatsnew Reliability).
Also finalize the previously-merged Unreleased-since-debb711 section by
dating it 2026-07-22 (the c6d2367 merge), and open a fresh
Unreleased-since-c6d2367 for the current changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Path Analyzer now documents its fourth view (Routes — consecutive
hop-segment stats with an "as path end" count) and the `>`-chained
sequence filter, the map's instant shortest-route draw and channel
label, and the mobile collapsible filter bar / 45vh map. My Repeaters
gains the Settings -> Location "Pick from map" picker and the
saved-password prefill on login retry; architecture.md adds the new
GET /api/repeaters/<pk>/password endpoint and a four-views note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the Path Analyzer map descriptions in user-guide.md and
whatsnew.md for the red numbered/name-labeled path points and the
reversible candidate assignments (per-hop undo + Reset picks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- user-guide.md: new Path Analyzer section (opening, filters, the
Messages/Repeaters/Map views, phone layout notes, local-view caveat);
TOC entry; per-item placement action count 12 -> 13 with Path
Analyzer added to the enumeration
- architecture.md: /api/path-analyzer/messages added to the Messages
endpoint table plus a Path Analyzer design section (batched echo
fetch, client-side filtering rationale, SNR attribution, hash ->
contact resolution, legacy-row degradation)
- whatsnew.md: three user-facing bullets under Unreleased (the tool,
the combined filters, phone support)
No deploy notes needed: no new dependencies, no schema changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
user-guide.md gains a full My Repeaters section (adding repeaters,
saved passwords + the wrong-password/unreachable ambiguity, path
editing, and the six management tools) and fixes the stale Quick
Access placement list (now 12 items, real labels). architecture.md
documents the /api/repeaters/* endpoint family with its shared error
mapping, the repeaters DB table, and a DeviceManager subsection on
serialization, login sessions, CLI reply correlation, settings
batches and the actions whitelist. whatsnew.md adds three New
features bullets, a Reliability note (console login reports role),
and Deploy notes (meshcore>=2.3.7 rebuild, plaintext password
trade-off). rpt-mgmt.md now points to the built-in panel first.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
user-guide.md gains the Observer tab section (master switch, IATA code,
scheduled flood adverts, broker management, live badges) and the updated
Settings tab list. architecture.md documents the ObserverManager design
(hot path, per-broker paho clients, wire-format fidelity, reload without
restart), the observer_brokers table, the /api/observer/* endpoints and
the observer_status socket event. whatsnew.md announces the feature and
notes the paho-mqtt dependency (image rebuild via standard mcupdate) and
plaintext broker credentials.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings the docs up to date with the two undocumented batches on dev since
the last docs commit (d16093b). whatsnew.md is split by ship date: a
2026-06-26 section (already live on main) gains the raw channel-message
resend feature, the Edit-message button rename, and the log-loop /
connection-badge / failed-reconnect fixes; a new 2026-07-02 section covers
touch-device explicit send, DM line-break preservation, and the Letsmesh
analyzer becoming a normal editable row. A fresh Unreleased(since debb711)
opens on top.
architecture.md documents the channel_messages.raw_packet column, the
POST /api/messages/<id>/resend endpoint, the new /api/status fw/resend/
path-hash fields, the DeviceManager raw-resend + liveness-watcher
behavior, the werkzeug log-filter, and the analyzer seeding change.
user-guide.md adds a Message Actions section (Edit/Resend), the touch
Enter-vs-Send note in channels and DM, and rewrites the Analyzer tab to
reflect Letsmesh-as-normal-row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-facing summary of changes since fd2b3d0 grouped into New features
(custom analyzers, apply-path button, DB Optimize button, auto retention,
sluggish-device watchdog), Reliability & polish (polling-only fix for
load freezes, channel cache, scope-key refresh, multi-byte path
rendering, TCP self-heal, region scope on slots >7, console parser),
and a deploy note about restarting the host watchdog. Also linked from
user-guide.md's Getting Help section. File is meant to be refreshed
before each merge to main.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-guide: new Region Scopes section (registry CRUD, per-channel picker,
firmware v1.15 default), updated Switching Channels (searchable picker on
narrow screens, sidebar previews on wide), Settings Regions tab, path_hash_mode
in Device tab.
Architecture: regions/channel_scopes tables, /api/regions and /api/channels/scopes
endpoints, per-channel scope-key push under _send_lock in DeviceManager,
path_hash_mode field in /api/device/config, channel POST/join idempotency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduces a dedicated conceptual walkthrough of device contacts, the
cache layer, the ignored/blocked flags, Settings → Contacts toggles,
recommended configuration, scenarios, recovery from the 350 limit,
auto-cleanup interaction, and FAQ/migration notes for users coming
from the official Android/iOS apps.
Also ships a standalone, responsive HTML version with embedded CSS
(light/dark auto) for sharing outside the repo, and links the guide
from user-guide.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catch up on ~36 commits since b60c99a. Document Device Settings tab
(public info + radio) with map picker and regional presets, quick-access
FAB cluster with drag/collapse/sizing, configurable route popup and toast
timeout/position, multi-arch Docker images (amd64, arm64, arm/v7), and
new endpoints /api/device/config and /api/ui/settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add new docs to README table (DM retry logic, BLE pairing guide), update
architecture diagram and DB schema for BLE/delivery tracking, rewrite DM
retry settings section in user guide to reflect 4-scenario matrix, add BLE
troubleshooting reference, and update .claude/context files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update all documentation to reflect features added since last doc update:
- README: new features list, gallery screenshots, development status
- User Guide: global search, console commands, device dashboard, settings,
system log, backup, updated DM and contact management sections
- Architecture: complete API reference, WebSocket namespaces, updated
project structure and database tables
- Troubleshooting: remove v1 bridge references, add UI-based backup,
system log references
- Gallery: add 4 new screenshots (search, filtering, settings, system log),
update 12 existing screenshots
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The MeshCore community uses "companion" not "client" for type 1 nodes.
Rename the CLI label to COM across all UI, API, JS, and docs to align
with official terminology. Includes cache migration for old CLI entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add clickable "?" icon on DMs without ACK, showing a popup
explaining that delivery is unknown (mobile-friendly).
Update README, user guide with new features (Analyzer links,
DM delivery tracking).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The scheduler now uses the timezone configured in .env (TZ variable)
instead of hardcoded UTC:
- Add get_local_timezone_name() helper to manager.py
- BackgroundScheduler uses system local timezone
- API returns timezone field in cleanup-settings response
- Frontend displays timezone next to hour selector and in status text
- Updated documentation to reflect timezone behavior
This makes the cleanup hour more intuitive for users in non-UTC timezones.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Allow users to select the hour (0-23 UTC) when automatic contact
cleanup runs:
- Add hour selector dropdown in Advanced Filters (disabled until enabled)
- Hour field saved to .webui_settings.json with cleanup_settings
- API validates hour (0-23), scheduler uses CronTrigger with hour param
- Status text shows configured hour (e.g., "Enabled (runs daily at 03:00 UTC)")
- Documentation updated in user-guide.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Simplify README.md from ~925 to ~217 lines
- Create docs/user-guide.md with detailed feature documentation
- Create docs/architecture.md with technical details and API reference
- Create docs/troubleshooting.md (merged from COMMON_ISSUES.md + README)
- Move DOCKER_INSTALL.md to docs/docker-install.md
- Remove COMMON_ISSUES.md (content merged into troubleshooting.md)
- Add Documentation section with links to all docs
The README now focuses on quick start and installation,
while detailed documentation is organized in docs/ folder.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>