Files
mc-webui/docs/whatsnew.md
T
MarekWoandClaude Opus 5 f82ee981b2 fix(docker): ship the compose file for image installs so HTTPS works there too
The optional HTTPS proxy is defined in the repo's docker-compose.yml behind the
"https" Compose profile, which only git-checkout installs have. Docker Hub /
GHCR installs write their own compose file from a README heredoc that had no
npm service, so COMPOSE_PROFILES=https matched nothing and started the app
alone - exit 0, no warning. The same gap swallowed MC_TRUST_PROXY,
MC_BIND_ADDRESS and MC_DEMO*, which the compose file passes to the app.

Add docker-compose.image.yml - the image-based counterpart of
docker-compose.yml, maintained here and downloaded instead of hand-copied, so
the npm service and every variable stay in sync. The image tag becomes
MC_IMAGE in .env so the file itself needs no editing and can be re-downloaded
when something is added to it.

Docs: README Option A downloads the file (and .env.example) instead of pasting
one, and switches tags via MC_IMAGE; https-setup.md Step 1 splits into the two
install paths, tells image installs to refresh the file, notes that
MC_TRUST_PROXY / MC_BIND_ADDRESS need it too, adds the curl for
npm-default-site.conf (no docker/ folder there) and a troubleshooting row for
"only mc-webui starts, no error".

Verified with `docker compose config` in a scratch project directory: the old
hand-written file plus COMPOSE_PROFILES=https in .env resolves to mc-webui
only, the new file to mc-webui + npm with MC_TRUST_PROXY passed and port 5000
bound to 127.0.0.1; `config --images` lists both images, so update-image.sh
pulls and recreates them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:18:36 +02:00

88 KiB
Raw Blame History

What's New

User-facing summary of changes since the last main release. Maintained on dev and finalized before each merge to main.

Releases are numbered MAJOR.MINOR.PATCH from 2.1.0 onward and tagged on GitHub — MAJOR when a deploy needs manual action from you, MINOR for new features, PATCH for fixes only. Sections before 2.1.0 are dated only; the project wasn't tagging releases yet. The exact build running on your server (a date and commit, like 2026.07.26+95d96ec) is still shown in the menu under the release number, for pinning down a specific deploy.

For deep technical notes, see architecture.md. For the full git history, run git log.


Unreleased

Fixes

  • Optional HTTPS now works on installations made from the Docker image, not only on those built from source. mc-webui can be installed two ways, and they differ in one detail that turned out to matter: an installation built from the repository uses the docker-compose.yml that comes with it, while an installation from the published Docker image uses a docker-compose.yml that the README told you to write yourself. The optional HTTPS proxy (HTTPS Setup) is defined in the project's file behind a Compose profile, so adding COMPOSE_PROFILES=https to .env starts it — but a hand-written file never had that definition, so the same line did nothing at all: the app started as before and no proxy was mentioned, with no error to explain why. Pasting the proxy into the file by hand worked, and then had to be redone by hand every time anything else was added to it. The same gap silently swallowed MC_TRUST_PROXY and MC_BIND_ADDRESS — the two settings that let the app see the real client address and close the plain-HTTP port — because those are passed to the app by the compose file too.

    The compose file for image installations is now maintained in the repository as docker-compose.image.yml and downloaded rather than typed, so it contains the proxy, both proxy-related settings, and demo mode, exactly like the one used when building from source:

    cd ~/mc-webui   # the folder with your docker-compose.yml
    curl -fsSL https://raw.githubusercontent.com/MarekWo/mc-webui/main/docker-compose.image.yml -o docker-compose.yml
    docker compose up -d
    

    If you installed from the image, that command is also the upgrade path: it replaces your compose file with the current one, which is safe because none of your settings live there — they are all in .env, which the download does not touch. Keep the file unedited from now on and the next addition to it is one download away again. The image tag moved into .env for the same reason: MC_IMAGE=mawoj/mc-webui:dev selects development builds, where previously you edited the tag inside the compose file — so if you had edited it to follow dev, add that line to .env when you replace the file, or the next pull brings the stable image back. Installations built from source are unaffected.


2.18.1 — 2026-09-11

Fixes

  • A renamed device now shows its new name straight away, and Resend no longer repeats a message under the old one. Saving a new name under Settings → Device renamed the radio at once, so everyone else saw your messages under the new name, but the interface kept using the old one until the container restarted — and a page left open kept it even then, until it was reloaded. It showed in the navbar and above every message you sent, and, worse, in the copy the interface keeps of each sent message so that Resend can put exactly the same packet back on air: that copy was built with the old name, so resending a message broadcast a second packet, which everyone else received as a duplicate from your old name. The same mismatch kept the repeater list from appearing under messages sent after the rename. The interface now reads the name back from the radio the moment it is saved, and every page you have open, in any browser, switches the navbar and your next messages to it without a reload; set name in the console behaves the same. Messages sent between a rename and this update keep the name they were stored with, so resending one of those still repeats it under the old name. A name can also be at most 31 bytes long, which is all the radio keeps: a letter such as ł takes two bytes and an emoji four, so a longer name is shortened on a whole character, and the Settings field shows the name as it was kept.

2.18.0 — 2026-09-09

Features

  • One-click updates now work without cloning the repository. mc-webui can be installed two ways: by pulling the ready-made Docker image, or by cloning this repository and building it. The ↻ button next to the version in the menu has always told both kinds whether a newer version exists — but Update now only ever appeared for a repository installation, because the small service behind it was installed from the scripts/ folder, which an image installation does not have. That folder is no longer needed: one command installs the service and downloads what it needs, and the whole thing can be removed the same way.

    cd ~/mc-webui   # the folder with your docker-compose.yml
    curl -fsSL https://raw.githubusercontent.com/MarekWo/mc-webui/main/scripts/updater/install.sh | sudo bash
    

    The update it then performs matches the installation it found: an image installation pulls the newly published image and recreates the container, a repository installation pulls the commits and rebuilds, exactly as before. Nothing changes for anyone who already has the updater installed from a checkout. The container watchdog — which restarts the container when it stops responding, and power-cycles a frozen USB device — installs the same way now, from scripts/watchdog/install.sh at the same address.

  • The line under an incoming channel message now names the region it was sent in, and no longer spells out the route. A MeshCore sender can stamp a packet with a region — a flood scope such as pl that only repeaters allowing it pass on (see the previous release). The stamp is not the name but a 16-bit code derived from the region's key, so the interface can recognise it only for regions you have added under Settings → Channels: a message stamped with one of them now ends its line with Region: pl, while an unstamped message, or one from a region you have not configured, shows nothing extra. The route part changed too. It used to print the first route inline (SNR: -5.5 dB | Hops: 4 | Route (3): 142C→1122→271C→EE6C), which with 2- or 3-byte hashes crowded everything else off a phone screen. It now only says how many copies of the message were heard and how wide their per-hop hashes are — (3) 2B, three copies with 2-byte hashes — with the size coloured red, orange or green for 1, 2 or 3 bytes, and tapping it opens the same popup as before. The words Hops, Route and Region are gone as well, replaced by a small antenna, signpost and map-pin icon, each naming itself when hovered or long-pressed: spelled out, the line took two rows on a phone and pushed the messages apart. The SNR and hop count stay where they were: they describe the copy your node decoded, arrive with the message itself, and so are there even when no repeat was heard. The popup is now complete: it lists every copy in the order heard, including one that came Direct from the sender with no repeater in between (previously left out, which made "Hops: 0" under a message look wrong next to popup entries with one hop), and marks the copy your node actually decoded with ✓ received. The whole line is also a size larger and darker, since it was hard to read on a big screen. The region stamp is kept from this version on, so messages received before the update have no region to show.

Fixes

  • An update that had nothing to install no longer claims it timed out. Pressing Update now watched for the version to change and gave up after two minutes if it did not — so an update run that legitimately found nothing new ended in "Update timed out. Please check server manually." That was the normal outcome shortly after a release, while the Docker image for the newest commit was still being built. The update now reports what it did, and the screen says so plainly: that the change is on GitHub but its image is still being built, or, for an installation built from source, that it was already on the newest commit. It appears the moment the run finishes, instead of two minutes later, and no longer wears a red cross.

  • A failed request no longer shows a programmer's error message. When something in front of the app answered instead of the app — the login page of a reverse proxy or a service like Cloudflare Access after the session had expired, or a gateway error page — the interface tried to read that web page as data and showed what the attempt threw: SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON. It now says what actually happened, and what to do about it: that a login page answered and the page needs reloading and signing in again, or that something in front of the app answered, with the HTTP status quoted for anyone diagnosing it. Requests are read the same way everywhere now, so a genuine error from the app itself keeps its own message — a repeater that did not answer in time still says exactly that.


