~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>
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>
- 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>
- 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>
height: max(260px, calc(100vh - 430px)) instead of a fixed 340px, so
the scrollback grows with the window while staying usable on small
screens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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>
/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>