The dropdown already rendered a .sidebar-unread-badge element, but the
styling rule was scoped to .channel-sidebar-item, so on narrow screens the
count fell back to plain text in the row's own colour. Extend the selector
to .channel-selector-item so both views share one badge style.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tabs open made every endpoint answer in ~20s, which two sessions read as
contention on a shared server-side lock. It wasn't the server. With 3 tabs open,
/health - which touches neither the database nor the device - measured a 14.7s
median from inside a tab and 11ms from a client outside the browser at the same
instant. The server was idle throughout.
Socket.IO clients pinned transports: ['polling'], upgrade: false, so each tab
held one HTTP connection open for its whole lifetime. Browsers allow six
concurrent HTTP/1.1 connections per origin, shared across every tab, so three
tabs consumed the pool and everything else queued in the browser waiting for a
free connection. /proc/net/tcp in the container confirmed it: pinned at exactly
6 established connections, unmoving.
The pin dates from 1d47c9c, when werkzeug had no WebSocket support and every
upgrade attempt returned HTTP 500. python-engineio==4.8.1 (pinned five weeks
later, in d3590f9) pulls in simple-websocket, which fixed that; the workaround
had outlived its premise. Drop it and use the default transports, which open on
polling and upgrade. A WebSocket is not part of the HTTP pool, so the pool is
released. Where the upgrade is blocked - a proxy that drops the Upgrade header -
the client stays on polling by itself, which is exactly today's behaviour.
Measured with 3 tabs, in-page medians: /health 14664ms -> 12ms, /api/status
19282ms -> 64ms, both now matching what the same probes read from outside the
browser. All three tabs report transport "websocket", server pushes still
arrive over it, and the log no longer fills with "Session is disconnected"
(0 occurrences, 0 tracebacks across the run).
Also corrects the earlier diagnosis in the docs: the per-endpoint timings that
looked like a lock were measured request->requestfinished in the browser, which
includes connection-queue time, so every endpoint flattened to the same figure
regardless of its own cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/api/status is polled by every open page (on load, every 60s, and on
visibility resume) and cost ~266-450ms in-container while doing no device
I/O at all — check_connection() is just an attribute read, and /health
returns in ~1ms, so all of it was SQLite.
Two calls were over-fetching:
- get_stats() ran COUNT(*) over 11 tables to answer a question about 2.
COUNT(*) FROM echoes alone was ~200ms over 19k rows and the result was
discarded. That table only grows, so the endpoint kept getting slower.
- get_channel_messages(limit=1) was a SELECT * fetching every column,
raw_packet included, to read one timestamp. ORDER BY timestamp has no
usable index (idx_cm_channel_ts leads with channel_idx), so the plan
was a full SCAN through two temp B-trees. MAX(timestamp) is served off
that index as a covering scan instead.
Replaced with Database.get_status_summary(): three scalar subqueries on
one connection. ~371ms -> ~46ms for the DB work; endpoint median ~57ms
in-container, ~78ms from the page. Response verified byte-identical.
This does not address the multi-tab congestion — at one poll per tab per
60s /api/status was never that cause — but it removes a recurring cost
and re-baselines the probe those measurements are taken with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Testing after the batching fix showed three tabs still push /api/status from
~600ms to a ~13s median, from a different cause (every endpoint degrades
together under load, so a shared lock rather than a request storm). The entry
claimed multiple windows were no longer a problem; scope it to the load
reduction that was actually measured.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every incoming echo triggers a sweep of the whole rendered message list, and
refreshMessagesMeta() awaited one /api/messages/<id>/meta per message inside
the loop. Messages that never gain a route (nothing heard them) never stop
qualifying for the sweep, so the same ~180 messages were re-fetched every few
seconds: 7,500 requests in nine minutes on a single tab, each opening its own
SQLite connections. The single-threaded werkzeug server — the same one
production runs — had no room left for anything else, so the UI hung on
"Loading messages..." / "Connecting..." while the device was in fact connected.
Add GET /api/messages/meta?ids=... resolving the whole sweep with a handful of
queries, batching the row and echo lookups, and have the client collect ids
first and fetch them in chunks. The per-message endpoint stays for forced
single refreshes; both now share _build_message_meta() and the existing
_build_channel_secrets / _get_row_pkt_payload helpers, so the payload is
unchanged (verified byte-identical against the old response).
Measured on the local container, 500 messages rendered / 181 needing meta:
one 273 ms request in place of 181 sequential ones at ~82 ms each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The chat view only ever grew by socket push, so anything that arrived
while the connection was down was never drawn. Android tears the
connection down behind a locked screen, and the wrapper keeps the same
page alive for days, so the list stopped at the last message that got
through until the app was force-stopped. A browser tab hid the same bug
by reloading the page on resume.
Every way back from a gap now re-reads the list from the server: the
socket reconnecting, the page becoming visible after more than a glance
away, a heartbeat noticing its own tick arrived far too late (the page
was frozen), a new Refresh item in the menu, and window.__mcAppResumed,
which the wrapper calls from onResume since a WebView is not guaranteed
to report the page as hidden at all. Direct messages get the same
treatment.
Verified in Chrome against the local container: all five triggers fire a
resync, with no page errors and the list intact afterwards.
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>
The signed release build of the notification work from 954e99b, signed
with the same key as 1.0 (certificate SHA-256 425857b3...d230), so it
installs straight over the previous version and users keep their saved
server address.
Verified against the packaged APK: versionCode 2 / versionName 1.1,
POST_NOTIFICATIONS present, and assets/notification_shim.js included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Android's WebView ships no Web Notifications API, so window.Notification
was undefined, mc-webui detected that and greyed its toggle out as
"Unavailable". The app also declared no POST_NOTIFICATIONS and created no
channel, so it never even appeared in Android's notification settings.
A shim injected at document start puts window.Notification back and
forwards it to a @JavascriptInterface bridge that posts through Android's
NotificationManager. mc-webui itself is untouched - the page keeps using
the standard API.
- onPageStarted is early enough: mc-webui reads the permission on
DOMContentLoaded, a whole parse and script pass later
- web permission states map onto Android's, with
shouldShowRequestPermissionRationale separating "ask again" from
"blocked for good" after a refusal
- tags replace notifications the way the web API expects; a tap returns to
the running app (singleTop) and fires the page's onclick
- notifications only arrive while the process is alive, same as the PWA
versionCode 2 / versionName 1.1. Verified: debug and release both build
clean (lintVitalRelease included), shim and drawable land in the APK, and
the shim's contract is covered by a Node harness against a fake bridge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the debug APK with a release build signed with the project key
(CN=Marek Wojtaszek, valid to 2081, v1+v2 schemes, not debuggable), so
every later version installs over this one instead of forcing a reinstall.
Docs catch up with what the app can now do: QR scanning works on https
instances, downloads land in the phone's Downloads folder, and the address
form is a non-destructive screen you can reach deliberately. New SHA-256,
size, permission list and the signing fingerprint to verify against.
Also silences the Kotlin warning for the deprecated shouldOverrideUrlLoading
overload kept for Android 5.x - source-only, the APK is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wrapper is what users sideload, so its source belongs next to the APK -
they can read what they install, or build it themselves.
Behaviour fixes on the way in (APK rebuild pending):
- The saved server address survives. Back on the first page and connection
errors used to delete it, so a stray tap or a moment without signal meant
typing the address again; the form now opens pre-filled and only a save
replaces what is stored. Back at the top level asks: exit, change server,
or cancel
- QR scanning works: the page's camera request is mirrored to an Android
permission request (CAMERA, on an https instance - getUserMedia needs a
secure context, as in any browser)
- Downloads work: a DownloadListener hands database backups and other files
to DownloadManager, which puts them in the phone's Downloads folder
- Links to other hosts and non-http schemes open in the system browser, so
a URL in a message no longer navigates the app away from the instance
- Rotating the screen no longer reloads the page
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>
Drop the -dev suffix and date the whatsnew section so release.sh can find
its notes. Also make the build-string example in the whatsnew header read
as an example, instead of naming one particular build that goes stale the
moment it ships.
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>
The app already knew exactly which build was running - a calendar version
of commit date plus short hash - but nothing gave a release a name users
could quote, and the repo had no tags at all.
Adds a VERSION file as the single source of truth for a SemVer release
number, read by app/version.py alongside the existing build string rather
than replacing it: the number is for people, the build is for pinning down
a deploy, and both ship in /api/version and the template context. The menu
shows the release first with the build underneath. #versionText still holds
the build string, because the remote-update poller compares it to detect
that the server came back on a new build.
Resolution order is unchanged (frozen file > git > fallback), and a frozen
file written before this change still yields a correct release number, so
an already-deployed server does not need re-freezing to stay sane. The
container has no git, hence COPY VERSION into the image - otherwise a plain
'docker compose build' reports 0.0.0.
scripts/release.sh cuts a release from main: it refuses a dirty tree, a
wrong branch, a malformed number or an existing tag, extracts the notes
from the matching whatsnew section, then tags, pushes and publishes via gh.
Numbering starts at 2.1.0 rather than 1.x: the v2 line has been in
production since March and 'v1' is the archived pre-migration branch, so
1.x would have been ambiguous. Sections in whatsnew before 2.1.0 keep
their date-only headings - they were never tagged.
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>
- Rename "meshcli Console" to "mc-webui Console" (modal title + docs).
- Drop redundant "Connected to..." messages; replace intro with a one-line "Type 'help' for available commands." hint.
- Use a teal device-name style so the header label is readable on the dark background.
- Display contact paths with commas (D1,90,05,54) instead of arrows in `contacts` and `path`, matching the standard MeshCore client.
- Fix `change_path`: previously read only args[2] after shlex split, silently writing a 1-byte path. Now joins remaining args, accepts comma/space/continuous-hex, validates hex, auto-deduces hash_size from comma-chunk length (1/2/3-byte hops), and routes through _change_path_async so path_hash_mode is set and the contacts cache is invalidated.
- Update `help` line and add a usage hint for the no-args form.
- Add capped persistent output transcript: GET/POST/DELETE /api/console/output (cap 500 entries). Console restores prior entries (faded) above a divider on open and exposes a trash button to clear it.
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>
bluetoothctl info auto-connects to trusted devices, stealing the
connection from Docker — document hcitool as safe alternative and
add connection loop recovery steps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MC_BLE_PIN was non-functional — bleak in Docker cannot perform
interactive pairing (no BlueZ agent). Pairing must be done on
the host before starting mc-webui. Added comprehensive pairing
guide at docs/meshcore_bluetooth_pairing.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Explains the 4 delivery scenarios, how settings map to behavior,
why actual wait times can exceed configured intervals (firmware
suggested_timeout), and what to look for in the System Log.
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>
Root cause is device firmware contact table being empty (after reflash
or reset), not the v1→v2 migration itself. DB retains hundreds of
contacts from advert history but device only has those explicitly added.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents breaking changes including the need to re-add DM contacts
to the device firmware table after migration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add meshcore>=2.2.0 for direct device communication (Phase 0.1)
- Exclude docs/v2/ from git (local working notes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create v2 branch for mc-webui direct device communication migration
- Add docs/v2/STATUS.md for development progress tracking
- Exclude PRD documents from git (local-only planning docs)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement a smart auto-detection and low-level fcntl ioctl reset mechanism for LoRa USB devices. This 'last resort' recovery is triggered if the meshcore-bridge container fails to recover after 3 restarts within an 8-minute window. Includes updates to the installer, systemd service, and newly added README.
Co-Authored-By: Gemini CLI <noreply@google.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>
Documented the bridge crash-loop scenario where the MeshCore device
serial port connects but firmware doesn't respond to commands,
including symptoms, what doesn't help, and the fix (re-flash firmware).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>