2.17.0 — 2026-09-07

Features

  • Repeater regions can now be managed from the interface, not just the CLI. A repeater decides which flood-scoped packets it passes on from its own list of regions — a name like pl that a sender stamps on a packet, which only repeaters allowing that region will forward. Setting that up meant typing region commands into the console and reading back an indented text tree. My Repeaters → Settings → Regions now shows that tree as a list: each region with its Flood allowed / Flood denied state, nested regions indented under their parent, and the home region marked. A ⋮ menu on each row allows or denies flood, sets the home region, moves the region somewhere else in the hierarchy (previously only possible by deleting it and adding it again), or deletes it, and Add region takes a name plus an Inside picker, so a region can be nested inside an existing one or left at the top level. The wildcard row at the top — Packets without a region scope — is the rule for ordinary unstamped packets, and can be switched like any other. Below the list, Default scope sets the region this repeater stamps on packets it sends itself.

    Changes behave the way the firmware does, and the screen is explicit about it: adding, deleting or switching a region takes effect at once but lives only in the repeater's memory, so an orange banner appears until you press Save, and leaving with unsaved changes asks first. Without that save the repeater forgets them at its next reboot. The Default scope card is the exception — the firmware stores that one by itself, and the card says so.

  • The Path Analyzer's map can be maximized to the whole screen. On a phone the map view splits the screen between the message list and the map, leaving the map about 45% of the height — enough to know a route was drawn, not enough to read it. The ⛶ button in the map's top-right corner now expands the map over everything else, the panel header included, so a five-hop route across half of Poland is legible without pinching. The button turns into ✕ Close while maximized, Escape does the same, and either way the route you had drawn and the area you had panned to are still there when the normal layout comes back. To make room for it, the All repeaters and Alternative paths checkboxes moved from the map's top-right corner to the bottom-left.


2.16.0 — 2026-09-07

Features

  • Notification profiles — a channel can now alert you only about the messages that matter. Until now a channel had two settings: everything or nothing. A busy regional channel therefore either buzzed all day or went silent, and the one message that named you got lost among the rest. Under Settings → Notifications you can now define profiles: a named set of rules such as mentions my name, message contains "webui" or sender name contains "Kosu", with a choice of whether any rule or all of them must match. Then, in Manage Channels, the bell next to a channel opens a small menu instead of flipping a switch — Off, Every message (what you had before) or one of your profiles by name. A channel on a profile counts only matching messages: the red unread badge next to its name shows just those, the bell in the navbar and the app-icon badge add up just those, and the browser or Android notification is posted just for those. Everything else still arrives in the chat as before, it simply no longer asks for your attention. Matching ignores letter case and diacritics, so krakow finds Kraków, and a mention is your device name anywhere in the text, whether typed as @[name] or plainly. Direct messages are untouched: a message addressed to you personally always notifies. Deleting a profile puts the channels that used it back on Every message — louder, never quieter, is the safe direction to fail in.

Android app

  • Notifications now keep arriving while the app is in the background. Until now they went quiet a few minutes after you switched away, and no setting on the phone fixed it — Android freezes an app that has nothing running in the foreground, and a frozen app holds no connection to your server and runs no code to alert you with. "Unrestricted" battery use never helped because it exempts an app from Doze, which is a different mechanism. The app now asks Android to leave it running, which Android grants in exchange for a permanent, silent notice in the shade — Listening for new messages. That notice appears only once you turn notifications on in mc-webui itself, so anyone who does not use them never sees it, and it disappears the moment you turn them off. Its Stop button ends it for the rest of the session, and it can be silenced permanently on its own in Android's notification settings, under Background delivery. Nothing new is sent anywhere: the connection held open is the one to your own server, and the alerts are still built by the mc-webui page itself — which is why mutes, blocked senders, channel names, translations and the tap-to-open-the-conversation behaviour all keep working exactly as they already did.
  • The address screen remembers the servers you have used. If you run more than one instance — your own and the public demo, say, or a test box next to a live one — switching between them meant typing the whole address again every time. The screen behind Back → Change server now lists the six most recent addresses under Recent servers; tap one and the app connects straight away, with the username and password saved for that server still applying, since those were always kept per address. The ✕ beside an entry forgets the shortcut without touching the server you are on or its saved login, and only addresses that actually answered are ever listed, so a typo never joins them.

Both need the new app version. They are changes to the Android app, not to mc-webui itself — the app updates itself from Google Play, or can be reinstalled from the .apk in this repository.


2.15.0 — 2026-09-05

Features

  • The Android app is on Google Play. mc-webui for Android is now published in the Store, so installing it no longer means downloading an .apk, granting the "unknown sources" permission and clicking past Play Protect — you tap Install like for any other app, and updates arrive on their own instead of waiting for you to notice them. Nothing about the app itself changed: it is the same build that has been in the repository, with the same permissions and the same lack of analytics, accounts or background services. The .apk stays published here for anyone who prefers to install it directly or wants to read what they are installing first, and both are signed with the same key — so if you already run the repository build, installing from Play updates it in place, keeps the server address and any saved login, and needs no uninstall. Moving back the other way works just as well. Keeping that single signing key through Play's own signing scheme was the one irreversible decision in the whole process, and it is the reason nobody has to set their server address up again.

2.14.0 — 2026-09-02

Features

  • Repeater settings are no longer read all at once when you open a section. Every field under My Repeaters → Settings is a full round-trip through the mesh, and expanding a section fired one for each field in it: seven questions to a repeater four hops away, answered one at a time, while you sat and waited for the one value you actually came for — and often for a batch that timed out halfway through and left half the section blank. Nothing is read now until you ask. Each field has its own read button beside it, so checking or changing a single setting costs a single round-trip; Read all fields at the top of a section still fetches the whole thing for when that is what you want. Clicking read on several fields in a row is fine — they queue and run one after another. The values you have read are also remembered per repeater, so a section opens showing the last readings rather than empty boxes; a remembered value carries a small clock beside its name and a note at the foot of the section saying when it was read, because the repeater may well hold something else by now. Those values stay editable — applying a setting writes an absolute value, so editing from an old reading still sets exactly what you typed — but the field is one click away from being current when it matters. Field hints moved into an ⓘ next to each field name, and every field has one now instead of the handful that did; the sections are considerably shorter for it.

  • The RX delay setting is now available under My Repeaters. Settings → Advanced offered the two transmit delays, txdelay and direct.txdelay, but not rxdelay, which the firmware has had all along — the only way to reach it was a raw CLI command. It is a different mechanism from the other two: instead of scattering retransmissions at random, it staggers them by how well the packet was heard, so a repeater with a strong copy repeats a flooded packet first and one that barely caught it waits, then skips the repeat entirely if a better-placed node has already done it. On a dense network that is the difference between one retransmission and five. The field accepts 0 to 20 and takes the reading you already had on the device; 0 turns it off, which is what the firmware ships with and what most repeaters should stay on unless there is a reason to change it.

Fixes

  • Updating no longer stops at a GitHub username prompt. mcupdate started halting on its very first step, showing Username for 'https://github.com': and then sitting there waiting for something to be typed — on every server at once, on the same day, with nothing having changed on any of them. It is an odd thing to be asked by a repository that is public and has never needed a login, and nothing was wrong with the installs: GitHub changed how its front end answers the second half of an anonymous fetch over HTTP/2, and the git that ships with Debian 12 — version 2.39, built against libcurl 7.88 — now gets a 401 back for a request that had always been allowed. Git reads any 401 as credentials, please and asks for a username, which is why typing one never helped and why a token would not have helped either. The update script now asks git to speak HTTP/1.1 for its pull, which GitHub still answers exactly as before, and it no longer waits on the keyboard when a fetch genuinely does fail — it reports the error and stops, instead of leaving an unattended update hanging indefinitely. If you are stuck on an older copy and so cannot update to pick this up, run git config --global http.version HTTP/1.1 once on the server and mcupdate will go through.

  • The same routing path can no longer be added to a contact twice. Both path editors — the one in a contact's info panel and the one under My Repeaters → Paths — took whatever they were given, so an identical route could end up in the list two or three times: added by hand, picked again from the repeater picker, or imported a second time from the device, which is the easiest of the three to do by accident because the button gives no hint that the path is already there. A duplicate is worse than untidy, because the list is a retry order: when a message fails, the app works down it, and a repeated entry simply retries a route that has just been shown not to work while the genuinely different alternative below it waits its turn. Adding a route that is already on the list is now refused with This path is already on the list, and the entry box stays open so the hops can be corrected. Two paths count as the same only when both the hops and the hash size match — the same bytes read as 1-byte hops describe a different route than as 3-byte hops, and both remain available. Duplicates already sitting in the list are collapsed into one when you update, keeping the starred copy if one of them was starred.


