Commit Graph
15 Commits
Author SHA1 Message Date
Jean-Pierre Martineau 1b6a5cc1e7 Packet viewer: a walked trace is not a direct shot
Emptying a trace's relay path — right, since those bytes were readings all
along — left the card contradicting itself: three legs on the links row,
"direct — no relays" two lines under it, and a note crediting the reading
to a sender nothing had relayed for.

An empty chain means different things by class. For every other frame it
means the sender was heard straight off the air. For a trace it means the
relays went unrecorded, which is not the same claim at all — the readings
are proof something forwarded it. So the row says how far the packet got,
which is the one thing those readings do establish, and the reception note
goes back to naming the last relay.
2026-08-22 14:27:15 -04:00
Jean-Pierre Martineau 71f60c0eeb Packet viewer: show a trace's links, and say whose signal the reading is
A trace reports on the mesh as it crosses it — every node that forwards
one records the SNR it heard its predecessor at — so an overheard trace
arrives carrying a reading per hop travelled. Those are now a `links` row,
in walk order and coloured by quality like every other reception figure,
which for a walk out and back through a repeater reads as one strong
outbound leg and two weaker ones home.

The card's standing caveat also stopped being true. "Reception describes
the last relay, not the origin" is right for a relayed frame and wrong for
one that crossed nothing — those are only listed at all as of this
branch — where the reading *is* the origin's, and worth saying so: a frame
heard straight off its sender is the best evidence a link ever gives.
2026-08-22 14:03:23 -04:00
Jean-Pierre MartineauandClaude Opus 5 ce86e86693 Packet card: two columns back from the gutter, and rssi holds its line
On the PicoCalc the viewer's reception row folded the bare word "rssi"
onto a line of its own. Two things were spending the cells it needed.

The dialog gutter: every floating box left six columns to the backdrop it
sits over. Three a side reads as floating at 72; at 53 the same gutter is
a twelfth of the whole display, and it costs content. How wide the gutter
is worth being is now the platform's call (Platform.dialog_margin), and
the console's is four — still floating, two columns back.

The separator: the row chained its two atoms on the roomy "  ·  " no
matter the lane. That is the header's problem too, and the header already
solved it — roomy by default, compact when the cells run out — so the row
now takes the same trade, sizing itself at render time like the via path
beside it. Two cells alone would have left a two-digit SNR beside a
three-digit RSSI still folding; together they fit with a cell to spare.
The roomy/compact pair moves to ui/menus.py, one definition for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 17:01:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 8a80d47815 F-key lane: a jump rides the Shift half of the page it follows
Home and End are on the PicoCalc's keyboard. Paging is not. So the
jump-to-either-end pair was never the unreachable thing that earned it
F1/F2 -- it only ever wanted a chip for consistency with the pager it
belongs to. Now it rides that pager: Shift+F5 is Top because F5 is
Page up, Shift+F4 is Bottom because F4 is Page down. One axis, one pair
of keys, the second-grade verb sitting behind the first.

That frees F1-F3 on every screen, and the lane is the only place the
device can advertise anything -- there is no hint line to read a chord
off. So the freed slots went to the affordances that were invisible
there:

  select lists  Sect down/up, the section jumps. Ctrl+PgUp on a
                keyboard with no PgUp is the one binding in the app
                that was physically unreachable. Plus Delete, lit on
                exactly the rows the key acts on.
  map           Region (Home, renamed -- it frames the ground the mesh
                covers, it does not "reset" anything), You (new: Ctrl+L,
                recentre on our own node at the zoom you chose), and
                Frame for the find matches.
  node page     the tab it would switch you to.
  time machine  the window span it would take you to -- w was
                undiscoverable on the device.

The map is the one exception to the new rule: Region answers "where
should I be looking", not "how far down the body am I", so it keeps F1
beside the other two destinations rather than hiding behind a pager it
has nothing to do with.

Screens that never scroll (every dialog, the busy splash, the reorder
list) now say so with an empty lane instead of offering a pager that
does nothing. Screens whose Home/End mean something else say what:
Latest/Oldest in a chat, Newest/Oldest in the packet viewer, You on the
walk (where End is bound to nothing, so its slot stays blank).

