Commit Graph
30 Commits
Author SHA1 Message Date
MarekWoandClaude Opus 5 c96f1fd1d0 feat(diagnostics): record a support capture the maintainer can read offline
Some faults cannot be diagnosed from a screenshot on a server we have no
access to. The one that prompted this: a sent channel message gets no
repeater badge, and nothing in the database distinguishes "nobody in
earshot repeated it" from "the repeat came back over the air but the
firmware dropped the RX-log frame before the app ever saw it".

Settings > Diagnostics records a bounded window into a zip: every RX-log
frame, the echo correlator's decision (with the pending-send list that
explains a non-match), what each send expected to hear back, the
application log at DEBUG, and the device's own packets.recv counter at
both ends of the window. That last comparison is the whole point - it
turns the firmware's silent 4-frame queue overflow into a measurement
instead of a hypothesis.

scripts/diag_report.py reads a capture offline and prints the verdict.

Captures can be downloaded, or uploaded to a Zipline instance once the
user pastes a token handed to them out-of-band. The URL ships as a
default; the token never does, because this repository is public.

- Hot path follows observer.py's contract: one deque append, no locks,
  no I/O. The log handler uses a no-op lock object rather than
  lock = None, which raises under Python 3.13's `with self.lock` in
  Handler.handle() - latent on the 3.11 container, fatal on 3.13
- Caps: 5/15/30/60 min, 25 MB, 10 stored captures. All enforced by the
  writer thread, so a user stop and an auto-stop finalise identically
- The capture contains the text of messages received while it runs; the
  UI states this and confirms before any upload

Also drops a redundant function-local `from flask import current_app` in
get_status(): it shadowed the module-level import for the whole function
and broke /api/status as soon as anything referenced current_app above
it. Found by the capture tool, in its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 15:10:12 +02:00
MarekWoandClaude Opus 5 3b34dd9646 feat(i18n): translate the DM panel (stage 6)
Covers app/templates/dm.html and app/static/js/dm.js: the sidebar, the
searchable conversation selector, the composer, message bubbles and their
delivery meta, the Contact Info modal, path management, and the repeater
list/map pickers.

The DM panel's path management and repeater pickers are the code the My
Repeaters panel was adapted from, so its strings reuse the repeaters.*
keys added in stage 3a rather than duplicating them. Shared chat chrome
(filter bar, FAB titles, composer placeholder, status bar) goes to a new
chat.* namespace so index.html and app.js can reuse it in stages 7 and 9,
and console.status.* is promoted to common.connected/disconnected/
connecting now that two status bars want the same three words.

formatRelativeTimeDm() was the last of the four local relative-time
implementations; it is gone, folded into formatTimeAgo() from
datetime-utils.js. The contact-info row uses the long form to match the
contacts page, the map popup the short form to match My Repeaters.

Also fixed along the way, both found by grepping this slice's shared
strings against the already-translated panels:

- repeaters.js reset the map picker label from JS in three places with a
  hardcoded English string, so the header translated on load and flipped
  back to English the moment the picker opened (missed in stage 3a).
- tHtml() on a plural key always renders the "one" form, because only
  tn() passes the count to the resolver. That made the repeater picker's
  shared-prefix tooltip read "3 repeater ma ten prefiks". Fixed in both
  callers and now an error in scripts/i18n_check.py, which had no way to
  catch it.

Left English on purpose: Flood/Direct/FLOOD and the SNR labels (glossary),
and the 'Device path' label written to the DB on import - it is stored
data, so translating it would freeze one language into the row.

Catalog: 471 -> 526 keys, pl at 100%. Verified against the local
container with i18n-diff.js plus a new i18n-stage6-dm.js that drives the
panel through each modal, since the diff tool cannot see hidden ones.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 21:38:14 +02:00
MarekWoandClaude Opus 5 92117668fb feat(i18n): translate the Repeater Management panel (stage 3b)
~110 keys across repeater-manage.html and repeater-manage.js, finishing stage 3.
Catalog is now 243 keys, pl at 100%.

The interesting part is the three declarative schemas — TOOLS, SETTINGS_SECTIONS
and REPEATER_ACTIONS — whose title/label/help/note/desc fields held English
prose. Those fields now hold catalog KEYS, resolved with t() at each render site,
with a comment on each array saying so. The firmware `key` next to them
(radio.rxgain, advert.interval) is untouched: it is a CLI parameter, not text.