2.13.0 — 2026-08-30

Features

  • Neighbour lists can now be tidied up and refreshed on demand. A repeater's neighbour list under My Repeaters → Neighbors could only be read: nodes that had moved away or gone off the air stayed in it, and there was no way to ask the repeater to go looking for new ones — both are ordinary buttons in the standard MeshCore app, and both were missing here. A ⋮ menu next to Refresh now holds Discover neighbours, which has the repeater broadcast a zero-hop discovery request that nearby repeaters answer over the following minute, and Remove all neighbours, which empties the list after a confirmation. Single entries go through the list itself: selecting a neighbour — a row, or its marker on the map — opens a dialog with its pubkey prefix, SNR, last-heard time and position, with a Remove from list button under them. The dialog exists rather than a trash icon in each row because on a phone that row is already tight enough to truncate names, and a delete button at the edge of a scrolling list is the easiest thing on the panel to hit by accident. Everything here needs an admin login; a guest sees the same details, just no buttons. Worth knowing: the list belongs to the repeater, so a removed neighbour comes straight back the moment it is heard again — this tidies a stale list, it does not block anybody. While the panel was open, the neighbour table also stopped pushing the whole page sideways on a narrow phone when a node has a long name.

  • Notifications now say who wrote what, where — and tapping one takes you there. A new message used to produce the same line every time, New: 1 channel, with the app's own name on top and nothing else: not the sender, not the text, not even which channel it landed on. Tapping it did no more than bring the app forward, wherever it happened to have been left. Everything needed was already arriving with the message and simply was not being used. A channel message now shows the channel as the title and sender: message underneath, a direct message shows the sender's name and the message itself, and each conversation keeps its own notification instead of one shared line, so a busy channel cannot bury a DM. When several messages arrive in the same place while you are away they collapse into one notification carrying the newest text, with a count beside the name — #krakow (4) — rather than replacing each other silently. Tapping opens the conversation it is about: the channel, or the direct-message panel on that contact. That works whether the app was already running or started from cold, in the Android app and in the browser alike, and it reaches the right place without reloading the interface when the app was merely in the background.

Fixes

  • One corrupted advert no longer empties the contacts map. Switching Cache on over the contacts map could leave you looking at a blank map of the world with not one of your contacts on it, and the Path Analyzer could draw a route as a single line stretching across the globe. Nothing was wrong with the contacts you could not see. Adverts arrive over the radio unchecked, and a packet mangled in flight can carry a position that is not a position at all — a latitude of 1642 degrees, a longitude of -1768. One such entry is enough, because the map fits itself around everything it is given: it zooms out until that impossible point is on screen, and every real contact ends up several world-widths away, outside the window. The tester who reported this had exactly two of them among 2131 cached contacts. Positions outside the range the Earth actually has are now thrown away rather than stored, thrown away again on the way out to the browser so a cache that already holds some heals itself the moment you update, and refused once more by the map itself. The contact is still kept and still listed — it simply no longer claims to be anywhere. The same check now guards every map in the app: contacts, repeaters, a repeater's neighbours and the Path Analyzer, which had its own looser version of the rule and would place a node reporting a longitude of exactly zero off the coast of Africa.

  • Demo mode no longer locks the sidebar layout setting, which was only ever yours. On an instance running in demo mode the whole Settings → Interface tab was greyed out, and that swept up one control that had no business being there: the width at which the sidebar appears. That number is stored in your own browser, exactly like the theme and the quick-access bar, so changing it never touched anybody else's view of the instance — it was locked only because it happened to sit on the same tab as the toast settings, which genuinely are shared and stay locked. The rule the guard was meant to follow is that a preference living in your browser is yours to change: theme, desktop notifications, the quick-access bar and where its items go were all already allowed, and the layout breakpoint now joins them.

  • New messages on the channel you had open no longer arrive in silence, and old unread ones stop announcing themselves as new. Two separate faults made notifications look unreliable even while the app was awake and connected. The first: a message on whichever channel you last had on screen was quietly excluded from notifications, on the assumption that you were looking at it — which is exactly wrong once the app is in the background, and meant that anyone who left mc-webui on their busiest channel was told about every channel except that one. A channel now counts as open only while somebody is actually looking at it. The second: the first time the app compared its unread counts against the server it treated the entire backlog as having just arrived, so a page that loaded in the background could announce New: 492 channels about messages that were days old. That first comparison is now only a measurement, and anything already unread when the app starts is treated as history rather than news.

  • Still unresolved: notifications going quiet after a while on Android. The changes above fix what a notification says and where it takes you, but not the separate problem of the Android app falling silent some minutes after you switch away from it. That has a different cause — Android freezes apps that have nothing running in the foreground, which stops the connection the notifications depend on — and no setting on the phone can prevent it. Fixing it properly needs a background service in the app itself, which is planned for a later release.


2.12.0 — 2026-08-27

Features

  • An instance you can hand to strangers: demo mode. mc-webui has never had a login, which is the right trade on a home network and the wrong one the moment you share the address — to show the project off, to collect feedback, or because a store wants a dozen testers who use the app for a fortnight. What happens next is predictable: visitors assume the node is theirs and rename it, and the console is right there for anyone curious enough to type into it. Putting MC_DEMO=true in .env turns the instance read-only for everybody except you. Everything that reconfigures the radio is refused — the name, the coordinates, the frequency and the rest of the radio settings, reboot, advert and flood advert — along with everything that deletes data, everything that reaches other people's repeaters, the update trigger that rebuilds the containers, the settings that publish packets to an external broker, and the handful of things that hand out secrets such as stored repeater passwords, database backups and the server log. The console stays open for the commands that only read, so it is still worth demonstrating, and refuses the rest with a list of what it will accept. What does keep working is the part that makes a demo worth visiting: reading everything, sending messages in channels and as DMs, and changing the theme and layout, which live in your browser and affect nobody else. The refusal happens on the server, in front of every endpoint and on the console's own connection, so a stale page or a crafted request gets the same answer as a click. In the interface the locked controls are greyed out rather than hidden — the point is to show mc-webui off, and a feature nobody can see impresses nobody — with a yellow DEMO badge next to the version, a short explanation at the top of Settings and above the console, and a message rather than a silent shrug when something is clicked. Full guide in demo-mode.md.

  • Two ways to keep full access to your own demo. Locking the instance is only half of it; you still need to administer the thing. MC_DEMO_TRUSTED_NETS takes a list of networks — 192.168.0.0/16, say — whose visitors are never restricted, so from your own LAN the app behaves exactly as it always has, with nothing to type. MC_DEMO_UNLOCK_CODE sets a code you enter once under Settings → Operator access; the browser is then remembered for thirty days from any network, which is what you want when the instance is published through a tunnel and every visitor looks like it is coming from the same place. A Lock again button sits in the same spot for when you want to see the instance the way a visitor does. The cookie holds a derived token rather than the code itself, so changing the code in .env logs every unlocked browser out — which is also how you take access back. One warning worth repeating from the guide: if mc-webui is published through a tunnel or a reverse proxy, every visitor arrives from that one address, and if the tunnel runs on your LAN then listing your LAN as trusted would unlock the instance for the whole internet. mc-webui declines to make that mistake — an address that arrives with a proxy header is not matched against the trusted list unless MC_TRUST_PROXY is on — and says so in the log at startup, but the safe habit is to use the unlock code whenever the app is not reached directly.

