Commit Graph
108 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Fable 5 aef80d18bb Repeater admin: set up remote nodes over the mesh
A new Device management entry for administering repeaters and room
servers. The shared credentialed-first picker (extracted from TX
optimize) chooses the node, login runs on remembered passwords with a
one-time prompt otherwise (forgotten on rejection), and the editor
lands in the local device-configuration screen's exact lanes — but
speaking the node's text CLI through a new public
Device.send_remote_command, which brings the repeater-only knobs the
local editor never had: TX delay, Direct TX delay, RX delay, airtime
factor, advert intervals, flood max, repeat, guest password.

Reads cost one paced mesh round trip each, so the editor never
bulk-reads on open: values show what the last read said, age-stamped,
from a new per-node remote.json cache; Read settings refreshes them
under an abortable progress dialog and Apply sends the staged sets
the same way, folding confirmed values back into the cache and
keeping refused ones staged. Advert, clock sync, password change, and
reboot run as confirmed popups.

The Command line is a readline-style screen onto the node's raw CLI:
per-node history persisted across sessions (↑↓ recall, adjacent-dupe
suppression, capped), Tab completion over the known verbs plus every
catalog get/set spelling with ghost-text preview, one command in
flight at a time. The simulator grows a full repeater CLI so all of
it drives against --mock, and a scripted one-shot
(repeater-admin <node> <command…>) covers automation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 04:14:16 -04:00
Jean-Pierre MartineauandClaude Fable 5 62585f5602 Dashboard: the live mesh overview leading the Mesh section
One full-screen, live view of everything going on around the node,
stacked coarsest first. Activity: a three-row braille bar chart of
every packet heard over the trailing two hours — the header
indicator's big sibling, drawn from the same monitor buckets — with a
pulse line reading the rate, nodes heard, and the window's busiest
transmitter. Traffic: session tallies by packet class with
proportional bars (the monitor grows per-kind counters). RF health:
median SNR on the trace tool's quality bar and RSSI extremes from the
stored window, beside the radio's own live numbers — noise floor,
last RSSI/SNR, airtime, battery — polled the way Device info reads
them. Feed: the latest packets in fixed lanes (time, class, node,
reception, relay path), seeded from stored history via a new
recent_observations window query so the screen opens full, then
streamed straight off the event hub.

Menu-only (it rides the hub and repaints every second); scripted
inspection stays with the monitor and nodes subcommands.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 04:00:16 -04:00
Jean-Pierre MartineauandClaude Fable 5 5046a86980 TX optimize rebuilt on the trace screen's armed-idle pattern
The old flow asked for a hand-typed comma-separated path, logged in
before showing anything, and started transmitting the moment the
screen opened, on parameters you couldn't touch. It now walks like the
trace feature: two pickers choose the link — the node to tune
(credentialed repeaters lead, wearing the key glyph), then the target
whose reception is optimized — and the screen opens armed but idle.

An action list drives it: Route opens the trace path composer pinned
at the tuned node (suggestions from observed links; direct by
default), Range/Step/Samples float small dialogs, and the Sweep row
always quotes the worst-case transmission count, so the cost of a
commit is on screen before anything goes on the air (coarse_levels
goes public to price it). Sweep logs in first — the password prompt
floats only when nothing is remembered — then runs coarse → refine →
verify under the trace tool's in-flight dialog with Abort, levels
landing in the bar chart behind it, best starred. Completion parks the
cursor on a new Apply-winner action row (the after-sweep dialog's
second chance), and a finished sweep leaves the screen armed: adjust
and sweep again, the chart clearing as a new measurement starts.

The scripted CLI keeps its one-shot flag set, table, and HTML chart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:51:01 -04:00
Jean-Pierre MartineauandClaude Fable 5 85fc82a606 Regroup the menu: Mesh, Device management, Tools
Messaging becomes Mesh and takes in Nodes (between Channels and Map —
the "who's out there" view) and Send advert (last in section: its one
action, after its views), so everything mesh-wide lives under one
heading. Device becomes Device management and keeps the box-level
tools; Diagnostics becomes Tools.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:35:00 -04:00
Jean-Pierre MartineauandClaude Fable 5 8ecfbc865d Send advert floats over the menu; the actions screen drops its copy
The main-menu advert entry ran the same flow as Device actions but
replaced the screen instead of floating over it. Tools can now declare
popup = True to run while the menu stays pushed — their prompts and
result draw as modal dialogs over it, the quit-dialog pattern — and
the advert tool does, so both doors into the flow looked and felt the
same. With the popup one keystroke away there was no reason to keep
the second door: the Device actions row is gone, leaving that screen
to the box-level operations (clock, backup, key, reboot, reset).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:32:47 -04:00
Jean-Pierre MartineauandClaude Fable 5 87d2d10dff Every main-menu item leads with its own emoji
Tools gain an icon attribute — menu-only, so result windows and the
CLI keep the plain title — and the menu loop composes it into the
aligned name lane. Single-codepoint, emoji-presentation glyphs only,
so the two-cell width holds across terminals: 💬 Chat, 📻 Channels,
🌍 Map, 👥 Nodes, 📡 Send advert, 🔧 configuration, 🔨 actions,
📋 info, 🎯/👣 the traces, 🎧 Monitor, 📶 TX optimize, 🚪 Quit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:27:35 -04:00
Jean-Pierre MartineauandClaude Fable 5 5027d05943 Header unread badge speaks the red-dot language
The envelope glyph gives way to the ● err-red dot with its warn-tinted
count — the same badge the channel list and conversation picker draw —
so unread reads identically everywhere it appears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:24:25 -04:00
Jean-Pierre MartineauandClaude Fable 5 b27af22f36 Header shows the node, its port, unread mail, and a mesh pulse
The status bar drops the static "device:" prefix and the heard
counters. The device segment now leads with the node's own mesh name —
the remembered registry name, then the profile alias, then the
hardware name — with where it's reached in parentheses (the COM port,
or BLE), cached per connection so repaints never re-read the registry
file.

