Commit Graph
24 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Fable 5.1 c9f5a71ce7 Every Python file names its licence with an SPDX identifier
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 13:09:18 -04:00
Jean-Pierre MartineauandClaude Opus 5 92d31a3191 Every file is spelled the way the formatter spells it
`ruff format .` across the repo: 153 files, no behaviour change. This is the
second gate CONTRIBUTING.md promises, and until now it reported 154 files needing
reformat — so a first contributor ran the command the guide told them to run and
could not tell their own diff from the background.

Landed on its own, with nothing else in it, because a whole-repo reformat is only
safe to skip past in blame if it is provably nothing but layout. It was checked
that way rather than assumed: every changed file was formatted in memory and
compared by full `ast.dump()`, and 152 of 153 come out structurally identical
(the one exception was a docstring opening on a quote character, fixed in the
commit before this one). The string and comment token streams were diffed
separately, so a quote flip or an edited comment could not hide inside a
re-layout — five quote-style normalisations and sixteen implicit-concatenation
joins, all value-identical, and one commented-out line gaining the space after
its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite
is 1888 green, gallery gate and `test_theme16` included.

What actually changed, in order of how much of the diff it is: wrapped calls and
signatures rejoin onto one line where they fit inside the hundred columns; the
magic trailing comma expands what it is there to expand; complex slices gain
their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are
laid out to be read were fenced in the previous commit and are untouched here.

The aligned trailing-comment column is gone from about ninety lanes that were not
fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced
where it sits. Every comment stays on the entry it describes, so nothing there
reads wrong; it just no longer lines up.

Add this commit to `.git-blame-ignore-revs`, which follows immediately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 01:07:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 b5006cc415 Every line fits the hundred columns it was configured for
`E501` across 69 lines, wrapped rather than exempted.

The prose ones — module docstrings in `menu`, `node_detail_screen`,
`pathline`, `pathgraph`, `walk_screen`, `trace_runner` and `test_about` — are
reflowed as paragraphs, so the block keeps its shape instead of gaining one
short orphan line. Nothing they say changed.

The code ones mostly gained a line break inside a call. Where a break would
have read worse than a name, the sub-expression got one instead: `reach` in
`topology`'s quality clamp, `plain_cells` in `pathline`, `numbering` in the
trace list, `listed` in `flash.py`'s refusal, and a small `shape`/`is_strip`
helper in two tests that were comparing or matching the same shape twice on one
line. Four docstring summaries lost a word they were not using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:22:52 -04:00
Jean-Pierre MartineauandClaude Opus 5 ca0023f5b7 The import blocks hold only imports
`E402` read as seventeen deliberate late imports and was in fact two accidents.
A module constant had settled into the middle of `packet_viewer`'s import block
(`_BODY_CACHE_MAX`, now below it), and ten test modules put their `_FIXTURE`
path or a small helper above the last import — the `tests.conftest` screen
reader, which now joins the block it belonged to.

`B008` is the opposite case: all six are Typer declaring a CLI parameter by
calling `Argument`/`Option` in the default, which is the framework's API and
cannot be written another way in this style. The objects it returns are inert
sentinels no command mutates — exactly the thing `extend-immutable-calls` is
for, so the rule now knows about them instead of being silenced per line.

`.claude` leaves the lint's reach: those are helper scripts for the skills, not
package code, and CI has no reason to gate on them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:17:12 -04:00
Jean-Pierre MartineauandClaude Opus 5 bcf387c620 The test fixtures stop being a picture of one person's mesh
Every node in the suite was named after hardware on JP's desk: YUL-Cartierville
and YUL-Poly are repeaters he runs, two more were the BLE
companions, another his own handle, and a long tail of YUL-Far / YUL-Me / bare
YUL besides. A stranger reading the tests learned his callsign, his city and his
gear before learning what the tests check.