Fixes

  • Syncing a repeater's clock now uses the server's time, and your own node's clock is kept right by itself. Pressing Sync clock under My Repeaters → Actions could set a repeater to a time that was wrong by an hour or more, and the time it reported back rarely matched what the same panel showed a minute later. Two separate things were behind it. The command mc-webui sent asked the repeater to copy the timestamp out of the packet that carried the command — and that timestamp comes from your own node, not from the server, because the firmware deliberately stamps command packets with its own clock. Your node's clock, meanwhile, is only ever set by hand and then left to run free, and on boards without a dedicated timekeeping crystal it drifts badly: the test device here turned out to be sixteen hours behind. So the button was faithfully copying a wrong time onto the repeater. mc-webui now puts the moment inside the command itself, taken from the server's clock and untouched by anything in between, and it checks your node against the server when it connects and once an hour after that, correcting it whenever it has drifted more than half a minute. One limit is worth knowing, because nothing in the app can work around it: the firmware refuses to move any clock backwards, so a node or repeater that has run fast cannot be corrected over the air at all and needs a restart before it will accept the right time. When that applies to your own node it is now written to the log instead of being retried in silence every hour.

  • Clocks now say which timezone they are showing. The same instant was being written three different ways with nothing on screen to tell them apart: the Clock row under My Repeaters → Status is drawn in your browser's timezone, the console printed the server's, and repeaters answer their own clock command in UTC. Reading 17:16 from one and 19:16 from another, minutes apart, is enough to make a perfectly healthy setup look broken. The Status row now names the zone alongside the time (23.08.2026, 21:21:01 CEST), and the console prints both readings at once (2026-08-23 21:15:50 CEST / 19:15:50 UTC), so a console reading can be held against a repeater's own reply without doing the arithmetic first.

  • The Quick Access buttons no longer sit on top of the send button, because they are now off by default. On a phone the column of round buttons down the right edge overlapped the message composer, and the bottom one landed squarely on top of send — you could see the button, but tapping it hit the floating button instead. Enough people ran into it to make clear this was the default's fault rather than theirs, so the bar is now opt-in: Settings → Appearance → Show Quick Access buttons, unticked to begin with. With it off, everything that lived on the bar — filter, search, direct messages, contacts and settings — appears in the main menu under the ☰ icon instead, so nothing becomes unreachable. If you had already made a choice there, it is kept: only browsers that never touched the setting see the new default. For anyone who likes the bar, it is unchanged once switched on, including the size and spacing sliders and dragging it by its chevron to a spot that suits you. One gap remains: the direct-message screen has its own separate bar, which can be dragged and collapsed but does not yet follow this setting.

  • A message quoting something in angle brackets no longer breaks the row of buttons under it. A bot answering with Type 'channel <name>' for a description was enough: the buttons beneath that message came out mangled, with a stray title="Quote" printed next to them, and the first two — reply and quote — stopped working. The cause was in how those buttons were built: the message text was pasted into them without being escaped, so an apostrophe closed the button early and a < ended it altogether, spilling the rest of the markup onto the page as text. The text is now escaped properly wherever it is handed to a button, in channel chat and in direct messages alike, so quotes, angle brackets and both kinds of quotation mark are just characters again. The same fix covers sender names containing an apostrophe, whose reply, map and block buttons were quietly dead for the same reason.


2.11.0 — 2026-08-20

Features

  • The Android app has been rebuilt for Android 16, and now paints the system bars to match your theme. Google requires everything submitted to Play from September 2026 to target Android 16, and that version takes away an app's ability to opt out of drawing behind the status bar and the navigation bar. Left alone, that would have pushed the top of the interface under the clock and the message box under the gesture bar. The app now measures those bars itself — along with the display cutout and the keyboard — and keeps the interface clear of them, so the layout is unchanged from what you are used to. What is new is that the strips behind the two bars take the background colour of the page rather than staying white, and follow you when you switch between the light and dark theme, without needing the app restarted. The bar icons flip between dark and light along with them, so they stay readable either way. If you run the app from the .apk in this repository, this is version 2.0; the signing key is unchanged, so it installs straight over the previous one and keeps your saved server address.

  • The Android app now has a published privacy policy. It is a requirement for anything listed on Google Play, but it is worth having regardless, because the honest answer is short: the app collects nothing, contains no analytics or advertising, and the author operates no servers to collect anything with. What stays on the phone is the address you typed, the login for your server if it asks for one, and the browser cache — each listed, with the reason it exists, alongside every permission the app requests. It is published from this repository at marekwo.github.io/mc-webui/privacy-policy.html, in English and Polish.


2.10.0 — 2026-08-19

Features

  • Opening a busy channel now takes you to where you stopped reading, not to the bottom. Until now, every channel opened at the newest message, which is the right place to be when you are up to date and the wrong place to be when you are not: catching up on a channel that had collected forty messages while you were away meant scrolling up by hand and hunting for the last thing you recognised. A red New messages line is now drawn at the boundary between what you had already seen and what arrived since, and the view opens on that line rather than at the bottom, with the last message you had read still visible above it for context. The line stays where it was put for as long as you are in the channel — it does not creep downwards as you read — so you can always tell how much of the backlog you have worked through, and messages arriving while you are reading are added below it, where they belong. It appears only when there is genuinely something to catch up on: a channel you are up to date with, or one you are opening for the first time, behaves exactly as before and opens at the newest message. The same line, and the same behaviour, applies to direct messages. Should you want the bottom immediately, the jump-to-bottom button is already there waiting, and sending a message takes you down on its own.

  • Contacts, channels and locations shared in chat now look like something you can use. Other MeshCore clients let you drop a contact, a channel or your position into a conversation, and the mesh carries it as ordinary message text — which is why, until now, it arrived here as a wall of hex: <e56c230c…3ae729:1:MarWoj mobile>, unreadable and useless. Those messages now appear as cards with a name, a type and a button, in channel chat and in direct messages alike, including every such message already sitting in your history. The button says what will actually happen before you press it: Add Contact when the key is new to you, Update name to X when you already hold that key under a different name — the key is the identity, so this renames what you have rather than creating a duplicate — Already in contacts when there is nothing to do, and for channels a plain refusal when the name you were sent is already occupied by a different key, instead of quietly shadowing the channel you already had. A shared contact goes into your cache rather than onto the device: it arrived unsolicited, device contact memory is limited, and the card offers Push to device right there if you do want to spend a slot. Channels go straight to the device, since without the key in the firmware there is nothing to decrypt with. A shared position becomes a card with View on map, a copy button, links out to Google Maps and OpenStreetMap, and — something the official app does not show you — how far away it is and in which direction. Coordinates are only recognised when they are genuinely coordinates, so "1,5 km" in a sentence is still just text.

  • You can share those things yourself, with the new + button beside the emoji button. It offers three: a contact, a channel, or a location. Your own details are the first entry of the contact list rather than a separate menu item, so sharing yourself is one tap, and the same applies to your own position in the location picker — which otherwise lets you tap the map for any point you like. The contact list covers everyone you know, cache included, with a search box because a busy mesh gives you well over a thousand of them. Sharing a channel takes its region scope along, which matters: a recipient transmitting under the wrong scope simply would not be heard. What lands in the message box is the same plain text every other MeshCore client uses, so the card shows up properly whatever app is on the other end, and you can write a comment around it. While the text is valid you get a preview of the card the recipient will see; break the text by editing it and the preview disappears, which is your warning that it will arrive as raw text. These payloads are bulky — a shared contact eats roughly 90 of the message's bytes — so the counter under the box accounts for it, and anything that will not fit in one message is refused rather than trimmed, because half a public key is worth nothing.

  • Repeater settings now tell you when a setting simply isn't there, instead of calling it an error. Repeaters on the mesh do not all run the same firmware or the same hardware, so a setting that exists on one is genuinely absent on another. Until now the panel had only two answers — a value, or a red error — so a perfectly healthy repeater that had never heard of a setting looked broken, and the section reported a failed read. Such a field now gets a grey not supported badge with a short explanation, stays locked, and is left out of anything you Apply. The section itself no longer counts it as a failure. There is one honest caveat, and it comes from the firmware rather than from mc-webui: when a repeater rejects a setting after having already stored it — which is what newer firmware does with RX boosted gain on radios that cannot do it — "not supported" says nothing about what the device now holds. Press Refresh to see the truth.

  • Three settings added for repeaters that support them. Advertisement gains two more hop limits alongside the existing one: a separate cap for flooded adverts, and a cap for flood packets carrying no region scope. Both have existed in the firmware for a while but were never exposed here. Advanced gains channel activity detection (firmware v1.17 or newer): the radio listens before transmitting and holds off if the channel is busy. On older repeaters it appears as not supported, as described above.

  • A repeater can be powered off from the Actions tab (firmware v1.17 or newer). This is deliberately not the same kind of button as Reboot. A reboot brings the repeater back on its own in a few seconds; a power-off does not, and nothing on the mesh can undo it — the node stays off until somebody physically gets to it, which for a repeater on a mast is a trip rather than a click. So it is not confirmed with a Yes/No box: you have to type the repeater's name to unlock the button, and the server checks that name again before sending anything.

  • Repeater telemetry labels the processor temperature as what it is. Repeaters on firmware v1.17 and newer report the temperature of the MCU chip itself. It arrived as a plain "Temperature" row on channel 1, which invites reading it as the temperature where the repeater is installed — it is not, and it normally sits well above room temperature. It is now labelled MCU temperature. Boards without a usable internal sensor send nothing, and in that case the row is simply absent rather than showing zero.