The heard numbers give way to an unread badge (channels and direct,
shown only when something waits) and a two-hour braille activity
indicator: every packet the hub hears — adverts, telemetry, messages,
acks — lands in a five-minute bucket, drawn twelve characters wide at
1/5/21/89 thresholds with the ●/○ live-light in front. The braille
sparkline renderer moves from the channel manager to widgets so both
draw with one brush.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:23:32 -04:00
Jean-Pierre MartineauandClaude Fable 5 ce3c2d1e2e Background adverts on a per-device cadence, one send per pass
MeshTerm now announces the connected node by itself: a zero-hop advert
every 1/2/3/4 hours (default hourly) and a flood advert every
3/6/12/24/48/168 hours (default daily), each switchable off. The
cadences are staged and applied from a new Background adverts section
in the device-configuration editor, and persist per device (keyed by
public key) in adverts.json alongside the last-sent mark for each type
— so the countdown survives restarts, and a manual send from any
advert flow resets the same clock the scheduler reads.

The scheduler stays transmission-shy: at most one advert per 30 s pass
(a flood due together with a direct waits a pass), and a never-sent
device is armed to count from now rather than advertised immediately.
It holds no device subscription — each pass just skips when nothing is
connected — so reconnects need no special handling, and scripted CLI
runs never start it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:12:49 -04:00
Jean-Pierre MartineauandClaude Fable 5 529612559e Drop the View full configuration row from the editor
The editor menu already lays every setting out in the same
setting / value / description lanes as the table it opened, so the
Review section duplicated the screen it sat on. config_table itself
stays: the config CLI subcommand and Device info still render it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 03:01:23 -04:00
Jean-Pierre MartineauandClaude Fable 5 c50da836f9 Path walks arm from the last stored walk; new paths reset the stats
The path screen opened showing the previous walk's route yet refused to
trace it ("compose a path first"): the display seeded from history but
the spec never did. The last successful stored walk now feeds path
mode's auto route — verbatim, with "last walk" provenance — so the
route on screen is the route Enter walks.

Adopting a different spec (composed, explored, or re-rendered at a new
width) also clears the aggregates, per-hop medians, and trace log: they
described the old route. The screen-lifetime total the owner reports
survives the reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:37:55 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 d4853b9cfd Unlit meter segments dim to braille, not plain dots
The SNR quality bars' unlit remainder now reuses the same full braille
cell as a lit step, dimmed to a new dark-gray track style, instead of
a plain "." placeholder — the meter reads as one glyph system, filled
in from a background rather than dots giving way to blocks. The
boundary half-step still carries the reading's own colour, since a
single character can't be split between it and the track.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 01:13:43 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 dacdb043f2 Narrow the SNR quality bars: braille, 8 chars, 16-step resolution
The per-hop meters (shared by the trace and TX-optimize screens) drew
16 solid block characters. They now pack two fill steps into each of
8 braille characters, using only the middle two dot rows (2, 3, 5, 6)
so the glyph sits mid-cell instead of hugging the top or bottom edge
— half the width at the same 16-step resolution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-11 00:49:48 -04:00
Jean-Pierre MartineauandClaude Fable 5 c819fdfd2f Auto traces walk proven routes: the device almost never has one
An auto (device-routed) Trace target went on the air as a bare
destination hash — an observer confirmed no repeater hops — so any
target past a direct neighbour could never answer. Dumping the
radio's contact table showed why: every contact reports
out_path_len -1 (flood). MeshCore firmware only learns a contact's
out_path from two-way addressed traffic, and tracing repeaters
teaches it nothing, so "device-routed" has nothing to route with.

Auto now resolves a route the session can prove, in order: the
device's learned route when one genuinely exists, else the outbound
leg of the last successful stored walk (_previous_outbound extracts
it from the recorded boomerang), else a bare direct attempt. One
resolver feeds both the wire and the screen, so the planned route
line renders exactly what Trace will transmit, with its provenance
under it — (auto · device route), (auto · last trace · stamp), or
(auto · direct — no known route) — and the summary's path row says
the same instead of claiming a device route that never existed.

