12 Commits

Author SHA1 Message Date
MarekWo 05ec5fa13e feat(https): optional HTTPS front end via Nginx Proxy Manager
Adds an opt-in HTTPS layer without touching anything for existing installs.

The proxy is a service in docker-compose.yml behind the "https" Compose
profile, activated with COMPOSE_PROFILES=https in .env. That keeps the whole
setup inside files git owns while the switch lives in a file it ignores --
important because scripts/update.sh runs `git pull`, so any user edit to a
tracked compose file would break the next update with a merge conflict.
mcupdate needs no change: compose reads COMPOSE_PROFILES from .env itself.

Both services share the project's default network, so the proxy forwards to
http://mc-webui:5000 internally; MC_BIND_ADDRESS lets the plain-HTTP port be
restricted to loopback once HTTPS works, and MC_TRUST_PROXY turns on ProxyFix
so the app sees the real client address and scheme.

Also fixes copying over plain HTTP. navigator.clipboard only exists in a
secure context, and 9 call sites across 5 entry points used it with no
fallback, so copy buttons silently did nothing on a LAN address. They now
share clipboard-utils.js, loaded via _head_i18n.html (the one include every
entry point already has), which falls back to execCommand.

Verified locally against the real stack: self-signed cert uploaded to NPM,
proxy host created, app served over HTTPS with socket.io upgrading to a real
wss:// WebSocket (transport "websocket"), and the clipboard fallback checked
end-to-end by pasting back what it copied over http://.

Two findings from that run are documented in docs/https-setup.md:
NPM 2.15 dropped the default admin@example.com account for a setup wizard,
and its default 443 server now refuses connections without SNI -- which is
every connection made to a bare IP address. docker/npm-default-site.conf is
the opt-in way around that, mounted writable (a :ro mount aborts s6 init and
leaves nginx not listening at all).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:58:03 +02:00
MarekWo fbd6820df0 fix(i18n): finish repeater management, contacts, path analyzer (stage 9d)
An end-of-stage-9 sweep over every JS file found ~85 user-facing strings that
stages 3-5 never extracted, because their browser diffs only ever opened the
states those pages start in.

Most of it is repeater-manage.js: the entire login flow (prompt, progress,
failure hint, password modal), the reboot and radio-change confirmations, the
"Updated Xs ago" labels and fmtHeardAgo, the Status/Telemetry/Neighbours/CLI
tool chrome and the Status table's section headings and row labels. Plus
contacts.js (auto-cleanup status, push/move confirmations, bulk progress
counters, QR error), path-analyzer.js (map popups, table tooltips, the map
toggles) and two strings in repeaters.js.

Row labels in the Status table go through t(), not tHtml(): statusSection()
already runs esc() over the title and every label, so tHtml would escape twice.

Protocol terms stay English as before: flood/direct in the packet counters,
the CLI quick commands and the Cayenne LPP sensor type names.

Verified with F:\tmp\pwverify\i18n-stage9d.js. Two strings come back identical
and both are correct: "Hop" is glossary, and "Repeater not in list" is backend
error text, which is out of scope by decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 15:20:00 +02:00
MarekWo 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
MarekWo 4e8292bd03 refactor(i18n): consolidate date/time formatting (stage 0b)
Prerequisite for string extraction. formatTime() existed twice with subtly
different behaviour, and both hardcoded 'Yesterday ' — a string that would have
survived a naive extraction pass and stayed English forever.

app/static/js/datetime-utils.js now owns the today/yesterday/older logic, with
options for the two variants that actually differed: app.js needs an absolute
date in archive views, dm.js needs the short "5 Aug" form because DM rows are
narrower. Both keep a thin local formatTime() wrapper, so their ~15 call sites
are untouched. formatTimeAgo() moves over as-is (it only ever existed in app.js).

repeater-manage.js's fmtInt() no longer forces toLocaleString('en-US'), so
thousands separators follow the reader's locale. Kept as a function declaration
rather than a const alias so it stays hoisted, like the code it replaced.

Policy, documented in the module header: numeric formatting follows the BROWSER
locale, only the words are translated. Tying the clock to the UI language would
flip a Polish operator to "09:53 AM" the moment they switched the interface to
English, and mesh operators want 24-hour time whatever language the menus are in.