Fixes

  • A direct-message conversation no longer drags you back to the bottom while you are reading it. Scrolling up through an old exchange in Direct Messages held for at most a minute: the periodic refresh redrew the conversation and threw the view back to the newest message, over and over. Channel chat had never done this. Direct messages now leave the view alone unless you were already at the bottom.

2.9.1 — 2026-08-14

Fixes

  • On an iPhone, tapping a text field no longer blows the whole page up. Tap the message box, and the page zoomed in — the send button pushed off the right edge, the layout too wide for the screen — and it stayed that way after the keyboard was dismissed, until you pinched it back down by hand. It happened in Safari and in Chrome, on a freshly unboxed phone with nothing configured, and it was not the phone's fault: iPhones deliberately zoom in on a form field whose text is smaller than 16 pixels, on the assumption that you would otherwise be squinting at it. The message box was set to 14.4 pixels on narrow screens, so every tap tripped it. Worse, the app's layout is pinned to the height of the screen and does not scroll as a page, which leaves the browser nothing to scroll back — so the zoom stuck instead of undoing itself. Every text field, dropdown and search box in the app is now at least 16 pixels on a touch screen, which is the size iPhones are content to leave alone. iPads never did this, and Android phones never did either, which is why it went unnoticed for so long. On a computer the fields are unchanged.

2.9.0 — 2026-08-14

Features

  • A path can now be set straight to Direct. The path dialog could send a node's route back to FLOOD, and it could push one of your configured paths — but the third option the firmware supports, Direct, had no button. Direct means no repeater at all: the packet goes to the node and nowhere else, which is what you want for a neighbour you can reach on your own radio, and the only way to stop your traffic from being repeated across the mesh for a node sitting on the next hill. Until now, getting there meant clearing the path through the Console. A Set Direct button now sits next to Restore FLOOD, in both places paths are managed: the contact info dialog in Direct Messages, and the path dialog under My Repeaters. Like Restore FLOOD it changes only the path on the device — your configured paths are left alone — and it asks before it does it.
  • You can restart your device from the interface. Rebooting a node used to mean either opening the Console and typing reboot, or getting to the hardware and pulling its power — awkward when the device sits on a mast or in another room, and awkward for anyone who never opens the Console. Open the menu, pick Device, and the Info tab now has a Reboot button under the table. It asks first: the device drops off the mesh for a few seconds while it restarts, and a message addressed to you in that window can be lost. The firmware sends nothing back in reply to the command, so what the app confirms is that the command went out — not that the device is back. It will be, on its own, a few seconds later, and mc-webui reconnects without any help. The same restart a remote repeater has had all along under My Repeaters → Actions, now for the node on your own desk.

Fixes

  • Switching channels on a phone no longer pops the keyboard open. The channel picker in the top bar was a text field you could type into, so every tap on it summoned the on-screen keyboard — which on a phone covers half the screen, over a list four or five items long. The search was solving a problem nobody has: the firmware caps how many channels a device can hold, so there is never enough of them to be worth searching. The picker is now a plain button that opens the same list, with the same last-message previews, unread badges and favourites, and nothing to type into. On a computer, Enter or Space still opens it, the arrow keys move through it, Enter picks and Escape closes.

2.8.0 — 2026-08-13

Features

  • You can now record what the app receives from the radio, and send that recording to whoever is helping you. Some problems are impossible to diagnose from a screenshot. The classic one: you send a message to a channel, and the badge listing the repeaters that heard it never appears — while the Analyzer plainly shows the packet travelled onward. Nothing on screen can tell you whether nobody in earshot repeated it, or whether the repeat did come back over the air and was dropped on the link between your device and mc-webui before the app ever saw it. Those two have the same appearance and completely different answers. A new Settings → Diagnostics tab records the raw material needed to tell them apart: press Start recording, reproduce the problem, press Stop and save, and you get a file. It contains every radio frame the app received while recording, what the app decided about each one, what it expected to hear back for each message you sent, and — the number that matters most — your device's own count of packets received, taken at the start and at the end. If the device says it heard two hundred packets and the app only ever saw a hundred and twenty, frames are being lost on the companion link, and that is now a measurement rather than a guess. Recording stops on its own after the time you choose (5 to 60 minutes) or once the file reaches 25 MB, whichever comes first, so a recording you forget about cannot fill your disk. While one is running, a red Recording marker sits in the status bar at the bottom of the chat, because it should never be something you leave on by accident.
  • Sending a recording. Under the same tab, each saved recording can be downloaded, deleted, or — if the person helping you has given you an upload token — sent to them directly with one button, which gives you back a link to pass on. Without a token nothing is sent anywhere: the download is always there, and you forward the file yourself however you prefer.
  • Read the privacy notice before you send one. A recording necessarily contains the text of every message that arrived while it was running, along with contact names and public keys. That is what makes it useful and also what makes it worth thinking about: the tab says so plainly, and asks for confirmation before an upload. Only send one to someone you trust.

Fixes

  • Long routing paths stay readable on a phone. Under My Repeaters, a path through several hops is written as AB→CD→EF with nothing between the hops, and a browser will not break a line in the middle of that — so on a narrow screen the path either ran off the right edge of the card or was cut short with an ellipsis, hiding exactly the hops you opened the page to read. Paths now wrap onto a second line, breaking after a hop arrow, in all four places they appear: the repeater list, the header of a repeater's management page, the picker for adding a repeater, and the current-path line in the path dialog. A repeater's coordinates also no longer get separated from their pin icon when the line wraps.

2.7.0 — 2026-08-07

Features

  • The device's Statistics tab now shows everything the device actually reports. Open the menu, pick Device, and switch to Statistics: until now it listed the battery, an error counter, the number of packets sent, and then some counts out of mc-webui's own database — while the stats command in the Console, reading exactly the same data from exactly the same device, printed a good deal more. The reason was a plain mismatch of field names: the tab asked the device for "uptime", "queue", "airtime" and "packets received" under names the firmware does not use, so those rows quietly disappeared instead of showing up empty, and nobody could tell the difference between a value the device withholds and a value the app failed to ask for. All of them are back, and with them the ones that were never displayed at all — last RSSI, last SNR and the noise floor (worth knowing: how loud the band is where your node sits determines what it can still hear), the split of sent and received packets into flood and direct, the count of RX errors, and channel utilization, which is the share of the time since the last reboot that your radio spent either transmitting or receiving. The tab is now laid out in four titled groups — System Information, Radio Statistics, Packet Statistics and Database — the same grouping, in the same order, with the same labels as the status view for a remote repeater under My Repeaters, so the two read the same way. Airtime is shown as a duration (8h 31m) rather than a number of minutes. Everything here except the Database group is counted from the last device reboot, not for all time.

Fixes

  • The Path Analyzer map no longer shows raw HTML in its pop-ups. Click a repeater on the map — either a hop on the route or one of the amber candidates the analyzer offers where the path is ambiguous — and the pop-up naming it ended the second line with the literal text <code>47</code> instead of the repeater's hash in a monospaced box. Only the display was affected; the hash itself was always correct.
  • A message you sent no longer loses the list of repeaters that heard it when something else goes out at the same moment. Your own messages find out which repeaters picked them up by listening for their own echo coming back off the mesh a few seconds later — and while waiting, the app could only keep track of one message at a time. Send a message and then press Resend on an older one, or send two in quick succession, and the second action took over that slot; when the first message's echo finally arrived it was filed as somebody else's traffic. The message kept its text but never got its signal badge, its repeater list or its Analyzer button, and nothing would bring them back. On the test server this affected roughly one message in thirty. The app now keeps every recent send in mind at once, and listens for five minutes instead of one, because repeats do sometimes arrive minutes after the send. Resend repairs the damage too: a message with no badge could not be re-armed before — re-arming needed exactly the packet identifier that was missing — so it is now recovered from the copy of the packet the app keeps at send time. Press Resend on a badge-less message, wait for a repeater to pick it up, and the badge appears. One thing this cannot fix: if no repeater in range repeated your message at all, or the repeat was lost on the way, there is no echo to hear and the badge stays empty — that is the radio, not the app.
  • Resending an old message now asks first. Resend puts the exact same packet back on the air, and repeaters normally recognise it and stay quiet — that is the whole point, it only reaches the ones that missed it the first time. But a repeater only remembers packets it saw recently, so past a certain age nothing recognises it any more and the message spreads across the channel as if it were brand new: everyone sees it again, dated today. Pressing Resend on a message older than an hour now tells you how old it is and what will happen, and lets you back out. Anything more recent than that behaves exactly as before, with no extra click.