The connection layer's path-less fallback keeps the mirrored return
leg for contacts that do carry a learned route (the trace protocol
reflects nothing home), and its docstring now says the firmware
never routes a trace itself. Resolution verified against the real
database and radio: a multi-hop target resolves to its full stored
boomerang, the direct gateway stays a single-hop spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 00:24:16 -04:00
Jean-Pierre MartineauandClaude Fable 5 944634939f Back out of a trace to the menu; airier action rows
Esc (or Back) from the Trace target screen now returns to the main
menu instead of looping through the target picker. The action list
breathes: a blank line after Explore paths and after Sample count
groups the rows, and the previous-route stamp sits flush left under
the route instead of indented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:58:44 -04:00
Jean-Pierre MartineauandClaude Fable 5 0ded99883f Trace splits in two: Trace target vs Trace path, with paced sampling
A trace is one walked path and "target" is pure UX (hardware-verified),
so the feature now splits along that line: Trace target keeps the
symmetric mirror-and-return flow (picker, Explore paths), while the new
Trace path skips the picker entirely and walks a hand-composed route —
no Auto, no Explore, Trace gated until a path exists, history recorded
under the "(path)" sentinel so composed walks never pollute the target
picker. The composer loses its mode toggle: a pinned target means
mirrored, no target means verbatim, and its preview opens as us → us.

Sampling returns, deliberately: a Sample count action (1/2/3/5/8) runs
that many traces per commit, paced by trace_cooldown_s between sends and
counted off on the in-flight dialog — never a burst. Actions reorder to
compose/explore/width/samples/trace with a Back row; the w/p/x hotkeys
retire.

Display: route nodes drop the underscore (prompt_toolkit draws U+00A0 as
"_", so name (hash) now joins with plain spaces), the composer renders
nodes exactly like the route line, the (previous · …) stamp gets its own
line, the path row ends with the displayed hop count, Explore rows lead
with the route instead of an "observed path" label, and the empty Traces
placeholder is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:33:12 -04:00
Jean-Pierre MartineauandClaude Fable 5 22b182000b Route out one way, home another: asymmetric paths, width picker, actions
Four trace-feature upgrades, all UX-preserving:

- Stored traces recover their path-hash width from the (uniform) hop
  hashes at rehydration, so a target's previous route renders short
  hashes instead of the device's full 64-char public key.
- The trace screen's verbs are selectable action rows now — Trace,
  Path width, Compose path, Explore paths. The cursor opens on Trace so
  plain Enter still just traces; p/x/w hotkeys stay on their rows, and
  the cursor only pins the view while it's actually being moved, so
  PgUp/PgDn still read a long trace log freely.
- A floating path-width dialog picks the 1/2/4/8-byte hop addressing
  (each row previews the target key with the addressed slice lit, the
  device's routing width marked). Changing it re-renders a standing
  forced path — hops widened back through their canonical hashes — and
  shapes every spec composed or explored afterwards.
- The composer's new ⇄ row toggles symmetric (return mirrors the
  outbound, the current behaviour) against asymmetric, where the whole
  walk is composed by hand — target routed *through*, outbound
  repeaters reusable, only the landing back on us implicit. Toggling
  materializes the mirror into editable hops (or regenerates it from
  what precedes the target), so the wire route never changes under the
  toggle; the planned-route preview dims only palindromic return legs.

Also: the target now carries its hash in the composer preview like
every other node, and the composer's footer hint reads briefer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:07:40 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 fbcf13baf5 Forced traces walk the return leg too: no auto reflection
The trace protocol has no separate return-path field, so a forced
path (from the composer or an adopted scenario) has to spell out the
whole boomerang itself: outbound hops, the target, then those hops
mirrored back. We were only sending the outbound half — repeaters
never saw the return leg, so a forced multi-hop trace couldn't rely
on the destination reflecting it.

render_forced_spec() in topology.py is now the one place that builds
a forced-path spec; PathScenario.spec() and the composer's _spec()
both call it. The composer's live preview already showed the mirrored
return dimmed alongside the outbound hops - that display code stays,
but the trace screen's planned-route preview now splits the *actual*
wire spec at its midpoint instead of assuming it was outbound-only,
and the composer's reopen-with-current-path reseeding drops the whole
return leg instead of just the spec's last token.

Also normalized node-hash display in the composer to the spec's
path-hash width (matching the planned/completed route views) instead
of a hardcoded 6-hex-digit slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 22:42:08 -04:00
Jean-Pierre MartineauandClaude Fable 5 a65ce0ccf7 Exit in bounded time, connect BLE first try: teardown and startup hardening
Two long-standing flakes, one root cause each:

Quit with a serial companion often froze until the exit watchdog killed
the process. The meshcore dispatcher's stop() awaits queue.join(), but
its processor task exits after draining at most one event once stopped —
so a routine advert/RX-log burst (two+ events queued at that instant)
deadlocks the join forever (repro confirmed against the installed
library). MeshCoreDevice.disconnect now gives the graceful teardown two
seconds, then abandons it: the dispatcher task is cancelled synchronously
and the raw transport closed directly, so the port is still released and
even the forced path finishes well under the watchdog.

BLE startup often failed until the user re-selected the device. Connecting
by bare address makes bleak re-discover the peripheral with a fresh
internal scan, which on Windows intermittently misses a slow-advertising
companion. The discovery scan's live BLEDevice now rides along
(DiscoveredDevice -> probe/context -> create_ble) so the connect opens the
peripheral directly, and a transport-level ConnectionError is retried once
before surfacing — the manual re-select was never anything but a retry.
PIN/bond rejections still surface on the first attempt, unlooped.