First five real catalog entries: common.yesterday, just_now, minutes_ago,
hours_ago, days_ago. days_ago exercises the plural machinery — Polish needs
"1 dzień temu" / "3 dni temu" / "5 dni temu" where English has one form.

Verified in the browser in both languages: all four helpers global, both
formatTime wrappers still routing through the shared core, Polish plural
categories correct, and the clock and number formats provably unchanged by the
UI language. Live chat timestamps render identically to before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:42:34 +02:00
MarekWo f54dc1d481 feat(repeaters): prefill saved password on login retry, map location picker, tidy list
- Login retry prompt now prefills the saved password (a wrong stored
  password and an unreachable repeater are indistinguishable, so a
  connection-caused failure no longer forces retyping). Adds
  GET /api/repeaters/<pk>/password for the trusted local UI.
- Repeater list: "last login" moves to its own line so a long path keeps
  the full row width on narrow phones.
- Settings -> Location: "Pick from map" button opens a Leaflet picker;
  clicking the map fills lat/lon and marks the section dirty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:27:13 +02:00
MarekWo a036d6af6c feat(repeaters): Actions tool (stage 8)
- POST /api/repeaters/<pk>/action {action}: whitelisted one-shot commands
  via repeater_cmd_wait — zerohop_advert (advert.zerohop), flood_advert
  (advert), clock_sync (clock sync), reboot; admin-gated
- Replies surfaced verbatim with an ok flag (reply starts with OK), so
  firmware refusals like "ERR: clock cannot go backwards" show as-is
- reboot special case: the firmware restarts without ever replying, so a
  clean send followed by silence reports success ("Reboot command sent")
- Actions pane: action rows with inline result lines, flood advert
  styled as warning ("Not recommended - high network load"), Danger zone
  card with confirm()-guarded Reboot and a muted note that erase is only
  available on the USB serial console (firmware restriction)