That created a checker gap worth naming: a key reached as t(f.label) is invisible
to a scanner looking for t('literal'), so all 57 of them would have been reported
unused. The scanner now also treats a bare string literal that exactly matches a
catalog key as a reference. It only relaxes the unused-key warning — missing-key
errors and the markup lint still come from real call sites, so a typo in a schema
key still surfaces, just at runtime rather than in the checker.

Trimmed the do-not-translate glossary from 19 terms to 10. It fired four
warnings on this slice and three were wrong: "direct neighbours" is an adjective,
not the Direct mode; "telemetria" and "czujniki" are simply the Polish words.
A warning you are supposed to ignore teaches you to ignore all of them, so the
list now holds only jargon whose native translation would stop an operator
matching the UI against firmware output — flood, hop, advert, ACK, RSSI, SNR,
LoRa, MQTT, pubkey, repeater. The rest stayed as guidance in the translator doc.
"flood advert" kept the word in Polish for that reason.

Bulk edits were applied by a script that asserts an exact hit count per
replacement and writes nothing if any count is off — which caught one case where
I had miscounted 7 occurrences as 6.

Verified in the browser: all 57 schema-resolved keys resolve in both languages
(none falls back to its own key), and the page title and toolbar render Polish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 19:00:34 +02:00
MarekWoandClaude Opus 5 93c74d150f feat(i18n): translate the My Repeaters panel (stage 3a)
~100 keys across repeaters.html and repeaters.js: the list, the add picker, the
password/login flow, the path editor, the map picker, and every toast.

Boundary call made here and written into docs/translations.md: "path" is
translated ("ścieżka"), because it is a word the user reads, not a value the
device reports. The mode names it can hold — Flood, Direct, FLOOD — stay as-is,
as do hop hex, repeater names and login roles. The rule is now stated explicitly
in the translator guide, since "Skonfigurowane path" is exactly the kind of
half-translation that makes a UI worse than leaving it in English.

Removed a third copy of relative-time formatting. Stage 0b merged app.js and
dm.js's formatTime but I grepped for the wrong names and missed
formatRelativeTime; repeaters.js had its own, with a "Never" branch that was
unreachable because its one call site already guards a falsy timestamp. It now
uses the shared formatTimeAgo. contacts.js and dm.js still carry their own
copies — noted in place, folded in with stages 5 and 6.

Two checker fixes, both found by this slice:
- Keys chosen inline, tHtml(x ? 'a.b' : 'c.d'), were invisible to the scanner.
  That produced false "unused key" warnings and would have silently hidden a
  typo in either branch. Added a second pattern for that shape.
- The generic en/pl diff caught two real misses I had made — the page <title>
  and the toast header, both still reading "My Repeaters". Swept every other
  template for the same pattern; the remaining ones belong to later stages.

Verified: checker clean, and the diff now reports every comparable string on
/repeaters as changed between en and pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:49:13 +02:00
MarekWoandClaude Opus 5 05f502ee53 feat(i18n): translate the System Log panel (stage 1)
First vertical slice: /logs chrome in English and Polish. Small on purpose —
~13 strings — but it exercises every part of the mechanism end to end: the head
include, the catalog route, the cookie, server-rendered t()/tn(), client-side
t()/tHtml()/tn(), a JS-rendered plural, and a rebuilt <select>.

Translated: title, pause/clear tooltips, the two "All ..." filter options, the
search placeholder, the reset tooltip, the loading and load-failed messages, and
the entry counter.

Deliberately NOT translated, per the boundary in docs/translations.md: the log
lines themselves and the DEBUG/INFO/WARNING/ERROR level names. Those are
protocol-side identifiers — the level filter also compares against them, so
translating the labels would be a step toward breaking the filter. Marked with a
comment in the template so the next person does not "fix" it.

The counter is the interesting case: English has two plural forms and Polish
four, so "100 entries" is "100 wpisów" while "2 entries" is "2 wpisy". Rendered
by tn() through Intl.PluralRules on the client and the matching rule table on
the server, which agree.