Verified against hardware: BLE info read off the Waymarker connects and
exits cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:16:36 -04:00
Jean-Pierre MartineauandClaude Fable 5 ce133a321e One trace per action: retire bursts so repeaters never see us flood
Repeaters penalize (and can blacklist) nodes that burst traffic, so
every trace-transmitting surface now sends exactly one transmission
per human action: Enter on the live screen runs a single trace (the
screen still aggregates the session's medians), the scenario probe
measures each candidate path once, and the CLI drops --samples to
become a true one-shot. trace_runner.measure and the s-key sample
cycle go with it.

Also de-pollute the target picker's "Recently traced" section: the
repository now returns a short recency-ordered list instead of the
ever-growing all-time tally, and stored hex prefixes fold back to
their contact names so a node shows once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:51:04 -04:00
Jean-Pierre MartineauandClaude Fable 5 f1e1896918 Composer asks the gateway: fetch a repeater's neighbour table mid-path
The topology graph gains its fourth evidence source — the only one not
derived from our own reception. A repeater we hold admin rights on
reports its neighbour table (who it hears directly, at what SNR) when
asked over the mesh, revealing links and whole nodes our radio has
never received anything from: exactly the second vantage point the
composer runs dry without, two hops out.

Probed live against YUL-Cartierville (v1.15.0) through the Waymarker
before building: firmware answers the binary NEIGHBOURS request but
silently ignores guests, so the flow is login-first, tx-optimize style
(stored password, one-time prompt, forget on rejection). An empty table
is a normal answer — the gateway's own table was empty during the probe
(confirmed via its text CLI: '-none-') — so the UI says "ask again
later" instead of erroring.

Standing on a fetchable repeater, the composer shows a "fetch
neighbours" row with the suggestions it extends; the fetch runs under
the abortable spinner dialog, persists as a runs-row-owned snapshot
(schema v8, neighbour_reports), and the composer reopens mid-thought
with the same hops and refreshed suggestions tagged N. Refetching
supersedes the old snapshot per (repeater, neighbour) pair rather than
inflating the link's sample count: a table is current state, not an
event stream.

Verified end-to-end on hardware via MeshCoreDevice over BLE (login +
fetch + clean empty-table return) and in the mock (guest refusal,
discovery of a never-heard node, composer render + hand-off). 441
tests pass; the real database migrated v7 -> v8 cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:25:32 -04:00
Jean-Pierre MartineauandClaude Fable 5 3502a2e09d Trace opens idle: compose the path from received topology, probe scenarios
Everything we receive carries a fragment of the mesh's shape: successful
traces crossed every link in their path (out and back — the reply retraces
the reverse path, so links read both ways), each contact's out_path is a
route the firmware distilled from received floods, and RX-logged packets
report the relay chain they rode in on. A new topology service folds those
into one undirected evidence graph (schema v7 persists packet paths).

The trace screen stops auto-starting on target pick. Enter runs a burst
under a floating tracing dialog (spinner chip, live progress, Abort); "p"
opens a hop-by-hop path composer whose suggestions are the tail's observed
neighbours, strongest link first, with raw hex entry for unseen nodes; "x"
browses ranked path scenarios (device route, direct, best observed
alternatives) and can probe them all — a measured burst per candidate,
recorded as path_candidates rows, reliability-first ranking, winner offered
for adoption.

Composed specs are outbound-only, always ending at the target's hash: the
stored data shows the firmware auto-replies along the reversed path, and
user-composed "there and back" loops (3d,f2,3d) never succeeded. Specs
collapse to a uniform per-hop width the narrowest known hash can honour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 21:00:00 -04:00
Jean-Pierre MartineauandClaude Fable 5 9979f66f2a TX optimize sweeps live and asks to apply after the evidence is in
The six-prompt chain (path, samples, min, max, step, apply-up-front)
collapses to one: pick the path, and the live sweep screen takes it
from there. Levels land in an SNR bar chart as they are measured,
ascending by power with the current best starred, while the optimizer
announces its phase — coarse sweep, refining around the leader,
verifying — through a new on_phase callback. Reliability and trace
counts sit beside every bar, and a level nothing got through at reads
as an explicit no-reply row.

The apply decision moves to where it belongs: after the sweep, a
floating dialog offers the winner against the node's original power
(Cancel left, Apply right, Enter commits), and `a` re-offers it if
first declined. The sweep itself runs with apply off, so Esc at any
point stops the search and the optimizer's unwind restores the
original transmit power.

The sweep persists under one runs row (levels and traces alike), the
scripted CLI keeps its full flag set, chart, and --apply semantics,
and the tool moves in with Trace under Diagnostics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 05:24:21 -04:00
Jean-Pierre MartineauandClaude Fable 5 d8d87d5b50 Trace becomes a live screen: pick a target, watch replies stream in
The menu's trace flow joins the living-screen pattern (chat, nodes,
map). One recency-ordered picker — recently traced targets first, then
contacts by freshness — replaces the target/samples/path prompt chain,
and the screen opens tracing immediately: each reply lands in a
newest-first log as it arrives, the route line and per-hop medians
(with SNR quality bars) update live, and Enter / s / p run another
burst, cycle the burst size, and edit the forced path without leaving
the screen. Esc steps back to the picker, chat-style. The route line
opens seeded from the target's last stored trace, so the path history
last saw shows while the first reply is still in flight.

