Commit Graph
44 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 4.8 e07ef3b379 Device state: cache the picker reads that run before each screen
The session cache fixed the reads screens make on open, but opening the
heaviest screens still stalled for seconds — because the pickers and
screen-openers that run *in front of* those screens were still reading the
radio directly. The menu wraps prompt-gathering and execution in one busy
overlay, so a Trace or Chat open pays its picker's slow round-trips before
the (already-cached) screen behind it ever builds, which is why caching the
screens alone showed no improvement.

Route the interactive-open reads through ctx.devstate too:

- Chat's conversation picker read the channel-slot probe *and* the contacts
  table on every open (~5s) — both now come from the cache (channels mapped
  from the cached slots via the new _channels_from_slots, contacts served
  cached). The CLI list/send path keeps reading fresh.
- Trace and TX-optimize target pickers now read contacts from the cache.
- The repeater-admin screen and the channels manager (its slot probe, on a
  local copy so mutations still re-probe the fresh layout after invalidation)
  read through the cache.

Small, session-stable reads (self-info, build_snapshot's tuning/path-hash)
are left as direct reads — they were never the seconds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:41:58 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 736b420a4f Device state: session-cache the reads screens make on open
Opening a screen re-read the same stable facts from the companion every time — the contacts table, self-info, the path-hash mode, the channel-slot probe. Over Bluetooth each is a round-trip, and on a busy node two dominate: get_contacts (~2.4 s with hundreds of contacts) and the channel probe (~2.6 s, it walks every slot index), so navigation stalled for seconds on each screen. Measured on hardware; and asyncio.gather makes it worse, not better — the meshcore BLE UART serializes commands, so concurrent reads collide.

DeviceState (ctx.devstate) reads each fact once and serves it from cache. Contacts use stale-while-revalidate on a 90 s TTL — a read past it returns the cached list instantly and refreshes behind it, never blocking navigation on the slow call. Self-info, path-hash mode, and channel slots are held until an in-app write invalidates them: config apply drops self-info + mode, the channel editor drops channels, a reconnect resets everything. The hot screen openers (nodes, dashboard, chat, trace, TX, map, atlas, time machine, courier, watchtower) now read through the cache. The first screen opened in a session still pays the one read — shared across every screen — and every open after is instant. Transport-agnostic, so serial benefits too; harmless on --mock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 22:09:14 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 34a3b0a01d Atlas redesigned: walk the mesh from a focus node
The whole-graph radar view drew every node on concentric hop rings at
once — a hairball past a dozen nodes, its pan/zoom camera unable to fix
what was fundamentally overplotted. The atlas is now a graph walker:

* One node holds the focus (you, to open). The canvas draws only that
  neighbourhood — focus centred, direct neighbours fanned around it,
  braille edges coloured by SNR and faded by evidence age — and sizes
  itself to the crowd, so a two-node link is a tight six rows while a
  hub earns the full fan. The node the trail came from anchors due
  west: walking reads as moving right, backing up as moving left.
* Beneath it the links list the same neighbours as selectable rows,
  strongest first: glyph, name (palette hue, you in white), hash in the
  shared hash widget, SNR with a quality bar, samples, source tags,
  age, and how many links continue onward. The highlighted row's marker
  lights white on the canvas.
* Enter walks (the breadcrumb trail grows across the top), ⌫ steps
  back, walking into the trail-back row retraces instead of ping-pong
  growing, Home refocuses you. Typing finds any node in the graph —
  nearest first, islands flagged — and Enter teleports there, the
  trail restarting since the walk never crossed the gap. ^R rebuilds
  from storage and keeps whatever of the trail survives.

The per-node details dialog is gone (focusing a node is its details),
and the ring-relaxation layout service with it — the fan needs no
global relaxation. PgUp/PgDn page the highlight, dashboard-style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:17:03 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 241be536d8 One exit language: shared menu chrome in ui/menus.py
Every select list now builds its closing furniture through one module,
so the conventions hold by construction instead of by copy-paste:

* Back is THE exit word — bare, no arrow, no icon — with exactly one
  blank separator above it (back_rows). Courier and Watchtower lose
  their "← Back"; the rules popover's "← Close" becomes Back too.
* The staged-changes exit pair (✓ Apply … / ✗ Back — discard) moves to
  exit_rows, shared by the config editor and repeater admin, along with
  changes_phrase and one confirm_discard dialog (apply/send verb
  parametrised).
* Label + description rows share menu_rows (cell-padded lanes): Device
  actions, channel detail, repeater actions, and — converted from the
  inline "label  —  description" style — Send advert, Identity key, and
  the channel manager's Add-a-channel section, whose rows now read as
  two aligned lanes with the trailing … that marks "opens a prompt".
* Grouped-list headings share section_heading (── Label ── in accent):
  Watchtower, Courier, the Time Machine picker, and the admin picker
  (which also drops the stray 🔑 inside its heading).
* Status marks normalize to ✓/✗ (U+2713/17): Watchtower's ✔ and the
  ✖ on Cancel-this-message / Stop-watching become err-tinted ✗ rows.
* fit_cells replaces the four private _fit copies (channels, dashboard,
  time machine, chat picker) and measures display cells, so a wide
  glyph in a name can't skew a lane; empty-state notes drop their
  parentheses ("no channels configured yet", "no matches").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:48:48 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 1c267378ef Relative ages speak grammar: "(now)", never "now ago"
format_ago in widgets.py is now the one prose form for relative ages —
bare "now"/"never", "5m ago" otherwise — and every row that stitched
_format_age plus a hard-coded " ago" suffix (packet viewer heard row,
Time Machine record/first-heard lines, Courier finished rows, channel
detail summary) goes through it, killing "now ago" everywhere at once.
The channel summary drops its one-off "just now" special case for the
same app-wide wording. Also: the Device actions description no longer
claims an advert action — that lives in its own main-menu entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:30:19 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 dfc4cb9845 Chart axis ticks, dialog polish, Time Machine alignment
A follow-up interactive-polish pass over the charts, the menu popups, and the
Time Machine.

Charts (braillechart)
- axis_chart now notches ┬ x-axis ticks under a continuous chart's labels too, the
  same grammar the columnar day charts use — one axis grammar everywhere. The
  dashboard activity chart and the node-history Volume/SNR/Rhythm charts pick this
  up through the shared widget; single-line sparklines are untouched.

Time Machine (mesh page)
- Empty days show as zero bars instead of being skipped, so the day charts read as
  real calendar time (a fortnight with two dead days no longer fuses to twelve
  adjacent bars). Never invents days from before recording began.
- The rhythm chart shares the day charts' y-axis gutter, so its left edge lines up
  with them; its own narrower width is unchanged. Its whole-window peak is folded
  into the shared gutter sizing (a slice can top a single day's total).

Packet viewer
- Holds a minimum body height so paging between a short packet and a tall one no
  longer resizes the dialog out from under the reader.

Menu popups
- Floating dialogs force a full frame rewrite as they're torn down, scrubbing any
  double-width glyph a terminal painted a cell past where the differential renderer
  tracks it — the "stray characters in a dialog's wake".
- Send advert: reworded a choice, and the fixed list is non-filterable (a stray key
  can't narrow/resize it); a select's default footer now only mentions type-to-filter
  when it actually filters.
- Watchtower Rules: a blank separator sits above the Close item.

Menu
- The Mesh atlas tool icon is a single-codepoint 🌐 instead of a VS16 🕸️: the
  emoji-width calibration strips VS16 on terminals that render it narrow, which
  measured the old icon a cell short and drifted that row's help text right.

630 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 01:14:50 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 d19266bd5c Punch-list pass: dashboard hints, packet paging, decrypt, Time Machine axes
Four small UX gaps closed together:

- Dashboard's footer hint was missing Home/End even though the screen
  already handled them.
- The packet viewer's paging keys were reversed relative to the list
  that opens it: PgUp/PgDn walked packets while the dashboard feed
  walks with up/down. Swapped so up/down page through packets (matching
  the feed) and PgUp/PgDn scroll a tall body (matching every other
  screen's scroll keys).
- A raw "packet" entry (an overheard RX_LOG_DATA frame) now shows its
  parsed payload class and route, and — for a channel-text frame naming
  a channel we hold the key for — its decrypted text. The AES/HMAC
  logic lives in core/channels.py as pure, unit-tested crypto
  (decrypt_channel_text), independent of the vendored meshcore
  library's own reader internals; the dashboard fetches the device's
  channel secrets once at open time and hands them down to each
  PacketViewer it opens. Raw-field dumps now skip fields already folded
  into a flavoured row instead of repeating them.
- The Time Machine's whole-mesh charts (packets/day, nodes/day, rhythm)
  gained the same mirrored numeric y-axis the dashboard's activity
  chart already had. That gutter-drawing logic moved into
  braillechart.axis_chart, a shared widget, and the dashboard's own
  activity chart was refactored to use it too instead of hand-rolling
  its own copy.

meshterm/ui/channels.py's _read_slots is now public (read_channel_slots)
since the dashboard opener needs it too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:29:45 -04:00
Jean-Pierre MartineauandClaude Fable 5 d61d4fbbed Menu: split the crowded Mesh section, Messaging now leads
Chat, Channels, and Courier move into their own Messaging section at
the top; Mesh keeps the views over the mesh itself - Dashboard, Nodes,
Map, Atlas, Watchtower, Send advert - dropping from nine rows to six.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:49:02 -04:00
Jean-Pierre MartineauandClaude Fable 5 ffa834182b Mesh Atlas rebuilt: a radar scope you can zoom, pan, and search
Ground-up rework of the unusably busy first cut. A new atlas_layout
service lays the evidence graph out in camera-independent unit space:
radii stay pinned to evidenced hop distance (the atlas's core question)
while angles relax toward each node's strength-weighted neighbour mean —
chains straighten into spokes, clusters gather into wedges — then each
ring enforces a minimum angular separation. Deterministic throughout, so
the mesh keeps its shape across rebuilds.

The screen becomes a camera over that layout, with the street map's
exact controls: arrows pan (Shift fine, clamped so the graph can't be
lost), PgUp/PgDn zoom 1–12x, Home refits, and every letter feeds a
find-as-you-type filter — matches labelled bright white over dimmed
context, Enter adopting the first match. Tab walks the selection
(recentring it when off-screen), Ctrl+R rebuilds, Esc peels find →
selection → screen. Hop rings draw as faint dotted guides, and the
label budget grows with the zoom's square, so 1x is a calm overview and
zooming in is what buys the detail. Panel and footer rows all fit 72
columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:26:30 -04:00
Jean-Pierre MartineauandClaude Fable 5 d75a9aa2db Courier: a store-and-forward outbox that delivers when the node shows up
Queue a direct message for a node that isn't reachable right now and a
session-long background loop delivers it when the recipient is next
heard — or at a scheduled local time ("next 07:00", "in 3 h", any
HH:MM). Each attempt is an ordinary chat send (it lands in the
conversation history with its ack state, exactly as if typed); failures
back off exponentially and, after the first shot, wait until the node
is heard again, so an absent node is never hammered on faith. One
attempt per pass keeps a backlog draining politely, and after five
unacknowledged tries the courier gives up and says so. Outcomes raise
Watchtower alerts, so the header's badge lights when an overnight
delivery finally lands.

The outbox persists in courier.json (a queued message must survive
restarts or the promise is hollow), a Mesh-section screen carries the
queueing flow, per-entry Send now / Cancel, and the finished history,
and a scripted `courier <node> <text…> [--at HH:MM]` queues without
transmitting anything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:52:51 -04:00
Jean-Pierre MartineauandClaude Fable 5 908696a2d2 Time Machine: the recorded history, explorable node by node
The passive monitor has been writing every overheard packet to the
database since the first session; nothing surfaced that history beyond
the dashboard's two-hour window. A new Tools-section explorer pays it
off: pick the whole mesh or any node ever heard and read its past as
braille charts over a switchable 24 h / 7 d / 30 d / all-time window
(w cycles). A node shows its reception volume, a median-SNR band
scaled between its own extremes and coloured by quality, its
hour-of-day rhythm, and the roll-up record; the mesh page shows
packets and nodes per day, first-ever arrivals, and the all-time
ledger. Histories read chronologically — oldest left, now right —
unlike the live dashboard's sliding chart.

Three repository queries feed it: a windowed per-node observation
record (packet rows excluded, as everywhere reception is attributed),
per-UTC-day activity totals, and first-ever appearances with names.
Everything is stored data; no device is needed and nothing transmits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:43:30 -04:00
Jean-Pierre MartineauandClaude Fable 5 b6fcda0f91 Watchtower: a passive sentinel with alerts for the nodes you care about
Star the nodes that matter (the roof repeater, the far gateway) and
session-long rules watch what the always-on hub already hears: a
silence alarm when a starred node goes quiet past its threshold
(latched per quiet spell, re-armed — with a recovery note — when it
returns), an SNR-sag warning when its receptions' median drops well
below the run before it, and a mesh-wide heads-up the first time a
never-before-seen node id appears (baselined against the whole stored
observation history, announced at most once ever). Nothing transmits.

Alerts persist in watchtower.json alongside the watchlist and rules,
show as a red ▲ badge in the header, and are read, acknowledged, and
configured in a new Mesh-section Watchtower screen. A remote-battery
rule was considered and dropped: observations carry no parsed battery
telemetry, so there is no honest data source for it yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:35:36 -04:00
Jean-Pierre MartineauandClaude Fable 5 9501505bed Mesh Atlas: the observed topology, drawn as a walkable braille graph
The trace path composer's evidence graph — trace walks, firmware
routes, overheard relay chains, repeater neighbour tables — finally
gets a face. A new full-screen Mesh section tool draws every link the
graph holds on the street map's braille canvas: our node at the
centre, everything else on concentric rings at its evidenced hop
distance, edges coloured by median SNR and faded by evidence age.
Clusters we only know exist from a repeater's neighbour table (no
observed path back to us) ride an outermost island ring and say so.

Rings place members at even angular slots ordered by the bearing of
their strongest parent, with the whole ring rotated to best fit those
wishes plus a per-ring twist — sparse meshes are pairs all the way
out, and without the twist every ring collapses onto one crowded line.

←/→ walk the graph node by node (selection brightens its links and
fills the bottom panel strongest-first), Enter floats a per-link
detail dialog, r rebuilds from storage. Nothing transmits, ever: the
atlas is a reading of what the radio already heard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:23:49 -04:00
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 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 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 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 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 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 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 d4fa056526 Identify companion hardware by its firmware model
Query MeshCore's device-info frame (send_device_query) on connect to learn the
board's own model string (e.g. "Seeed Tracker T1000-E") — the only reliable source,
since BLE companions advertise no manufacturer data, use randomized addresses with
no IEEE OUI, and expose no GATT Device Information Service.

The probe folds the model into the identity dict, so it's captured during the
connection that already happens, then remembered in the device store (preserved
across reconnects like the node name). The device list and splash picker rename
their "Vendor" column to "Hardware": it shows the firmware model for confirmed
devices and falls back to the USB vendor name for merely-attached serial ports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:17:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 7850587e2a Add Bluetooth LE companion support alongside serial
Companions can now be reached over Bluetooth as well as USB. Rather than
a parallel BLE path, this generalizes the existing serial abstractions so
a device is identified by a transport (serial | ble) plus a target (COM
port or Bluetooth address); every feature rides the same Device interface
and works over either link unchanged.

- discovery: DiscoveredDevice gains transport/address/name and
  transport-aware stable_id/label/confidence/target; discover_ble_devices
  scans for MeshCore* adverts via bleak, discover_all merges both.
- connection: MeshCoreDevice opens create_ble vs create_serial by
  transport; new link_present() liveness (OS port enumeration for serial,
  the BLE client's is_connected flag for Bluetooth); is_connection_lost
  recognizes Bleak errors and BLE transport callbacks; probe_device is the
  transport-aware smoke test.
- disconnect: the session watcher polls transport-neutral link_alive(), so
  a dropped BLE link raises the same reconnect dialog and auto-resume as an
  unplugged cable; reconnect rebuilds the same endpoint by address.
- selection/context/store/profiles: --ble address, remembered BLE default,
  and BLE device profiles all resolve to the right transport.
- UI/CLI: startup picker scans both transports behind a spinner and tags
  Bluetooth companions; `devices` lists them (--no-ble to skip); global
  --ble/--ble-pin flags; README and example config updated.

Validated on hardware: discovery, connect, reconnect, and channel
messaging (send + receive) over BLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:44:39 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 6cfe435d55 Redesign the nodes page with heat-map recency and live sorting
Sort contacts alphabetically with our own node pinned first, and add
Heard (relative age) and Pkts (overheard-packet) columns alongside a
per-type glyph and the full path-hash-highlighted key. Drop the boxed
table for a clean header rule and a glyph legend.

Colour each name by a continuous log-scale heat map of how recently it
was heard (white -> yellow -> orange -> red -> grey), and give the
node-type icons a shared, map-safe palette across the whole app: a
yellow star for us, a loud pink node and a calmer violet repeater
(both in hues no basemap uses, so nodes pop more than repeaters),
a white room and an orange sensor.

In the menu the list becomes an interactive screen: left/right pick the
sort column (cyan header + direction triangle), up/down set ascending/
descending, with column widths reserved so toggling never shifts the
row. The scripted CLI keeps a one-shot render via --sort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 02:38:28 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 76202ec0d1 Hide the devices inventory from the menu, keep it on the CLI
By the time the interactive menu is up a companion is already selected,
so the read-only devices listing has no job there; it belongs on the
command line as a startup-time "which port is my radio?" diagnostic.

Add a menu_visible flag to the Tool contract (skipped by the menu loop,
never by CLI registration) and set it False on the devices tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 00:23:26 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 8906e4ed53 Enliven the conversation list and keep section headings in view
Color-code the chat picker rows: a contact's dot takes that person's
chat hue (filled once talked-to, hollow before), previews light their
sender names and @mentions in-hue, and a relative age column sits
between the unread badge and the message text.

Pin a grouped list's section heading to the top row once it scrolls off
(sticky header in the frame), add PageUp/PageDown to SelectScreen, and
drop arrow wraparound in the conversation picker so it doesn't jump
across the section dividers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:03:31 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b692cce463 Confirm devices by smoke test on the startup splash
Reserve the "MeshCore device" tag for hardware we have actually talked to,
rather than guessing from the USB vendor ID:

- DeviceStore now keeps a registry of every confirmed device (keyed by
  stable serial id), remembered forever, migrating the old single-record
  file transparently.
- Selecting a device on the splash runs an immediate smoke test
  (smoke_test_meshcore): a genuine companion is remembered and confirmed;
  anything else re-prompts with a message to choose another. The probe uses
  a short handshake window so a non-MeshCore port is rejected cleanly in a
  few seconds instead of blocking on meshcore's full default timeout.
- The probe renders in place on the chromeless splash via a new BusyScreen
  (animated Braille spinner) driven by session.busy_startup, so the wordmark
  and box stay put instead of a separate progress dialog.
- Silence the meshcore library's console logging (its import calls
  logging.basicConfig, which printed "INFO:meshcore:..." over the TUI):
  route it to the JSON-lines file log with a NullHandler guard on root.
- The devices tool now marks confirmed devices and stops asserting "LoRa"
  before a device is proven.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 17:52:30 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 97557fc9b8 Frame the map's dense node core by default, tunable with --fraction
The default view fitted every located node, so a few distant outliers
(here a node near Toronto, 500+ km from the Montréal core) zoomed the
whole mesh out to a continental scale. Frame the densest half instead —
the nodes nearest the median centre — so the map opens on a useful local
region; the rest simply fall off the edges. Verified against a live
159-node mesh: 0.5 lands on the Greater Montréal metro (~77 km across,
two-thirds of nodes visible) rather than eastern Canada.

Viewport.fit gains a `fraction` argument (default 1.0, unchanged for
existing callers); the map's opening view and `r` reset pass 0.5, and
`meshterm map --fraction/-f` exposes the knob on the CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:03:26 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 0c0880b37d Pick the companion device at startup only
The devices tool doubled as a mid-session device switcher: its menu
prompt re-ran the picker and reassigned the active device. Drop that so
selection is a startup-only concern (or --port on the CLI); the tool is
now a read-only inventory in both the menu and CLI.

With the re-pick gone, startup is the sole caller of prompt_device, so
its cancel_label parameter (added only to swap in a "Back" label) is
removed and the "Skip…" label inlined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:36:51 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 2ec18138a7 Rename project from MeshTools to MeshTerm
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).

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