Also fixed in scripts/i18n_check.py: it warned when a markup-free value was used
via tHtml(), which is wrong and actively harmful advice. What tHtml() buys at an
innerHTML sink is param escaping, not markup support — the warning would have
pushed people toward t() in exactly the place where t() is the XSS hazard. The
error in the other direction (markup used via t()) stays. Output streams are now
forced to UTF-8 so `--missing pl > todo.txt` produces a usable file on Windows
rather than mojibake.

Verified in the browser: every chrome string differs between en and pl, level
names unchanged, no catalog key leaked into the page, the count stays translated
after filtering, and the filter bar wraps without overflow at 360px in Polish.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:49:09 +02:00
MarekWoandClaude Opus 5 2ec2f461e2 feat(i18n): add UI translation infrastructure (stage 0)
Groundwork for translating the interface. No strings are extracted yet — this
stage only adds the mechanism, so the UI is byte-identical in English.

Translations are one flat JSON file per language. An admin can add a language by
dropping <lang>.json into $MC_CONFIG_DIR/translations/ and picking it in
Settings > Appearance — no rebuild, no restart, no compile step. A drop-in file
overrides a built-in one of the same name. Built-ins: en (source) and pl.

How it works:
- app/i18n.py loads and merges catalogs over en.json, so per-key English
  fallback is baked in server-side and the JS runtime needs no fallback logic.
  Catalogs are fingerprinted with stat() per render, so a dropped-in file is
  live on the next refresh rather than the next restart — which matters,
  because restarting drops the device connection for up to 60s.
- Template text renders server-side via t()/t_html()/tn() from inject_globals(),
  so the first paint is already correct. That context processor covers every
  render_template() in the app, including the six standalone iframe pages, so
  routes/views.py needed no changes.
- JS text comes from /i18n/<lang>.<hash8>.js, a blocking immutable script shared
  by all 8 entry points. The hash is in the path, not a query string, so
  intermediary caches and the service worker bust reliably.
- Language = per-browser mc_lang cookie over a server-wide DB default. The
  cookie is what makes iframes work: they are same-origin, so they send it
  automatically and the existing modal-open reload wiring needs no changes.

t() deliberately does not escape — Jinja autoescape handles that, and escaping
here would double-escape every French apostrophe. t_html()/tHtml() escape their
params but trust catalog markup.

Also fixed along the way:
- save_ui_settings() replaced the whole settings blob instead of merging, so a
  language-only POST would have wiped the toast settings. It worked before only
  because the one form always submitted every key.
- `const t = document.getElementById(...)` in populateUiSettingsForm shadowed
  the global translation helper; same for `var t` in six theme IIFEs.
  scripts/i18n_check.py now fails the build if that pattern comes back.
- Removed app/templates/contacts.html, dead since the contacts pages were split
  (no render_template reference anywhere), and two stale CSS comments.

Verified locally: catalog route headers/security, drop-in without restart,
malformed catalogs logged and skipped rather than offered in the picker,
cookie propagation into iframes, and 360px layout. Python and JS agree on
interpolation, escaping and Polish plural categories.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:29:41 +02:00
MarekWoandClaude Opus 5 ed2cf07e73 fix(release): refuse to tag when main has not been pushed
Pushing the tag drags the commit objects to GitHub, so the release page
looks correct while the main branch still points at older code - exactly
what happened on the 2.1.0 release, where origin/main stayed on the
previous merge. The script now fetches origin/main and refuses unless the
local HEAD matches it, naming the push command.

Also spells out what to do when VERSION still carries a -dev suffix,
since the format check alone did not say which way to resolve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:41:39 +02:00
MarekWoandClaude Opus 5 4c6857a00f feat: numbered releases (2.1.0) with git tags and GitHub releases
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>
2026-07-26 19:20:57 +02:00
MarekWoandClaude Opus 4.7 422e7a3b34 feat(watchdog): catch sluggish-device failures via soft-pattern counting
The container watchdog only restarted on three legacy "device clearly dead"
log lines, so today's failure mode (firmware briefly stalls and get_stats_*
/ get_battery commands time out with an empty error while passive RX
keeps working) never tripped it — leaving the user with 10-15 s freezes
several times a day and no automatic recovery.