Every burst persists exactly like a scripted run — one runs row, each
trace recorded under it — and the scripted CLI keeps its one-shot
table output unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 05:24:06 -04:00
Jean-Pierre MartineauandClaude Fable 5 44a093c460 Retire Heard nodes, Link health, Link budget, Route map, and History
Five features leave the menu and CLI: the heard-nodes review, the
baseline-regression checker, the offline link-budget calculator, the
route-stability graph, and the run-history browser — along with their
services (link_quality, link_budget, route_stability), the pyvis route
renderer, and the pyvis/networkx dependencies.

The data layer is untouched where it still earns its keep: passive
monitoring keeps recording every overheard packet (the header counters,
Nodes, and Map read it, and `meshterm monitor` still captures a bounded
window from the CLI), run logging keeps writing `runs` rows, and trace
history stays queryable for the trace tools. Only the route-map-only
`all_traces` query goes.

Also fixes a latent tie-break flake in the nodes sort: ages are now
measured from one clock snapshot, so identical last-seen stamps really
tie and keep their A→Z order in both directions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 05:08:11 -04:00
Jean-Pierre MartineauandClaude Fable 5 ba1ff2eedf Sparkline becomes a 2-hour live pulse, newest at the left
The activity window shrinks from a week to two hours in 24 five-minute
buckets (12 braille cells), bucketed newest-first so "now" sits at the
left edge and traffic slides right as it ages. Bar heights switch from
peak-relative scaling to absolute tiers (1/3/8/21 messages per dot), so
the same traffic draws the same bar on every row, and silent cells drop
to a faint two-bottom-dot baseline — a quiet channel reads as a flatline
rather than a gap. The Reorder row's narrow ↕ arrow gains a second space
so its label aligns with the two-cell glyphs of its sibling rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 04:01:59 -04:00
Jean-Pierre MartineauandClaude Fable 5 96813130d5 Reorder screen slims to glyph+name rows and hugs its content
Enter is now the sole grab/drop key (Space is ignored), the rows drop
the openness/hash lanes since reordering is about position, and the
screen gains a dialog_width sized for its fullest state so the popup
fits its content without resizing mid-interaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:32:23 -04:00
Jean-Pierre MartineauandClaude Fable 5 8c75aea3b0 Reorder screen grows Apply/Back action rows below the list
Follow the device-config editor's exit-group pattern: once the order
has actually changed, an ok-tinted Apply joins an err-tinted
"Back — discard changes"; untouched, a lone Back. Enter on a list row
now grabs/drops like Space (matching Enter-activates-the-row), and
committing happens on the Apply row instead of Enter-anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:14:34 -04:00
Jean-Pierre MartineauandClaude Fable 5 1d62691599 Reorder screen gains OK/Cancel: Space grabs, Enter commits, Esc aborts
Esc used to be the only way out of the channel-reorder screen and it
always committed, so there was no way to back out of an accidental
shuffle. The screen now follows the other dialogs' Enter-accepts/
Esc-cancels contract: Space grabs and drops the highlighted row
(arriving as printable text from the session, with the normalized
"space" action accepted for symmetry), Enter resolves the final order
— dropping a still-carried row where it sits — and Esc resolves CANCEL,
which the session already maps back to the identity order so the
caller treats it as "no change". Footer hints re-phrase per state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:02:30 -04:00
Jean-Pierre MartineauandClaude Fable 5 7ed31c8f31 Activity lane becomes a 16-bucket braille sparkline
The three-threshold bar meter compressed a week of traffic into one of
four states; braille cells pack two dot columns of four rows each, so
the same 8-cell lane now draws a real 16-point histogram — each bucket
~10.5 hours — and a channel's rhythm (bursts, lulls, decay) is legible
at a glance. channel_stats() gains the per-bucket histogram, bucketing
the window's timestamps in Python off one filtered query; recent is
now simply the histogram's sum.