The map's hint line finally fits 72: its basemap status was never a key
hint, so it moved to the title's status chain where the standards put
those -- which is what took the map off the gallery's pre-existing
overflow list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:56:42 -04:00
Jean-Pierre MartineauandClaude Fable 5 4aef3e7b82 Tests: one way to read a screen
Eleven files defined the same _plain (plus test_chat's _strip_ansi and
test_timemachine's renderable flavour), and two of the copies had
already diverged into join-without-strip. tests/conftest.plain is now
the single strip-and-join reader — a list of lines or a bare string —
and every file imports it; the local variants keep their names so call
sites stay put (housekeeping H-003).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 23:12:24 -04:00
Jean-Pierre MartineauandClaude Opus 5 01ca242c98 Live feed: the lane is about the packet, whatever the packet is about
The NODE lane suited the two classes that name a node and left every
other one reading a dash. On this mesh that is half the traffic: the
direct messages, requests, responses, returned paths, acks and traces
that relay past us naming no origin. So the lane asks each class its own
question instead, and the header says SUBJECT — the word that stays true
down the whole column.

An advert or a telemetry frame is still about its node. A channel text is
about its channel, named by the key whose MAC confirms the frame — never
by the fingerprint alone, which 256 buckets make no proof at all; a
channel we hold no key for reads as that fingerprint and says so. A
direct message, request, response, returned path or anonymous request is
about the two nodes it travels between, drawn sender → recipient on the
hop-sequence widget, so endpoints read exactly as route hops do: resolved
names, key-derived hues, our own node white the moment a frame is
addressed to us. Too wide, and the sender drops to its hash — the frame
is addressed, so the addressee is the half worth the cells. An ack is
about the message it answers, a trace about its tag. And an unsigned
channel post is filed under its channel rather than the bare word
"channel", which named nothing.

Half of that was unreadable before because nobody had opened the frame.
The companion hands every overheard packet to the library, which decodes
the body for exactly two classes — advert and channel text — and leaves
the rest as bytes. Those bytes are not opaque: MeshCore addresses a frame
in the first few of them, and core/frames.py now reads them, class by
class, against the layouts the protocol's own docs/payloads.md publishes.
The trace tag is the little-endian uint32 the firmware writes, so it
reads as the same number a trace reply reports; an ack's checksum keeps
wire order, the form the companion reports its own delivery acks in. Two
checks guard every decode, because a wrong slice yields not an error but
a plausible-looking hash: the frame's payload version must be v1 (the
header reserves two bits for a v2 that widens both hash and MAC), and the
body must be long enough for the layout its class promises. Either
failing yields nothing at all.

The feed opens seeded from history, so any of this that lived only in a
live event's raw payload would leave the opening screen blank — the same
problem the channel-text crypto trio had. Same answer: three columns
(dest/src/tag), written on capture and restored to the very keys the
event used, so a replayed frame and a fresh one read identically all the
way up. Older rows carry NULL until re-heard.

The viewer gained the matching rows: to, from, tag, acks, and a channel
for a datagram — whose body is not text, but whose MAC still names it.

Verified against the MeshCore protocol documentation and firmware source,
then corroborated on the air: every frame captured off the radio has a
body length that decomposes exactly into its documented layout, an
anonymous request's 51 bytes (1 + 32 + 2 + one AES block) included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 19:11:07 -04:00
Jean-Pierre MartineauandClaude Opus 5 8afa592e9a Route graph: a hop heard twice is drawn twice, and said so
Merging a repeated hop into one marker is right across paths and wrong
within one. A via chain that names the same hop twice closes a cycle, and
the left-to-right flow has nowhere to seat it: the balanced-x relaxation
iterates a cyclic edge set, never reaches its fixed point, and returns
wherever the sweep budget stopped. The loop's relays then land in one
column — markers piled a cell apart, labels colliding or dropped, the back
edge falling as a bare vertical — while the hops outside it are crushed
against the ends. Worse, the residue depends on set iteration order, so
the pile's column swung with the hash seed: the very frame-to-frame jump
the first-appearance node order exists to prevent.

render_path_graph grows allow_duplicate_nodes, which hands each revisit
its own marker (occurrence-qualified ids the caller's callbacks never see,
so both markers draw as the same node). The walk is acyclic again, spreads
evenly, and reads in the order it was heard. Relays shared *between* paths
still merge — that fan is what the widget is for.

Opt-in, because the two situations want opposite things. The Trophy case
scores a path we composed, so collapsing a revisit is deliberate there —
re-walking a node must not make the shape look bigger than the ground it
covered. An overheard chain has no such duty: nothing in it is ours to
inflate, and a repeat is evidence.

Evidence we cannot read, though — hops are addressed by a one-byte hash,
so a repeat is as likely two colliding nodes as a genuine loop. The packet
card and the Message paths dialog now say exactly that under the via row
and the graph, naming the repeated hops through the shared path widget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:33:58 -04:00
Jean-Pierre MartineauandClaude Opus 5 50a3322b84 Live feed: rows slide sideways; the viewer's via folds
Three surfaces, one thread — a route should be readable where it is,
not only where it fits.

The feed's highlighted row now scrolls horizontally under ←→ (the
app-wide hscroll convention: the ▸ pointer lane pinned, the shift
abandoned the moment the selection moves on, the footer atom surfaced
only while the row overflows). A selected row carries its relay chain
whole so the scroll has something to reveal; every other row keeps
eliding its middle hops, so both ends of the route stay in view.