DeviceManager now tracks two liveness signals:
- _last_rx_at, bumped on every RX_LOG_DATA event
- _consecutive_stats_failures, incremented on get_stats_* / get_bat
  exceptions and cleared on success

New /health/strict endpoint exposes these to the watchdog. It returns 503
when the device is connected but has 5+ consecutive stats failures, or
when no RX event has been seen for over 5 minutes on a serial transport.
The cheap /health endpoint keeps its lenient behavior so Docker's
healthcheck doesn't suddenly start tripping.

The watchdog's check_device_unresponsive() gains a "soft" pattern class
with a count threshold of 5 in the last 2 minutes — matching against
get_stats_core/radio/packets failed:, Failed to get battery:, and
Failed to get channel. Hard patterns still trigger on a single hit.

Deploy note: the watchdog runs as a host-level systemd service and is
NOT restarted by mcupdate, so after deploy run:
  sudo systemctl restart mc-webui-watchdog.service

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 09:43:43 +02:00
MarekWoandClaude Opus 4.6 c36d7b5fbf fix(ble): simplify reconnection — rely on container restart for clean state
In-container BLE reconnection is unreliable because bleak leaves stale
GATT notification handles after abnormal disconnect, and adapter power-
cycling from within Docker corrupts bleak's internal BlueZ manager state.

New approach:
- On BLE disconnect or keepalive failure, immediately mark as permanently
  failed (no in-container reconnect attempts)
- Health endpoint returns 503, Docker healthcheck triggers container restart
- Docker entrypoint script disconnects stale BLE connections before app
  starts, ensuring clean GATT state for bleak

This is reliable because:
- MeshCore.create_ble(address=...) works on fresh container starts
- The BlueZ daemon on the host maintains adapter state correctly
- Container restart is fast (~5s) and gives a truly clean BLE state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 16:39:03 +02:00
MarekWoandClaude Opus 4.6 710f69c350 feat: add BLE transport support for companion devices
Integrate meshcore library's BLE connection (via bleak) as a third
transport option alongside serial and TCP. Priority: BLE > TCP > Serial.

Config: MC_BLE_ADDRESS and MC_BLE_PIN environment variables.
Docker: bluez/dbus packages, NET_ADMIN cap, D-Bus socket mount.
UI: transport type badge in navbar, transport_type in /api/status.
Watchdog: skip USB reset for BLE connections (same as TCP).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:03:45 +02:00
MarekWoandClaude Opus 4.6 33a71bed17 refactor(ui): rename contact type label CLI to COM (companion)
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>
2026-03-21 14:37:30 +01:00
MarekWo c1b0085710 feat(watchdog): skip USB reset if TCP connection is used
Since mc-webui can now connect via TCP to a remote proxy instead of local USB/serial device, the hardware USB bus reset logic in Watchdog will no longer blindly attempt a reset on repeated container crashes.

Added \is_tcp_connection()\ helper to read the config and conditionally skip the USB reset if TCP is active.
2026-03-06 09:53:06 +00:00
MarekWo 5c47a5b617 fix(watchdog): add logical USB unbind/bind and authorized toggle
When a native USB ESP32 device freezes, ioctl reset or DTR/RTS is often ignored. This uses sysfs unbind/bind and authorized toggles to forcefully drop the device from the kernel logic, causing it to re-enumerate cleanly without physical power cycles.
2026-03-03 20:28:40 +00:00
MarekWo 02b75c167b fix(watchdog): add ESP32 hardware reset via DTR/RTS
Since a standard USB bus reset often isn't enough to revive a hung ESP32, this adds a serial DTR/RTS toggle sequence (used by esptool) to physically reset the chip before trying a USB bus reset.
2026-03-03 20:20:52 +00:00
MarekWo d079f97a38 fix(watchdog): stop container before resetting USB bus
This prevents the container from holding the serial port open during the hardware reset, which was causing the reset to fail or the device to re-enumerate on a different port.
2026-03-03 20:13:19 +00:00
MarekWo ad8c5702f9 feat(watchdog): monitor mc-webui logs for unresponsive LoRa device
The v2 branch consolidated meshcore-bridge into mc-webui. Watchdog now:
- Monitors mc-webui logs for specific device connection errors
- Automatically restarts the container when errors are detected
- Performs a hardware USB bus reset if errors persist across 3 restarts
- Updated README.md to reflect the removal of meshcore-bridge
2026-03-03 20:01:46 +00:00
MarekWoandClaude Opus 4.6 e98acf6afa feat(v2): Add pkt_payload to DMs, update watchdog for single container
- Add pkt_payload column to direct_messages table for stable packet
  hash generation and Analyzer URL linking