They join the family the suite had already invented for exactly this — Alice,
Lounge, Yagi-Repeater, hub-1: Hilltop-Repeater, Lakeside, Waymarker, Homestead,
and Hub for the 3d-keyed node. Hub is not a new name; test_device_config already
called that contact Hub, so the suite was carrying two names for one fixture.

Replacements are the same length as what they replace, so every width budget,
truncation assertion and gallery line is unchanged. The two map tests that type
a literal query picked `yu`/`yul` to match the old name; they type `hi`/`hil`
now, same length, same single match.

Left alone on purpose: the About page and the donate link, where the handle and
the repeaters are the point, the hand-drawn splash art, and the XIAO README
reporting what JP's own radio advertises.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 02:01:55 -04:00
Jean-Pierre MartineauandClaude Opus 5 42743bded2 Dashboard: the idle second was re-walking the whole window
The screen repaints once a second to keep its rates, ages and clock
honest. Both of its expensive stages are pure functions of data that
mostly does not move between those frames: the window digest re-walked up
to four thousand observations to reach the numbers it already had, and
the braille activity chart re-rasterized an identical picture.

The window changes in exactly two places — an arriving observation, an
expiring one — so both bump a revision the digest compares against. The
chart memoizes on the numbers it draws from. An idle repaint: 8.4 -> 3.8 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 05:42:20 -04:00
Jean-Pierre MartineauandClaude Fable 5 b83c9005c7 Charts: the mirror returns where the frame affords it; Rhythm seeks its finest slice
The right-gutter marks come back on the desktop (redundant but nice)
and the console keeps the bare tick - axis_chrome is the one number a
caller budgets, platform-bound like the rest of the chrome. The Rhythm
ladder widens to 1/5/10/15/20/30/60-minute slices, finest that fits:
the desktop keeps its 15-minute sweep at 72 cols and earns finer on a
wide terminal; the mesh page re-slices one minute-grid scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 00:10:53 -04:00
Jean-Pierre MartineauandClaude Fable 5 3e7a1893a4 Time machine: spans on F1-F3, and a Rhythm that bends to the width
The 15-minute slices outran the PicoCalc; the chart now takes the
finest of 20/30/60-minute slices that fits (the mesh page re-slicing
one ten-minute base scan), and every section lays out for the single
left gutter. The window ring rides F1-F3 as direct chips - 24 h, 7 d,
30 d, the span on screen dimmed - and the handheld skips all time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:12:02 -04:00
Jean-Pierre MartineauandClaude Fable 5 d00c6349eb Braille axis: the right scale was only an echo of the left
Marks now tick the left gutter alone and the right edge closes with a
bare border, handing its cells to the chart. Deep tallies compact to
k/M marks, and the gutter sizes to the widest mark actually printed -
a 1.5k peak reads 1k while a lower tick still lands three digits wide.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:12:01 -04:00
Jean-Pierre MartineauandClaude Fable 5 f69d1785b7 Performance sweep: cache the render funnel, the queries, the graphs
Every repaint re-rasterized every row and re-ran every layout; every
screen open re-scanned the whole history. Now work is done once per
change instead of once per frame:

Render pipeline — a content-keyed ANSI cache in render_to_ansi (every
Text/str row app-wide), memoized corner glow, and panel-wrap memos for
the base frame and stacked dialogs, all dropped on a platform switch or
emoji recalibration. Idle-frame compose falls 5-100x per screen
(contacts 5.6->0.1 ms, record card 8.5->0.07 ms, trace 7.6->0.6 ms).

Repository — an observed_at index turns every since-window query into a
range walk (recent_observations 7.5->1 ms); heard_nodes and first_seen
aggregate in one streaming pass by ISO-string comparison instead of
hydrating dataclasses and sorting; node_names groups in SQL (NOT
INDEXED past a slow planner choice).

Per packet — livefeed no longer repaints on events that drew nothing,
the channel list's stats TTL rises above the tick so the repository is
not queried per frame, watch-store alerts batch through the throttled
flush instead of a full-state disk write per new node, and the header's
360-bucket histogram/flags tuples are memoized per minute and packet.