The packet viewer's `via` row leaves `path_text` for THE path line, and
folds instead of running off: a PathLine handed to Rich now renders
itself as the wrapped shape at whatever cell width the layout works out,
so a value whose lane is decided by the *other* rows' labels no longer
has to compute that width twice. Hops break at their own boundaries and
hang under the value block.

And the viewer's `from` row leads with the shared node-type mark — ★ us,
▲ repeater, ■ room, ◉ sensor, ● node, ○ when neither the packet nor the
contacts can type it — so a sender reads as the same kind of thing here
as it does on the map, in the contact list, and in the route graph three
rows down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 00:16:22 -04:00
Jean-Pierre MartineauandClaude Fable 5 24c70c487f Packet viewer: the class headlines the card, icon and all
Every packet now leads with its class — icon + UPPERCASE label — as the
first row of the viewer, whatever the kind: a raw frame shows its parsed
payload class, everything else its own kind. The new PAYLOAD_ICONS table
gives each raw payload class a concept glyph (📻 channel text, 🎯 trace,
…, matching the tools that own those concepts) and joins the CLAUDE.md
icon lexicon; the dialog title drops its emoji, per the title rule the
icon was violating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:45:06 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 ec41de0339 Route graph: draw the featured path in plain white, and give the packet viewer one
Two related passes on THE route graph (ui/pathgraph).

1. **Featured path is always white.** The Message paths dialog and the
   Trophy case coloured their selected/scored path by reception SNR
   (theme.snr_rgb). But on a graph that draws only *one* real path over a
   grey backdrop, colour carried no comparison — it just competed with the
   node-name hues on the same canvas. Drop the SNR tint: the selected/walked
   path reads white (the single-walk convention), the unused paths grey.
   Removes the now-unused theme.snr_rgb / _SNR_RGB and updates both captions.

2. **The packet viewer draws a relayed packet's route.** A `packet` frame
   that actually crossed a relay now slots the fan-lane graph between its
   parsed detail (heard/from/class/via) and its extras — origin → relays →
   us, the same picture Message paths and the Trophy case draw. A direct
   arrival skips it (its `direct — no relays` via row already says all there
   is). render_body splits into head/tail grids sharing one label-lane width
   so the rows stay aligned across the graph; the dashboard threads a
   node-type resolver through so relays wear their map marker (▲, …).

Full suite green (806).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:04:58 -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 70efb7c694 Grow-only dialogs, full raw labels, node rhythm in 15-min slices
Carried over from the last punch-list pass: the packet viewer opts into
a grow-only floating dialog (the box ratchets to its tallest body and
blank-pads shorter ones, so paging never re-centres it); raw payload
field names are shown whole by widening the label lane instead of
clipping at eight chars; the Time Machine node rhythm matches the mesh
page — 96 fifteen-minute slices sharing the Volume gutter — and its
time axis shortens marks to bare dates on multi-day windows; Courier
and Watchtower gain a visible Back exit beside Esc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:25:15 -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 a3398bebf5 UI punch-list: dashboard feed, chart axes, chat counter, popups
Two rounds of interactive-polish fixes across the dashboard, Time Machine,
chat, and the menu popups.

Dashboard
- Feed name lane names what a row actually is: a channel message's parsed
  sender, or a relayed packet's payload class (channel text / trace / …),
  instead of a bare "?" or a redundant "ch N". The class is persisted so
  history-seeded rows carry it too (observations.payload_typename, schema v10).
- PgUp/PgDn walk the feed selection so the highlight travels with the view.

Packet viewer
- Takes a live source and re-locates the viewed packet by identity, so packets
  arriving after it opens become reachable; the n/total title stays current.
- Decrypts overheard channel text straight from stored history (crypto fields
  persisted: chan_hash/cipher_mac/crypted, schema v9).

Time Machine
- Day charts label their bars with column ticks: each date sits under its own
  bar with a ┬ notch, shortens (month only on the first tick and on rollover),
  and thins to fit. axis_chart grows an optional ticks path for columnar charts.
- Node history charts render through the shared axis_chart widget; the mesh
  Rhythm is 96 fifteen-minute slices at 4x width, "now/newest" hints dropped.

Chat
- The byte counter pins to the right edge of the input's last line, so it stays
  bottom-right when the compose wraps instead of trailing the cursor.

Menu popups
- The per-node Rules popover gets an explicit Close row.
- The Send advert popup drops the emoji from its floating title (a terminal that
  paints it narrow left the content-sized dialog's border short, bleeding the
  backdrop through); the menu row keeps its icon.

625 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 22:47:36 -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