- Update insert_direct_message() and DeviceManager to store pkt_payload
- Add test for DM pkt_payload storage (43 tests pass)
- Update watchdog to monitor only mc-webui (meshcore-bridge removed)
- USB reset trigger now fires for mc-webui container failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:01:43 +01:00
MarekWoandGemini CLI 2f82c589c7 feat(watchdog): Hardware USB bus reset for stuck LoRa devices
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>
2026-02-22 20:15:27 +00:00
MarekWoandClaude Opus 4.6 eb303c35ad fix: Filter meshcli prompt lines to eliminate false WARN results
Prompt lines (DeviceName|* ...) and summary lines (> N contacts)
are normal meshcli output, not format changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 10:22:37 +01:00
MarekWoandClaude Opus 4.6 bb0937e52a fix: Show unparsed line content in WARN messages for easier diagnosis
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 10:20:40 +01:00
MarekWoandClaude Opus 4.6 527204ea87 fix: Support piped execution for compat checker (env vars instead of argparse)
The script runs from host piped into the container, so argparse
doesn't work with stdin. Use env vars (BRIDGE_URL, FULL) as primary
config with fallback CLI arg parsing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 10:16:26 +01:00
MarekWoandClaude Opus 4.6 47877fb9e1 feat: Add meshcore-cli compatibility checker script
Diagnostic tool that tests all meshcli commands and response formats
used by mc-webui against a running bridge instance, detecting breaking
changes early when updating meshcore-cli versions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 10:05:40 +01:00
MarekWoandClaude Opus 4.5 aa788d7a0b feat: Add auto-start for stopped containers in watchdog
- Added AUTO_START option (default: true) to automatically start
  stopped containers, not just restart unhealthy ones
- Added handle_stopped_container() function
- Updated documentation with new configuration option

Set AUTO_START=false to disable automatic starting of stopped containers.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 14:05:51 +01:00
MarekWoandClaude Opus 4.5 96efc2a716 docs: Add watchdog documentation and fix executable flags
- Added docs/watchdog.md with installation and usage guide
- Added watchdog reference to README.md documentation table
- Fixed executable permissions on watchdog scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:43:17 +01:00
MarekWoandClaude Opus 4.5 73e1c63083 feat: Add container watchdog service
New systemd service that monitors Docker containers and automatically
restarts unhealthy ones. Features:

- Checks container health every 30 seconds
- Captures logs before restart for diagnostics
- Saves diagnostic files to /tmp/mc-webui-watchdog-*.log
- HTTP status endpoint on port 5051
- Restart history tracking

Install with: sudo ./scripts/watchdog/install.sh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 13:39:08 +01:00
MarekWoandClaude Opus 4.5 a764c66070 fix: Improve update UI and install script
- Add sudo to journalctl command in install.sh help text
- Move "Update now" link below version number to prevent line wrap
- Add "What's new?" link in update modal pointing to GitHub commits

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 08:18:14 +01:00
MarekWoandClaude Opus 4.5 dd7bdca803 fix: Improve updater install script and revert test commit
- Add -u flag to Python for unbuffered logging to journald
- Configure git safe.directory automatically during install
- Revert test marker from base.html

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 21:38:04 +01:00
MarekWoandClaude Opus 4.5 df852a1a80 feat: Add remote update capability from web GUI
Adds webhook-based update system that allows triggering updates
directly from the mc-webui menu. Includes:
- Webhook server (updater.py) on port 5050
- Systemd service and install script
- API proxy endpoints for container-to-host communication
- Update modal with progress tracking and auto-reload

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 16:58:05 +01:00
MarekWoandClaude Opus 4.5 0dc66b8f3c feat: Add update script for easier upgrades
- Add scripts/update.sh with colored output and error handling
- Automates: git pull, version freeze, docker compose rebuild
- Update README with script usage and alias instructions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 16:27:18 +01:00