Per open — the resolvers, topology canonical()/display_name(), and
Yen's route search memoize over their frozen inputs; timemachine, chat,
and livefeed drop duplicate queries; the config editor, device actions,
info, advert, and tx-optimize reuse the devstate session cache instead
of re-asking the radio.

Per frame, per screen — dashboard folds five window scans into one
digest; walk caches everything derived from its frozen graph (the
onward-count quadratic included); packet viewer, record dialog, trophy
rows, trace stats/log, and the node-detail route fan memoize on their
frozen inputs, invalidated by revision counters where the input can
move.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:14:53 -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 4.8 8daf3f1294 Dashboard: advertise scroll keys only when the overview overflows
The footer named "↑↓ PgUp/PgDn scroll" unconditionally, even on a tall
terminal where the whole overview fits and those keys do nothing. Gate the
scroll atom on the body actually being taller than its viewport, so Esc is
the only key advertised when there is nothing to scroll.

Add a reusable Screen.content_overflows predicate (from the metrics the
frame already records each paint) for the pattern, so any scrolling screen
can gate its footer the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:12:51 -04:00
Jean-Pierre MartineauandClaude Fable 5 dcc98f9cd8 Dashboard traffic: the packet bucket breaks out by payload class
The lone "packet" meter lumped every overheard raw frame together; the
traffic panel now meters each parsed payload class on its own row —
channel text, trace, path, request, … — glossed and iconed from the
packet viewer's shared tables, with only class-less frames left as a
bare "packet". The composite packet:<TYPENAME> bucket shape runs
through the stored seed (Repository.kind_counts) and the live tally
(MonitorService) alike; an overheard ADVERT/ACK frame reads "raw
advert"/"raw ack" so no two meters share a label. Raw-class meters
stay muted — colour keeps marking the decoded families.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:55:01 -04:00
Jean-Pierre MartineauandClaude Fable 5 79410c4f16 Live feed: the dashboard's packet stream, promoted to the menu
The feed panel moves out of the dashboard into its own first-class Mesh
tool, right below it: a full-screen newest-first stream with the same
row lanes, highlight walking, and Enter-into-the-packet-viewer, wired
to the hub by its opener exactly as the dashboard is. The dashboard
becomes pure overview — activity, traffic, RF health — scrolling as
one body, and sheds the viewer-only plumbing (prefix bytes, channels,
self name, node types) it only carried for the feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:52:45 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 5ee3bb998c Activity charts: bucket by local time in SQL, storing UTC
The Time Machine's day / hour / quarter-hour activity feeds now group on datetime(observed_at, 'localtime') in SQLite — rotating each stored-UTC timestamp into the machine's zone per-instant (DST-correct) before the key is sliced — so bars break on local midnight and local hour boundaries in any zone, half-hour offsets included, not on UTC edges. The window 'since' filter stays on the raw UTC column: the window is an absolute span, only the labelling is local. The old caller-side current-offset histogram rotation is gone, and _fill_days / _fill_hours and the axis tick labels now walk local wall-clock keys to match. Tests derive their expected local buckets through the same rotation so they hold in any runner zone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:24:29 -04:00
Jean-Pierre MartineauandClaude Fable 5 7f1d8e7d10 Day charts: even dot-split columns, packed right-first ticks, honest y-marks
Day bars now share the chart's dot columns via one floor-edge split
(_day_spans), so widths never differ by more than one dot and the wider
days interleave with the narrower ones instead of pooling on the left.
Every day at least three dots wide opens with its notch — the oldest
day's doubling as its clearance off the axis border.

Axis ticks pack as dense as two blank cells between labels allow,
claimed newest day first, oldest second, then right to left, so a
crowded axis thins from the middle-left instead of down to an evenly
spaced handful. Months are written on the calendar anchors (the oldest
day and the first of a month) rather than tracked across placed ticks.