Bars scale to the row's own busiest bucket — the sparkline shows shape,
not cross-channel volume (the MSGS lane does that) — with non-empty
buckets floored at one dot so a lone message never vanishes. A silent
window keeps the muted ······· placeholder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:53:28 -04:00
Jean-Pierre MartineauandClaude Fable 5 df9cdc4f2f Channel list grows message vitals in config-editor lanes
The channel manager's flat name/hash rows become fixed, column-aligned
lanes under one header line — name, openness, hash, unread badge, total
messages, last-message age, and a three-bar activity meter over the
trailing week — so a glance shows which channels are alive, not just
which exist. A new Repository.channel_stats() aggregates totals, the
week's slice, and the last timestamp per channel identity in one SQL
pass; rows read it through a small TTL cache with callable titles, so
a message arriving while the list sits open updates its row in place
(the conversation picker's trick, unread badge styling included).

Both menus adopt the config editor's persistent-backdrop pattern: the
list stays pushed while every sub-prompt floats over it as a modal
popup instead of replacing the screen. The detail screen leads with a
vital-signs line (slot, totals, unread, last activity) and lays its
actions out in the Device-actions two-lane style, with the live unread
badge on Open in chat and the destructive Clear row tinted err.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:40:04 -04:00
Jean-Pierre MartineauandClaude Fable 5 0f49cf0a2d Passive monitoring is always on; CLI monitor becomes a timed capture
The on/off switch never made sense for a pure listener: recording is
now unconditional, so the toggle submenu, the persisted preference
(MonitorStore/monitor.json), and the header's ON/OFF framing are gone.
The monitor registers its hub subscription at startup — a device-free
step that cannot fail — and its run row opens lazily on the first
observation, so recording follows the hub wherever it opens (eagerly,
or deferred by connect_on_start) and silent sessions leave no empty
runs. The menu entry is now just the read side, "Heard nodes", and the
header shows live heard counters instead of a switch state.

meshterm monitor on the CLI drops --on/--off for a bounded foreground
capture (--seconds N, 0 = until Ctrl-C, mirroring chat listen): it
tails each overheard packet live and summarizes the window's heard
nodes when it ends, while persisting history exactly like a session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 02:07:23 -04:00
Jean-Pierre MartineauandClaude Fable 5 12188afaa0 Light every frame from its top-left corner
A new glow pass post-processes composed ANSI lines: it finds each
box's top-left corner glyph, lifts it toward white, and fades the
highlight quadratically along the top and left edges into the
border's own colour. It reads colours from the SGR stream itself, so
accent, muted, and warn frames each glow in their own hue and nested
tool panels are lit individually with no per-callsite wiring.

String surgery instead of a Rich parse/re-render round-trip keeps the
pass ~2ms per repaint. Replaces render_to_ansi_dialog, whose only
caller now composes through the glow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:51:59 -04:00
Jean-Pierre MartineauandClaude Fable 5 b0cd2b0ccf Anchor splash at 2/5 height so the populated picker reads centered
The banner was pinned so the box top sat at the terminal midline; the
box only grows downward, so a populated device list made the splash
bottom-heavy. Anchoring at 2/5 centers the grown block and leaves the
brief small states slightly high, the usual optical dialog placement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:43:18 -04:00
Jean-Pierre MartineauandClaude Fable 5 bca0d7f603 Aerate splash boxes; startup picker hint says Esc quits
The startup picker's footer read "Esc skip", but Esc there exits the
program (same as the Quit row) — say "Esc quit".

Extract the floating dialog's breathing-room rule (a blank row above
and below the body whenever it costs no visible content) into a shared
_breathing_room helper and apply it to the chromeless splash boxes too
(device picker, PIN dialog, message/spinner splashes), so every popup
box is aerated alike. Full-screen panels stay flush on purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:40:29 -04:00
Jean-Pierre MartineauandClaude Fable 5 3a972ea743 Short outcomes pop as OK dialogs instead of filling the result frame
A one-line result like "✓ flood advertisement sent" used to open the
full scrollable result window — a huge frame for a single sentence.
TuiUi.present() now collapses small, text-only output (at most 3 lines,
none wider than 76 cells) into a centered popup with an OK button,
floating over whatever screen is beneath; tables, long errors, and
multi-line output keep the window. Fixed centrally in present(), the
upgrade covers every such site at once: adverts, clock sync, backups,
key import, "applied N changes", tool failures, and future ones.

ButtonDialog learns to render a pre-styled (multi-line) Text prompt so
note markup survives into the popup, and the border echoes the message's
strongest tone (err > warn > accent). When the popup would sit on an
empty stack (a tool run from the popped main menu), a blank base frame
is pushed beneath it first — the same trick as the reconnect dialog —
so it draws as a floating box, not as the base.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:32:58 -04:00
Jean-Pierre MartineauandClaude Fable 5 920b3f8b83 Promote Send advert to the main menu as its own tool
Announcing the node is the everyday operation in the Device actions
bucket, but it sat two screens deep and out of reach of the main menu's
type-to-filter. Give it a top-level entry (order 10, right after Device
actions; Device info shifts to 11) via a new menu-only tool that reuses
the existing advert flow through a standalone send_advert() entry point.
Only SELF_INFO is read on open — the advert op and contact card never
need the full snapshot — so it stays snappy over Bluetooth. The row
inside Device actions stays, and scripting remains under `config
advert` / `config share`, so no new CLI subcommand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:27:41 -04:00
Jean-Pierre MartineauandClaude Fable 5 e4a4e7c03b Tuning params: real float units per firmware; drop no-op TX delays
Checked against companion firmware (MyMesh.cpp CMD_SET_TUNING_PARAMS):
rx_delay_base and airtime_factor are floats carried x1000 on the wire
(constrained to 0-20 s and 0-9 respectively), so the settings now speak
real units — previously the raw wire integers were shown and edited,
making "2" mean 0.002.

The TX delay factors added in the previous commit are removed: the
companion handler reads exactly the two fields and ignores trailing
bytes, so they were silent no-ops. They exist only as remote-CLI
settings on repeater/room-server/sensor firmware.

