`get radio.fem.rxgain` returned "unknown config key" from the mock, which reads
as the terminal offering a command that does not exist. It does exist: CommonCLI
implements get and set for it, gated at runtime by Board::canControlLoRaFemLna()
rather than compiled out, so the command is present in every build and the board
answers for itself — "Error: unsupported" where there is no front-end module.
Auditing the whole table found 31 of 70 config keys unanswered, all the ones no
portal form drives: alert.*, bridge.*, owner.info, path.hash.mode, dutycycle and
the rest. Plus 14 verbs (gps, powersaving, sensor, region, clock sync) with no
handler at all. They now live in a "cli" section of the mock config, typed
through the existing lookup tables and stripped from /api/config, which does not
carry them.
Two real bugs behind that:
- the `set` path gated on whether a key was *readable*, so write-only and
computed keys (prv.key, dutycycle, radio.fem.rxgain) were rejected as
unknown. apply_set now owns that decision alone.
- apply_set accepted anything it did not recognise and replied OK. That
leniency is what let the gap hide: a CLI `set` on an unknown key looked
like it worked. It is strict now — verified against every key in
WC_ALLOWED_SET_KEYS so the form batch is unaffected.
Also mqtt.neighbors / mqtt.neighbors.interval, which the MQTT tab binds but the
mock's config never carried, so that toggle could not round-trip.
webconfig_cli_audit.py keeps the two honest: it drives every command the
autocomplete table offers through /api/cli and fails on anything unanswered.
119 commands, all answered.
"meshcore@<node>" borrowed the user@host convention without the referents:
there is no user concept here, and the node name is already in the page header
directly above, larger. It was decoration duplicating what was on screen — and
it crowded the header enough that `help` had to be hidden below 430px.
Removing it fits all three buttons on a 375px phone, so that media query goes
with it.
Operators who already have a prepared config want to paste it, not tap through
four wizard steps. A quiet `>_` chip in the setup-mode header drops straight
into the terminal; everyone else still sees only the wizard.
That makes the CLI reachable in setup mode, which the previous commit had
deliberately blocked. Setup mode authenticates by proximity to the AP rather
than by password — but the wizard already sets the admin password and rewrites
the node's radio config from there, so the trust boundary is the AP either way,
and refusing the console would only push these operators back to serial.
Onboarding by paste does skip the one thing the wizard makes mandatory: the
admin password, which /api/config enforces before it will arm a reboot. Nothing
in CommonCLI enforces it, so the terminal says so on entry, and confirming a
reboot without a `password` command having run warns again. That is a client-
side reminder, not a gate; wiring the real gate belongs with /api/cli on-device.
The console is a one-way door out of the wizard otherwise, so its header grows
a "← setup" button that goes back.
The generator gzipped webui/index.html verbatim, so the page's comments — and
this file is commented heavily by house style — were paying flash rent. A
line-based pass now drops comments, indentation and blank lines before
compressing. The source stays as readable as it was.
Conservative on purpose: only a comment that starts its own line is removed, so
a `//` inside a URL or a `/*` inside a regex can never be mistaken for one.
Line breaks survive, which leaves JS statement boundaries (and the space a
newline contributes between HTML inline elements) exactly as written.
This ships to thousands of devices, so it is not taken on trust:
- check_stripped() fails the build if the page's structure changed or the
output shrank implausibly
- the pass lives in its own module, shared with the mock backend's new
--minify flag, so the bytes exercised in a browser are the bytes that get
embedded rather than a second implementation that could drift
- webconfig_minify.py joins the generator in the freshness hash, so editing
the stripper forces a regenerate
Today's page: 22,678 -> 17,671 bytes gzipped.
Design prototype, driven entirely by the mock backend — nothing here runs
on-device yet.
The portal's form batch is deliberately allowlisted (WebConfigKeys.h), which
leaves everything the serial console can do unreachable from a browser. This
adds a fifth tab holding a real terminal: monospace white-on-black in either
colour scheme, autocomplete over the full ~270-command surface, in-session
history, and a confirmation step for pasted command sequences.
Autocomplete goes past the flasher's <datalist>: rows carry descriptions, Tab
extends to the longest shared prefix before committing to a match, and once
`set <key> ` is complete it switches to completing the VALUE — enums from the
command table, broker presets from /api/presets, packet-type names per CSV
segment. The table is generated from a key list rather than written out per
slot, so mqttN.* tracks active_slots instead of being duplicated six times.
Pasting several lines never mangles the prompt: the lines are parsed (comments,
blank lines and pasted `>` prompts stripped), listed back numbered, and run only
after an explicit confirm. Commands that restart, erase, reflash or move the
node off its network get the same confirmation singly. History is memory-only —
`set wifi.pwd` and `password` pass through it.
/api/cli mirrors the config-save contract (202 + reqid, poll for results) for
the same reason: commands run on the node's main loop, not in the request. The
one difference is that results stream, so a long sequence fills the window as
it executes rather than landing all at once.
The tab is hidden in setup mode, where the portal authenticates by proximity
and no admin password exists yet.
Improve companion BLE delivery and idempotent remote CLI retries, make FPF7 persistence atomic with FPF6 migration, verify staged OTA data on resume, and clarify filter policy wildcard and BlackHole semantics.
Adds Station_G3_ESP32_repeater_observer_mqtt and
Station_G3_ESP32_room_server_observer_mqtt, mirroring the known-good
Station G2 observer envs. The two boards are the same family — ESP32-S3,
qio_opi, PSRAM, 16 MB flash, SH1106 display — so the overlay ports across
unchanged: default_16MB.csv partitions, adafruit-full cert bundle,
MQTT bridge + SNMP, and the same lib_deps pinning.
Verified the G3 partition signature is byte-identical to the G2 observer's,
so the OTA partition-compatibility gate treats them the same. Both envs
build clean (1.67 MB of the 6.25 MB app slot, 28% RAM) and produce the full
CI artifact set via build.sh: .bin, -merged.bin and .partsig. Observer env
discovery now finds 32 envs (was 30) and the ArduinoJson pin check covers
both new sections.
Docs: added the G3 build commands and the two partition-table rows to
MQTT_IMPLEMENTATION.md.