The y-gutter's ┤ tick crosses a braille row at its third dot, so each
mark now quotes the value of a bar peaking at that dot — computed with
timeline_rows' own baseline math — instead of the row's top edge, which
sat a dot above where the glyph points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 21:09:12 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 a52dfc7b12 Dashboard: start with the newest packet highlighted
The feed used to open with nothing selected, a holdover from before the
list scrolled in place. That meant a single Esc after highlighting a
packet only peeled the highlight, leaving a second Esc to actually
leave the screen. Default the highlight to the newest packet and let
Esc back out directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 17:56:22 -04:00
Jean-Pierre MartineauandClaude Fable 5 94f565e61f Windowed lists everywhere the chrome should hold still
The atlas's list-in-a-fixed-screen pattern graduates to a shared
ListWindow (tui.screen): fit() settles the window, the marker rows, and
the highlight clamp together, and remembers the PgUp/PgDn stride; the
frame now records the viewport (or a dialog's budget) before render_body
runs so screens can size their chrome to it. Adopters: the dashboard
pins its charts and windows the feed; the trace screen pins route,
aggregates, and actions over a windowed results block; the path
composer pins the route preview over windowed suggestions (and gains
PgUp/PgDn); the nodes table pins its title, column header, and legend
around windowed rows. The composer's route preview and node rows also
join THE path widget, dropping its hand-rolled Name (hash) renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:23:41 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 d4f7b33580 path_text: one compact, single-line path widget
widgets.path_text is now THE way a relay path renders on one line:
each hop as its resolved name in the app-wide per-name hue (our own
node pure white) or, unnamed, as its bare hash through the shared hash
widget with the routing prefix lit — names carry no parenthesized hash
repeat, hops join on muted arrows, and an empty path reads as the
caller's word for it ("direct"). The packet viewer's via row and the
dashboard feed's trailing note both draw through it, so a relayed
frame's path reads identically (and in the same colours) in the feed,
the packet detail dialog, and the message-path views to come.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:23:37 -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 Fable 5 872156b388 Dashboard rebuilt around the packet viewer: open any feed row
New packet_viewer module: per-kind icons and colours shared by every
packet list, and a floating viewer that lays any packet out in full -
advert identity/location, telemetry payload, a packet's relay path,
a message's text - pageable over its list with PgUp/PgDn/Home/End.
The feed stores packets as data and renders per paint: up/down walk
the rows, Enter opens the viewer, Esc peels the highlight; the class
lane drops to icon-only on narrow terminals. Node names take the
app-wide palette (ours white), RF health and pulse render as labelled
grids so wrapped items align with their block, traffic lanes ride the
shared braille meter, the chart caption gains intermediate time marks,
and history buckets draw grey like the header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:35:54 -04:00
Jean-Pierre MartineauandClaude Fable 5 169ea8af11 Dashboard + Time Machine charts through the shared widget, now at the right
The dashboard's activity chart flips to the app-wide direction (oldest
left, now against the right gutter; caption relabelled) and both screens
drop their private renderers for ui.braillechart. The Time Machine's SNR
band is now zero-anchored instead of min-scaled: readings hang below the
grey zero line by their actual depth (or rise above it), with a third
braille row buying back the swing resolution, and the heading quotes the
zero-folded scale so the label and drawing agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:01:47 -04:00
Jean-Pierre MartineauandClaude Fable 5 5ff3d7ebd0 Dashboard activity chart: newest left, minute buckets, full braille width
The activity histogram moves to one-minute buckets held six hours deep,
so both the dashboard and the header can draw a dot column per minute
and still have history in reserve on wide terminals. The chart now
anchors "now" at the left edge (matching the header sparkline's
direction), stretches across every cell the terminal offers, and
mirrors its count scale on both gutters with per-row axis marks.

braille_bars gains the font's full horizontal resolution — each value
is one dot column, two per cell — and the traffic bars step in half
cells the same way. The pulse line quotes rates over exactly the span
the chart shows and sheds the word "heard" rather than wrap when the
terminal is too narrow for it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 05:03:13 -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