2.6.0 — 2026-08-03

Features

  • The Android app can now sign in to a server that asks for a password. Putting mc-webui behind a reverse proxy is what makes publishing it to the internet reasonable, and the usual next step is to make that proxy demand a username and password — an Access List in Nginx Proxy Manager, for instance. The app had no answer for that: it showed the proxy's bare "401 Authorization Required" page and stopped there, which meant the phone was the one thing you could not protect. It now asks for the login the first time it connects and remembers it per server address, so later launches go straight in; get it wrong and the same prompt returns with the username already filled in. This appears only when your server actually asks for a password — an instance without one, local http:// or a proxy with no access control, behaves exactly as it always has and never shows this screen. One thing worth knowing: if you turn the password on, or change it, while the app is already open, the app cannot notice mid-session — close it and open it again. This is app version 1.3 — install the new .apk over the one you have; it is signed with the same key, so it goes in as an update and keeps your server address. See android-app.md.
  • The HTTPS guide now explains how to put a password in front of mc-webui. The guide always mentioned that Nginx Proxy Manager can do this — mc-webui has no accounts of its own — but never said how, which made the one step that actually matters for an instance published to the internet the one step you had to work out yourself. https-setup.md now walks through creating an Access List and attaching it to your proxy host, and is clearer about a distinction that is easy to miss: requiring a login keeps strangers out, while closing port 5000 (MC_BIND_ADDRESS=127.0.0.1) stops your own network from reaching the app around the proxy — and therefore around the password. Publishing to the internet wants both; a server whose testers still use http://<address>:5000 wants only the first.

2.5.0 — 2026-08-03

Features

  • You can now reach mc-webui over HTTPS. Until now the interface was served over plain HTTP only, which meant the address bar warned it was not secure, and — less obviously — that browsers quietly withheld a set of features they reserve for encrypted connections. There is now an optional HTTPS front end you switch on with a single line in .env; it starts Nginx Proxy Manager next to the app, and you pick your certificate in its web interface rather than editing configuration files. Three routes are covered: a free Let's Encrypt certificate for a public domain, a Let's Encrypt certificate via a DNS challenge — the one worth knowing about, because it gives a fully trusted certificate on a server that is not exposed to the internet at all, with no port forwarding — and a self-signed certificate for reaching the server by its IP address. Existing installations are untouched: nothing starts and nothing changes unless you ask for it, and updates keep working exactly as before. The guide is https-setup.md, including which switch to be careful about (Websockets Support — leave it off and several open tabs will crawl) and why a self-signed certificate cannot work with the Android app.
  • The interface can be translated, and Polish has started. mc-webui was written English-only, with every label and message baked into the code. There is now a translation system behind it, and a Language setting at the top of Settings → Appearance. It covers the whole interface: the main window — menu, chat, message bubbles, its dialogs and Settings — every panel that opens in its own window (System Log, Console, My Repeaters, Path Analyzer, Contacts, Direct Messages), and the running commentary too: the small toasts after every action, the confirmation dialogs, the search results, the update flow and the notifications your phone shows when the app is in the background. Error text that comes back from the server itself is still English — that is the one part left, and it is a separate job. Your choice applies to the browser you set it in and also becomes the default for anyone else opening the server without a preference of their own.
  • You can add a language yourself, without waiting for a release. A language is a single file. Copy en.json from app/translations/, translate the values, and drop it into a translations folder inside your config directory — the same place the database lives. Refresh the page and it appears in the Language list, named however you named it, with no rebuild and no restart. Anything you leave untranslated falls back to English, so a half-finished translation is perfectly usable. A file you drop in also overrides one that ships with the app, so you can correct the built-in Polish on your own server. English and Polish ship in the box; everything else is open to whoever wants to write it. See translations.md, which explains the format and — importantly — which words to leave alone: mesh terms like flood, hop, advert, RSSI and the repeater roles stay English in every language, because that is what the firmware, the CLI and the forums all use. The Console and the log lines themselves stay English for the same reason.
  • Times and dates behave the same in every language. Only the words are translated — "Yesterday" becomes "Wczoraj", "5 min ago" becomes "5 min temu". The clock and the number formatting keep following your browser's own settings, so switching the menus to English will not suddenly turn your 24-hour clock into "9:53 AM". One display of large numbers on the repeater statistics page had been hard-coded to American thousands separators; it now follows your locale like everything else.

Fixes

  • Copy buttons work over plain HTTP again. Copying a public key, a channel key or a routing path did nothing on some pages when mc-webui was open over http:// on a network address — no error, no text on the clipboard, just a button that appeared to do nothing. Browsers only offer the modern clipboard function to pages served over HTTPS (or opened on the machine itself), and several of those buttons had no fallback for anything else. They all share one now, so copying works over HTTP everywhere in the interface. Serving the interface over HTTPS removes the restriction at the source.
  • The Android app no longer renders text larger than the browser does. The app is a wrapper around the same page you open in Chrome, but Android's WebView quietly scales text by your phone's system font-size setting while Chrome ignores it and uses its own. If you had enlarged the system font even one step, everything in the app came out around 20% bigger than on the website — the title truncated to "mc-web…", longer messages wrapped earlier, and the connection status and refresh time at the bottom broke across two lines each. The app now pins text at the same size the browser uses. This is app version 1.2 — install the new .apk over the one you have to pick it up; it is signed with the same key, so it goes in as an update and keeps your server address. See android-app.md.
  • The bar at the bottom copes with text that does not fit. The connection status, the region badge and the "Updated:" time sat in a row that could not wrap, so when they ran out of room each one broke apart internally instead — a green dot with "Connected" stranded underneath it, a time split from its own label. They now drop onto a second line as whole items. This mostly showed up in translated interfaces, where the labels are longer than the English ones they were laid out for ("Aktualizacja:" against "Updated:"), and on narrow phones.

2.4.2 — 2026-07-31

Fixes

  • Unread counts stand out in the channel list on a phone. On a wide screen, a channel with unread messages shows the count in a blue badge next to its name. On a narrow screen the channel list is a drop-down from the top bar, and there the same number was drawn in plain text, the same colour and size as everything else in the row — easy to read as part of the timestamp and easy to miss entirely. It is now the same blue badge as on a wide screen.

2.4.1 — 2026-07-31

Fixes

  • You can keep mc-webui open in several tabs again. Two or three open windows used to bring the whole thing to a crawl — the message list crept, buttons took ten or twenty seconds to do anything, and the status could sit on "Connecting…". It looked exactly like an overloaded server, and it wasn't: the server was answering in a few thousandths of a second the entire time. The live connection that pushes new messages to an open page was running in a mode that keeps a browser connection permanently occupied, and a browser only allows six connections to one address shared across every tab. Three tabs took the lot, so everything else — loading messages, marking them read, sending — queued in the browser waiting for a free one. That connection now uses a proper WebSocket, which does not come out of that budget. Measured with three tabs open: a request that had been taking around 15 seconds now takes about 12 milliseconds. The advice to keep only one window open no longer applies. If you run mc-webui behind a reverse proxy that isn't set up to pass WebSocket connections through, the page quietly falls back to the old behaviour and works exactly as before.
  • Messages no longer go missing after the app has been in the background. Coming back to a minimised app — or to a phone that had been asleep — could show a chat that quietly stopped at whatever message arrived last before the screen went off, with everything since then missing until the app was force-stopped and reopened. New messages reach an open page over a live connection, and Android tears that connection down while the app sits in the background; nothing then went back to ask the server what had been missed. Now every way back from a gap re-reads the list: the connection coming back, the app returning to the foreground, and a heartbeat that notices when the page has been frozen. The same applies to direct messages, and to a browser tab that lost its network for a while.
  • A Refresh item in the menu. The browser's pull-to-refresh has no equivalent in the Android app, so there is now a Refresh entry at the top of the menu that reloads the messages from the server on demand — in the app, and everywhere else too.
  • The connection-status check is about five times cheaper. Every open page asks the server how the mesh device is doing — on load, once a minute after that, and each time you come back to a tab you had left. Answering that took roughly a quarter of a second, almost none of it spent on the device itself: the server was counting the rows of every table in the database to report two numbers, and the row it needed for the "last message" timestamp was found by reading the entire message table and sorting it. The heaviest part, counting the radio-echo records, was thrown away unused — and that table only ever grows, so the check was getting slower the longer an instance had been running. It now asks for exactly the three values it needs, in one go. This is a smaller effect than the bundling below and it does not change what you see on screen; it does take a recurring cost off the server on every open page.
  • Far less load on the server while you have messages on screen. Whenever new radio traffic came in, the page asked the server about every message on screen separately — hundreds of individual requests at a time, repeated every few seconds, and the same messages over and over. On a busy channel that was thousands of requests a minute from a single tab, which left the server little room to answer anything else; the worst of it looked like the mesh device had dropped off, with the message list stuck on "Loading messages…" and the status on "Connecting…", while the device was connected the whole time. Those requests are now bundled into one. A page that needed hundreds of requests per update now needs a single one. The remaining reason several windows were slow is fixed separately — see the first entry above.