Airtime factor now leads the Tuning section, ahead of RX delay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 01:14:05 -04:00
Jean-Pierre MartineauandClaude Fable 5 42a22fd228 Expand device config: new settings, sync clock, and a live status panel
New staged settings: contact auto-add bitmask, default flood scope
(hashtag-normalized, 31-byte slot), and the two TX delay factors the
library always zeroed (write-only on hardware, so unknown values fall
back to 0 exactly as before). TX power is now capped by the board's
reported max_tx_power instead of a hardcoded 22, with the prompt hint
and validation sharing one effective_maximum.

Device actions gain Sync clock (drift shown before writing host time;
also a config sync-clock CLI command), and Device info now leads with
a live Status panel: role, model/firmware, battery, storage, clock
drift, uptime, and radio/packet statistics — every read best-effort so
older firmware still renders.

The simulator models all of it, including a seeded clock drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 00:20:15 -04:00
Jean-Pierre MartineauandClaude Fable 5 8839e4cf70 Section headers: Capitalized and accent-highlighted, not UPPERCASE
Separator gains a style field (default muted) so section headings can
carry the accent highlight while blank spacers, column-header lines,
and inline notes stay muted. Panel titles and inline section labels
(Trace summary, Route, Busiest links, ...) follow suit; column headers
alone keep the UPPERCASE convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:58:13 -04:00
Jean-Pierre MartineauandClaude Fable 5 768487e456 Polish menus and dialogs: lanes, headers, and a Device actions screen
- Dialog footer hints take the border's hue one shade darker (hint.*
  theme styles), mirroring the brighter title tinting.
- Dialogs get a blank breathing row above and below the body whenever
  it doesn't cost visible content.
- The chat conversation picker gains column headers over its lanes.
- The map screen titles itself "Map", matching its menu item.
- The config editor lays settings out in aligned SETTING / VALUE /
  DESCRIPTION columns under a header line, and with changes staged the
  Apply and Back rows sit together below the blank separator.
- Device actions move out of the config editor into their own screen
  behind a new menu-only device-actions tool (CLI equivalents stay
  under the config group).
- Type-to-filter keeps section headers (and column headers) in place,
  and section jumps now work over the filtered rows too.
- The main menu aligns tool names and muted descriptions into two
  columns, headerless.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 23:10:11 -04:00
Jean-Pierre MartineauandClaude Fable 5 a23b12f6a4 Normalize UI copy: casing, headers, descriptions, backtracking
Enforce one voice across every screen: column and section headers are
UPPERCASE, menu items are Capitalized buttons, and short descriptions
drop their trailing period (with real ellipses instead of "...").