Verified live against PL-KRA Wegrzce 2: zero-hop advert replied
"OK - zerohop advert sent" and the advert arrived back at our device
(log: Advert from 'PL-KRA Wegrzce 2' type=2); clock sync surfaced the
firmware refusal; reboot confirm-cancel path checked in Playwright
(reboot itself intentionally not executed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:22:48 +02:00
MarekWo 92f5a7edca feat(repeaters): Settings tool (stage 7)
- GET/POST /api/repeaters/<pk>/settings?section=X: sequential text-CLI
  get/set batches via repeater_cmd_wait; per-field values/errors on read,
  per-field ok|failed|reboot_required classification on write (reply
  starting ok/password now = ok, mention of reboot = reboot_required);
  admin-gated via new _require_repeater_admin helper (also reused by /cli)
- Settings pane: accordion with 8 sections (Basic, Radio, Location,
  Features, Network health, Advertisement, Operator info, Advanced),
  lazy-loaded on first expand (each field is one mesh round-trip),
  per-section Refresh/Apply, dirty tracking with header badges
- Field types: on/off switches, 0/1 switches, select (loop.detect),
  composite radio (freq,bw,sf,cr) with reboot warning + confirm dialog,
  write-only admin password (updates the saved password on success),
  owner.info textarea with pipe-to-newline mapping
- Failed writes keep the field dirty and surface the firmware reply
  (e.g. "Error: interval range is 60-240 minutes") under the input
- Strip "%" suffix when loading number fields (get dutycycle -> "70.0%")

Verified live against PL-KRA Wegrzce 2: all 8 sections read cleanly,
flood.max 64->63->64 write round-trip, firmware range rejection surfaced
per-field, radio confirm-cancel path, dutycycle % parsing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:43:16 +02:00
MarekWo 3e80ea0c85 feat(repeaters): CLI tool (stage 6)
Remote text console for the managed repeater. Core mechanism:
repeater_cmd_wait() sends the command and synchronously waits for the
reply — CLI replies arrive as CONTACT_MSG_RECV txt_type=1 with no
protocol-level correlation, so correlation = repeater lock (single
command in flight) + sender-prefix match. A single-slot waiter is
checked at the top of _on_dm_received: matched CLI replies are
consumed there and never stored as chat DMs; unmatched ones (e.g.
console fire-and-forget cmd) keep the legacy DM behavior. Wait time
derives from the device-suggested timeout (10-45 s clamp).

POST /api/repeaters/<pk>/cli is admin-gated (403 for guest sessions:
firmware silently drops guest text commands, which would look like a
timeout). Pane: dark terminal styled after the Console module, quick-
command chips, Enter to send, per-repeater arrow-key history in
localStorage, elapsed-time line, inline timeout errors (lost replies
happen over radio — a manual retry typically succeeds).

Settings (stage 7) and Actions (stage 8) will reuse repeater_cmd_wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:45:51 +02:00
MarekWo 2b6d82a11c feat(repeaters): Neighbors tool with map view (stage 5)
List view: all zero-hop neighbours (fetch_all_neighbours paginates the
firmware's ~14-entry pages), rows show resolved contact name (or
[pubkey prefix] for unknown repeaters), heard-ago and SNR; count in
the toolbar. Names/positions are enriched server-side by prefix-
matching device contacts with the DB contact cache as fallback.

Map view (List/Map toggle, shown only when something is mappable):
Leaflet with the managed repeater as a red marker, positioned
neighbours in green, dashed connection lines labeled with permanent
SNR tooltips, and a footnote counting neighbours without a known
position. Toggle hidden entirely when no coordinates exist.

Verified live on PL-KRA Wegrzce 2: 37/37 neighbours fetched, 20
mappable, SNR labels rendered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:23:43 +02:00
MarekWo 995cb49f4c feat(repeaters): Telemetry tool (stage 4)
All Cayenne LPP channels shown at once (no channel dropdown like the
standard app): one card per channel with typed rows — icon, name,
value and unit (voltage V, temperature °C, current A, power W,
humidity %, GPS lat/lon+alt, etc.). Channel 1 is labeled 'device'
(repeater's own vitals). Refresh + updated-ago label; timeout renders
inline with a retry button (exercised for real on a 2-hop repeater —
first attempt timed out, retry succeeded with 2 channels).

Backend: repeater_req_telemetry in device_manager (serialized under
the repeater lock; the old name-only request_telemetry stays for
sensor nodes) + login-gated GET /api/repeaters/<pk>/telemetry.
GPS values arrive as {latitude, longitude, altitude} objects from
meshcore 2.3.7 — formatter handles both object and array forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:36:02 +02:00
MarekWo 707fd20426 feat(repeaters): Status tool (stage 3)
First management tool. The Status tile auto-fetches on open and shows
a compact three-section table (not MO's oversized tiles):
- System: battery %/V (linear 3.3-4.2 V estimate), uptime, repeater
  clock, queue length, debug/error events
- Radio: last RSSI/SNR, noise floor, TX/RX airtime
- Packets: sent + received (flood/direct split), duplicates, RX
  errors, channel utilization computed client-side as
  (tx_air+rx_air)/uptime (matches the firmware's own 10.09% reading)
Refresh button + "updated Ns ago"; errors render inline with retry.

Backend: GET /api/repeaters/<pk>/status and /clock, both gated on an
existing login session (401 need_login) to fail fast instead of a
2-min timeout. Clock loads as a follow-up request so the table
appears immediately, then the clock row fills in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:16:58 +02:00
MarekWo f288586ea6 feat(repeaters): Repeater Management panel (stage 2)
/repeaters/manage?pubkey=... — per-repeater management panel opened
automatically after login from the My Repeaters list:
- header card: name, shortened pubkey with copy, current path,
  location, ADMIN/GUEST badge from the captured login session
- tools grid (Status / Telemetry / Neighbors / CLI / Settings /
  Actions) with pane placeholders; CLI+Settings+Actions are locked
  for guest logins (firmware accepts text CLI from admins only)
- auto-login with the saved password when the in-memory session is
  gone (e.g. after app restart), password-modal fallback with retry
- REST: GET /api/repeaters/<pk> (merged entry + session state),
  GET .../session, POST .../logout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 07:24:52 +02:00