2.4.0 — 2026-07-29

New features

  • The Android app can send notifications now. When the app shipped in 2.3.0, notifications were the one thing it couldn't do, and the advice was to keep a Chrome "Add to Home Screen" install alongside it. That's no longer necessary — turn notifications on in the mc-webui menu as usual, allow the permission Android asks for, and new messages and pending contacts arrive as ordinary Android notifications. Tapping one reopens the app. Two things are actually better here than in the browser: they work over plain http:// too (Chrome refuses notifications on an unencrypted page), and the app shows up in Android's notification settings like any other, so you can silence it or change its sound there. The same limit as the browser still applies: notifications arrive while the app is open or recently in the background, and stop once Android suspends it — reopening the app catches you up. Requires app version 1.1; see the Android App guide for how to update.

2.3.0 — 2026-07-28

New features

  • There is an Android app now. A small companion app opens your own instance full screen — no address bar, its own icon in the app drawer, and it looks like a proper Android app rather than a page in a browser. It holds no mesh logic of its own: on first run it asks for the address of your server (http://192.168.1.100:5000 on the local network, or your https:// hostname behind a reverse proxy), remembers it, and from then on launches straight into mc-webui. Everything still runs on your server and your MeshCore device. It is not on Google Play, so you download the .apk from the repository and install it yourself — the new Android App guide walks through that, including the "unknown sources" permission and the Play Protect notice Android shows for any app that didn't come from the Play Store. Scanning contact QR codes works (on https:// instances — over plain http:// the camera is blocked by the same browser rule that applies in Chrome), and database backups download to the phone's Downloads folder. One thing stays with the browser: notifications, which the app has no access to — if you rely on them, keep the Chrome "Add to Home Screen" install alongside it, the two work happily side by side. The app's source sits next to the .apk, so you can read what you are installing or build it yourself.

2.2.0 — 2026-07-27

New features

  • Quotes now use the > convention everyone else already uses. Quoting a message used to wrap it in guillemets — @[Daniel] »Hejka« Cześć! — a shape mc-webui invented back when it couldn't send multi-line messages. It now puts the quote on its own line behind a > and drops the cursor underneath it, so a reply reads as a quote in the standard MeshCore app and every other client too, not just here:

    @[Daniel] >Are we still on for tonight?
    Yes, 8pm works.
    

    Quotes you type by hand get the same italic, tinted styling, so > written manually now looks like a proper quote as well. Nothing you received earlier changes: the old »…« form is still recognised and still displayed exactly as before.


2.1.0 — 2026-07-26

New features

  • Releases now have version numbers. This is the first numbered release: the menu shows 2.1.0 with the exact build (date and commit) underneath, so "which version are you on?" has a short answer, and the build is still there when a problem needs pinning down. Each release is tagged on GitHub with these notes attached, so you can see what changed without digging through the commit history.
  • The Path Analyzer map starts on the route you asked for. The map used to plot every located repeater on top of your route, which buried the path you actually wanted to see. It now opens showing just that route, and two checkboxes in the map's top corner add the rest back when you want it: All repeaters brings back the purple dots of uninvolved repeaters, and Alternative paths draws the other copies of the same message your node overheard. Both start off, and switching them never moves or re-zooms the map, so you keep the view you panned to.
  • See where a message's routes actually differ. With Alternative paths on, each of the message's other routes gets its own light colour, matched by a coloured dot next to it in the side list, so you can tell the lines apart — tap one to see its hops and SNR. Copies of one message usually travel most of the same way, so only the stretches where an alternative really diverges are drawn, plus a dot marking where it ends. Often that's the last hop alone, which used to be invisible under the main route.
  • The Path Analyzer remembers your filters. Time range, hop and hash-size filters, the text searches, and the Routes segment length are kept between visits, so a working set like "Last 1 day + 2/3-byte" no longer has to be set up every single time. Clear resets and forgets them. Opening the analyzer from a chat route ignores your saved filters for that visit, so they can't hide the message you tapped through to. The settings are stored by your browser, not on the device — each browser or phone keeps its own.
  • Jump from a chat route straight to the map. Tapping a route under a channel message used to just copy it to the clipboard. Now it opens the Path Analyzer on its map view with that message selected and that exact route already drawn — the quickest way to see where a message physically travelled. Copying isn't gone: each route in the popup keeps a small clipboard icon for pasting into the console's change_path.

Reliability & polish

  • Your own sent channel messages no longer show a route that isn't theirs. A bug could attach a stranger's overheard packet to your just-sent message, so the delivery badge and the Path Analyzer occasionally displayed a repeater hash and a physically impossible path that were never part of your message. Sent-message echoes are now matched exactly, so the route you see is really yours.

2026-07-22

New features

  • My Repeaters — administer your repeaters from the browser. A new full-screen panel (main menu → My Repeaters) for the MeshCore repeaters you hold the password to. Add repeaters from your device's contacts, save the admin password once, and from then on logging in is one click — the app shows whether the repeater granted you ADMIN or GUEST rights. Each entry also gets the same path editor as DMs, so you can pin a direct route to make logins and commands fast. Heads-up: repeaters never answer a bad login, so a wrong password looks exactly like an unreachable repeater — the app's error messages say so instead of guessing.
  • Monitoring tools per repeater: Status, Telemetry, Neighbors. After login you land in a management panel. Status shows battery, uptime, clock, RSSI/SNR/noise floor, packet counters, and channel utilization in one compact table. Telemetry lists every Cayenne LPP channel at once, with proper units. Neighbors shows every zero-hop node the repeater hears (name, last heard, SNR) — plus a map view that draws SNR-labeled links to the neighbours it can place. Works with guest logins too.
  • Remote CLI, Settings, and Actions (admin logins). CLI is a real terminal to the repeater — quick-command chips, per-repeater history, round-trip times. Settings edits the repeater configuration in collapsible sections (Basic, Radio, Location, Features, Network health, Advertisement, Operator info, Advanced): values load live from the repeater, only the fields you change are sent, and every field reports back individually — including a "reboot required" badge for radio parameters and the firmware's own error text for rejected values. Actions covers zero-hop advert, flood advert (marked "not recommended" — high network load), clock sync, and a confirmation-guarded reboot in a Danger zone. Erasing the file system stays USB-serial-only by firmware design, and the panel says so.
  • Observer mode — feed packet analyzers straight from mc-webui. The new Settings → Observer tab turns your node into a MeshCore observer: every packet the device overhears is published to one or more MQTT brokers in the standard meshcore-packet-capture format, so analyzer services (a self-hosted Corescope, letsmesh-style maps) see your local mesh traffic without a separate capture script or a dedicated second node. Configure brokers with host/port, optional username/password and TLS; each row shows a live connected/error badge, and the tab counts packets captured vs published in real time. Capture is completely passive — chat and direct messages are unaffected — and all changes apply immediately, no restart needed. (LetsMesh token-authenticated brokers are not supported yet.)
  • Scheduled flood adverts. The Observer tab includes an optional advert interval in hours: the app sends a flood advert on that schedule so your observer stays visible on analyzer maps. The timer survives restarts, so a redeploy won't send an extra advert early. Set it to 0 to keep adverts fully manual.
  • Path Analyzer — see how your messages travel the mesh. A new full-screen tool (main menu → Path Analyzer) loads every channel message from all your channels for a chosen window (1–7 days) together with every copy your node overheard, and lets you dig in four ways. Messages: expand any message to see all its routes hop by hop with SNR — copy a repeater hash, copy the whole route, or jump straight to the map. Repeaters: per-repeater statistics (how many packets each hash relayed, over how many messages, and average SNR when it was the hop you actually heard), sortable and clickable to filter. Routes: the busiest hop sequences across all your traffic — pick a length (2–4 hops) and see which stretches of the mesh relay the most, with an "as path end" count that highlights the routes actually delivering to you. Map: pick a message and its shortest route is drawn instantly as a red line with numbered, name-labeled points — when a short 1-byte hash matches several repeaters, the candidates are shown in amber and you pick the right one instead of the app guessing (a mistaken pick can be undone per hop or reset for the whole path), and uncertain segments are drawn dashed so you always know which part of a route is confirmed.
  • Path Analyzer filters. Everything is filterable live and in combination: hop count, path-hash size (1/2/3-byte), repeater — by hash or by name, and now as a >-chained sequence to find a specific consecutive route (e.g. AFE6>6E9A) — sender, and message text. All four views follow the active filters, and clicking a Routes or Repeaters row drops you into the message list already filtered.
  • Path Analyzer works on phones. On narrow screens the whole filter bar collapses behind a Filters button (with a badge counting the filters you have set), the tables shed their secondary columns (packet hash and hash size move into the expanded row) and long routes wrap instead of scrolling sideways, and on the map the message list gets most of the height with the map fixed to a comfortable share below it.
  • Pin a repeater's location from a map. In My Repeaters → Settings → Location, a new Pick from map button lets you click a point on a map to fill in the latitude and longitude — no more looking up coordinates by hand.

Reliability & polish

  • Console login reports your role. A successful repeater login in the Interactive Console now answers "Logged into X as admin" (or guest) instead of a bare success line.
  • Failed repeater login no longer makes you retype the password. Since a wrong password and an unreachable repeater are indistinguishable — and a failure is usually just a flaky connection — the retry prompt now comes back with your saved password already filled in, so you can retry with one tap.
  • Repeater list reads better on narrow phones. A repeater's "last login" time now sits on its own line, so a long pinned path no longer squeezes the rest of the row.

Deploy notes

  • This update adds a new Python dependency (paho-mqtt) and raises the meshcore library requirement to 2.3.7, so the Docker image must be rebuilt — the standard mcupdate flow does this automatically.
  • Passwords you save in the app — MQTT broker credentials in the Observer tab and repeater admin passwords in My Repeaters — are stored in plain text in the app database. That's a deliberate trade-off for a private single-user LAN app; use dedicated credentials where you can.

2026-07-02

New features

  • Explicit Send on phones and tablets. On touch devices, pressing Enter now inserts a new line instead of sending — you tap the Send button to publish. This stops a mistapped Enter on the on-screen keyboard from firing off a half-typed message. Desktop keeps Enter-to-send. Applies to both group chat and direct messages.
  • Manage the Letsmesh analyzer like any other entry. The built-in Letsmesh Analyzer is now a normal row in Settings → Analyzer — rename it, disable it, star it as the default, or delete it, just like a service you add yourself. The chart icon under a message resolves at click time: nothing enabled shows a "No analyzer configured" hint, a starred default (or a single enabled service) opens directly, and several enabled without a default show a chooser. The row's switch now reads Enabled when it's on.

Reliability & polish

  • Multi-line direct messages keep their line breaks. A DM you sent across several lines showed correctly to the recipient but collapsed to a single line in your own copy. Your own bubble now preserves the line breaks too.

2026-06-26

New features

  • Resend a channel message (same packet). Your own group-chat messages now carry a repeat-arrow button that re-broadcasts the exact same packet. Repeaters that already forwarded it stay quiet, but nodes that never heard it can still pick it up — so a resend fills in coverage on the existing message's delivery badge instead of posting a duplicate. Requires companion firmware 1.16 or newer; the button is hidden on older devices.
  • Clearer "Edit message" button. The button that copies a message back into the composer for hand-editing used to be mislabeled "Resend." It's now a pencil Edit message button on both channel and direct messages, clearly separate from the real Resend above.
  • Custom Analyzer services. A new Settings → Analyzer tab lets you register your own MeshCore Analyzer services. Each entry has an enable/disable switch, a "star" toggle to mark it as the default, and an Edit/Delete pair. The chart icon under each group-chat message now resolves at click time: built-in Letsmesh if you haven't configured anything, the default service when one is set, or a chooser modal when several are enabled. URL templates use {packetHash} as a placeholder.
  • Apply a saved path straight from Contact Info. Each entry in the Paths list inside the DM Contact Info modal gained an upload-arrow button. Click it to push that configured path to the device as the active route — no more switching to the console to run change_path.
  • Database "Optimize now" button + live size. The Backup modal now shows the current DB size and exposes an Optimize now button that runs SQLite VACUUM on demand. Useful after a big retention pass when you want to reclaim space without waiting for the nightly job.
  • Automatic message retention is on by default. A nightly job at 03:30 (using your container's TZ) trims old data: 90 days of channel messages and direct messages, 60 days of advertisements, and 30 days of diagnostic data (echoes, paths, acks — these account for the bulk of long-term DB growth). When at least 1 000 rows are deleted, the database is VACUUM-ed automatically so file size shrinks too.
  • Smarter watchdog (host service). The host-level mc-webui-watchdog systemd service now catches a new failure mode — a "sluggish" device that briefly stalls on stats/battery commands while still receiving traffic — by counting soft-pattern hits over a 2-minute window. The app also exposes a new /health/strict endpoint that external monitors can poll.

Reliability & polish

  • System Log tab no longer floods the server. Opening the System Log could trigger a feedback loop that hammered the server with 10+ requests a second; the log noise that caused it is now filtered out, so the tab stays quiet.
  • The connection badge stops lying about device state. The status badge could flip back to "Connected" on a routine message refresh even while the device was actually disconnected. Device status is now driven only by real device connectivity, with a 60-second fallback check so a long-open tab stays accurate.
  • Automatic recovery after a failed reconnect. A reconnect that failed quietly used to leave the app stuck "disconnected" until the container was restarted by hand. The background liveness watcher now keeps retrying instead of giving up after a single failure.
  • No more 10–15 s freezes on app load. The realtime channel used a transport that the dev server couldn't upgrade; we now stay on long-polling, which keeps real-time pushes working without the reconnect loop.
  • Channel list stays complete when the device is slow. Channels are now read from the local cache rather than re-queried slot-by-slot, so a brief device stall no longer leaves you with just the Public channel after a refresh.
  • Sending on a re-used channel slot now works after a deletion. When you delete a channel, the device compacts the remaining slots — until now the app kept using the old keys for that slot. We refresh the secret from the device just before each send.
  • Region scopes work for all channels, not just slots 0–7. Channels stored in higher slots (e.g. #ubot, #swietokrzyskie) now accept scope changes.
  • Multi-byte routing paths render correctly everywhere. Contact list, DM modal, retry status, console output — all now show 2-byte and 3-byte hops with the right hop count and byte size (e.g. D103,5E34 (2 hops, 2B)), where they previously truncated the path or rendered single-byte hops.
  • Self-healing TCP connection. Long-lived TCP sessions against meshcore-proxy can degrade in ways the socket can't see — some commands silently time out while RX still trickles in. The app now detects this on a send failure and reconnects in-place, with a backup liveness watcher that triggers a reconnect when no RX event has been seen for 5 minutes.
  • Settings analyzer modals: backdrop and URL wrapping. Add/edit/chooser modals now dim the Settings backdrop correctly, and long URLs in the Analyzer list no longer push controls off-screen on narrow mobile viewports.
  • Console change_path accepts more formats and respects hop size. You can now use commas, spaces, or arrows between hex chunks (D103,5E34 / D103 5E34 / D1->90->05). For multi-byte paths, all chunks must be the same length — that length determines whether the path is sent as 1-, 2-, or 3-byte hashes.

Deploy notes

  • After deploying this release, restart the host watchdog so it picks up the new soft-pattern detection:
    sudo systemctl restart mc-webui-watchdog.service
    
  • The retention job runs the first time at 03:30 local. Your DB may shrink noticeably overnight — that's expected.

How this file is maintained

This file is updated before each merge of dev → main. Each release section starts as "Unreleased" and is renamed/dated once the merge happens, then a fresh "Unreleased" section is opened at the top.

Sections are grouped roughly as:

  • New features — things a user can find/click that didn't exist before
  • Reliability & polish — fixes for problems users actually noticed, plus quality-of-life tweaks
  • Deploy notes — anything the operator must do beyond the usual mcupdate

Internal refactors, code-level cleanups, and developer-only changes belong in git log and architecture.md, not here.