Tools gain a display `title` separate from their kebab-case CLI name,
so a menu item now reads exactly like the screen it opens ("Device
configuration" opens Device configuration) and result windows inherit
the same name. Every sub-screen backs out via a "Back" row below a
blank separator (the config editor was missing both; its Close is now
Back, spelling out the discard when changes are staged), while the main
menu and device picker keep "Quit". Panel and dialog titles now wear a
brighter tint of their border colour via new title.* theme styles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:51:42 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 99643f52d9 Overhaul the config editor into a modal-dialog experience
Rework the device-configuration screen from a flat list with an
advert-laden "danger zone" into one grouped menu whose every prompt is
a real floating modal popup over a persistent backdrop, matching the
quit dialog. The main menu now stays pushed on the session stack for
the whole editor session, so sub-prompts float over it rather than
replacing the screen.

Settings are staged and applied together (with a dirty-cancel
confirmation); device actions — adverts, reboot, backup/restore,
identity key, factory reset — run immediately behind their own
dialogs. Adverts get a dedicated menu (zero-hop, flood, or share this
node as a QR/URI contact card). Destructive actions gate behind a new
TypedConfirmDialog that requires typing RESET / IMPORT. Reboot hands
off to the reconnect dialog and waits for the companion to return.
Location can be set by pointing at the full-screen map (new
LocationPickScreen) or by typing coordinates.

Make every dialog consistent: a short title in the border, the
instruction as an in-body prompt, and a width sized to its content
instead of a fixed wide cap. TextScreen/SelectScreen/AutocompleteScreen
gain an optional in-body prompt, and those plus ConfirmScreen gain a
dialog_width; the prompt is threaded through the surface and session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 22:28:42 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 62f19da405 Soft-retry unacked direct messages up to twice
A direct message was transmitted once and marked failed the moment its
single delivery-ack window lapsed, leaving Ctrl-R as the only recourse.
Now the send path re-sends an unacknowledged message automatically before
giving up, matching the mesh convention of a few tries.

_deliver_direct loops send_direct_message until an ack lands or the budget
is spent; both the initial send and the manual Ctrl-R resend route through
it. Each attempt already blocks a full ack window before the next, so the
retries are spaced by that window rather than hammering the radio, and a
hard send rejection propagates without retry.

The budget is the new direct_message_soft_retries setting: retries + 1
tries, clamped to 0..2 so a message is never re-broadcast more than twice
(0 = one shot, default 2 = up to three tries).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:41:14 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 19636e31e6 Float a fading ring spinner over laggy device navigations
Menu navigation over a slow link (Bluetooth especially, but serial too)
can sit on a blank frame while the companion answers — the menu screen is
popped and the tool's first prompt not yet pushed. Fill that gap with a
top-of-z-order spinner so the wait reads as work, not a hang.

- RingSpinner: a reusable, screen-agnostic braille circle with a cyan->blue
  comet chasing the rim. Plotted a quarter-arc and mirrored for exact 4-fold
  symmetry; stroked over a band of radii so it reads clearly as a circle.
- BusyOverlay + TuiSession.busy_overlay: draw the ring as the top-most float,
  centered, only in the gaps between screens (empty stack) so it never buries
  a prompt. It holds black for 200ms then fades in over 400ms, so quick work
  shows nothing and longer work never pops in. The fade restarts every time
  the ring is re-exposed (pop/reset), not just on first display.
- Wire it into menu navigation and the post-splash monitor resume for any
  real device link (mock simulator opts out); surfaced via ctx.ui.busy_overlay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:33:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 12577a1538 Add "Unpair & quit" to drop a BLE bond on exit
The quit dialog grows a third button, shown only when the session is on a
Bluetooth link that Windows actually holds a bond for (never serial, an open
PIN-less companion, or a non-Windows platform). Choosing it disconnects the
link and then drops the OS-level pairing during teardown, so the next launch
re-runs the PIN ceremony. MeshTerm's remembered-device record is deliberately
kept — unpairing forgets the credential, not the device identity.

Backing this: static WinRT helpers is_ble_paired / unpair_ble mirroring
_pair_ble_windows (Windows-gated, best-effort), an active_address on the
context, and _can_unpair / _unpair_on_exit in the menu.

Also close the WinRT BluetoothLEDevice handles in every helper. Each
from_bluetooth_address_async returns an IClosable that pins the OS ACL link
open; unpair removes the bond but not the link, so a leaked handle left the
device "unpaired but still connected" and refusing to re-pair until it was
power-cycled. Closing the handle in a finally is what finally tears the link
down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:46:02 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 0d2ffac70f Show the Bluetooth PIN as six fixed slots, capped at six digits
MeshCore pairing PINs are always six digits, so the PIN dialog now caps input
at six characters and draws a fixed six-slot field: each typed digit shows as a
bullet and each still-blank slot as a muted centre dot (· ), so the field reads
as a row of PIN boxes rather than a growing masked line. _LineEditor gains an
optional max_length (swallows keys past the cap, truncates an over-long paste)
and a slots render mode backing the field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 20:00:46 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 7d40929341 Pair PIN-protected BLE companions via the WinRT ProvidePin ceremony
bleak's pair() on Windows only performs the CONFIRM_ONLY ("Just Works")
ceremony and never sends a passkey, so a PIN-protected MeshCore companion
bonds without authentication and then rejects the GATT subscribe on its
authenticated UART characteristic with Insufficient Authentication -- a
correct --ble-pin was reported as "rejected" and the device could never
connect.

MeshCoreDevice now runs the WinRT PROVIDE_PIN ceremony itself before opening
the link, creating the ENCRYPTION_AND_AUTHENTICATION bond the characteristic
requires; Windows persists it, so later reconnects need no PIN. A first GATT
auth failure with a supplied PIN also triggers one unpair-and-re-pair retry to
heal a stale unauthenticated bond from an older attempt. The step is a
best-effort no-op off Windows, when no PIN is set, or when already bonded.

Verified on hardware (AA:BB:CC:DD:EE:FF, MeshCore-Homestead): correct PIN
pairs and connects (reads model Seeed Tracker T1000-E), wrong PIN fails clean
with the "rejected" DeviceAuthenticationError, no PIN reports "requires a PIN",
and a bonded device reconnects with no PIN. Full CLI path exercised end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:58:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 588ee67c3b Prompt for a Bluetooth PIN in the picker; bail cleanly in the CLI
When a companion needs pairing, the interactive picker now opens a centered PIN
popup (its own bordered box under the wordmark, matching the splash) and retries the
connection with the entered code — re-asking with a "rejected" note on a wrong PIN,
and returning to the device list on Esc. The masked field clears its stale error as
soon as the user types a fresh code.

The connect failure is now a distinct DeviceAuthenticationError (a DeviceCommandError
subclass), so the picker can catch "needs a PIN" specifically while the scripted CLI
still catches the base class, prints the remedy (pass --ble-pin), and bails without a
prompt — no interactivity for scripted runs. --ble-pin already feeds the first attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:39:07 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 77390860da Handle PIN-protected Bluetooth companions gracefully
A BLE companion that requires pairing accepts the link-layer connection but rejects
the GATT subscribe with "Insufficient Authentication", which bleak raised as a bare
BleakGATTProtocolError — escaping connect() as an unhandled traceback.

Translate that (and related "Insufficient Encryption"/"not paired" rejections) into
a clean DeviceCommandError that names the fix: pass --ble-pin, and pair once in the
OS Bluetooth settings on Windows. It's classified as its own actionable case, never
as a dropped link, so the session offers a PIN rather than a pointless reconnect.

The startup probe now lets this actionable error through instead of collapsing every
failure to a bare "didn't answer", so the picker shows the real remedy in the splash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:26:31 -04:00