18 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5.5 11b5274b31 Docstrings and comments of the tests are in ASD-STE100
Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 21:36:50 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 b6e7cd5c16 The handhelds are named for their variant: picocalc-lyra and cardputer-zero
M5Stack makes three Cardputers and only the Zero runs Linux; the PicoCalc
takes several cores and only the Luckfox Lyra runs MeshTerm. So the bare
names were ambiguous, and every reference to the two devices now carries
the variant: the --platform values and MESHTERM_PLATFORM, the Platform
constants (PICOCALC_LYRA, CARDPUTER_ZERO), their lane decks and every
screen's picocalc_lyra_lane / cardputer_zero_lane, the font inventories,
the lane chips' theme styles, the scripts/ folders, the two chrome test
files, and the docs and CLAUDE.md that name them.

There is no alias: --platform picocalc is refused with the three real
choices listed, rather than falling back to the desktop layout. The
PicoCalc is auto-detected from its device tree, so it needs nothing.
What names hardware or a user's data keeps its name: the PicoCalc
keyboard's input-device name, its battery's sysfs supply, and the serial
profile the Lyra setup script writes. .gitignore follows the firmware's
folder, and the changelog keeps the words each release used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 11:52:41 -04:00
Jean-Pierre MartineauandClaude Opus 5.5 fb2ead3cc3 Charts keep a three-cell y-axis gutter; the Rhythm drops its slice width when narrow
A chart's y-axis labels got exactly the cells their widest mark needed,
so the gutter grew under the reader as a busy minute took the peak from
9 to 10 to 100, sliding the whole chart sideways. Every gutter now gets
at least three cells (AXIS_LABEL_MIN), which hold every mark below a
thousand and every compacted one, so it stays put.

The Time Machine's Rhythm heading ended on its slice width ("20-min
slices"), which on the PicoCalc's and Cardputer's 53 columns wrapped the
heading onto a second row. Where the whole heading would wrap, the slice
width goes; the axis under the chart already marks the day's hours.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 02:59:31 -04:00
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 40ecf5151d The tests' docstrings lead with a sentence too
The last 122 `D205` sites, which takes `ruff check .` to zero.

Same shape as the package pass and just as additive: these were a paragraph
with no summary, so each one gains a plain sentence saying what is being
asserted, and every word that was there moves down into the description. The
dense ones benefit most — `test_pathline`'s 23, where "A seam is one cell, not
two" now stands alone and the chevron mechanics explain it underneath, and
`test_tui`'s wide-glyph repaint test, whose seven lines on prompt_toolkit's
relative cursor now sit under the one line that says what the test pins.

Four module docstrings (`test_advert`, `test_gallery`, `test_platforms`,
`test_repeater_admin`) opened with "X tests: a, b, and c" and now name the
subject on the first line with the list below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 20:37:51 -04:00
Jean-Pierre MartineauandClaude Opus 5 71b16ec7b7 The annotations, imports, and dead locals answer to ruff
`ruff check --fix` over the whole tree, ahead of putting the linter in CI:
1977 mechanical fixes, overwhelmingly `Optional[X]` → `X | None` (1186 in the
package alone) and the quoted annotations that `from __future__ import
annotations` already made unnecessary, plus import sorting and unused imports.
Nothing here changes behaviour — the suite is 1885 green before and after.

Four findings were real rather than stylistic and are fixed by hand: a dead
`cells` range in `MapCanvas`, a dead slot dict in the heat-ladder test, a
`ProgressScreen` task handle nobody read, and a tab loop binding a `label` it
never used. The two `F821`s were quoted forward references to names imported
inside their function bodies — now declared under `TYPE_CHECKING`, which keeps
`test_performance`'s local import (it guards the startup import graph) while
letting the linter resolve the name.

412 findings remain and are deliberately untouched: they need a decision about
the rule, not an edit to the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 17:26:12 -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 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 Opus 4.8 fa07d23adb Activity sparkline: drop the recency term that collapsed the scale
The shared scaling peak (activity_peak) was fully-lighting ~60% of the
header pulse's bars on a healthy 1–18 packets/min history. Cause: the
recency damper decayed each bucket's *count* by 0.5^(age/half-life)
before taking the 90th percentile, then that percentile scaled the
*undecayed* bars drawn against it — a unit mismatch. With a 30-min
half-life over a 6h pool, most traffic mass sits older than one
half-life, so the weighted percentile collapsed (1.14 here) and the
floor (3) became the ceiling; every minute with ≥3 packets, anywhere in
the drawn history, then saturated.

Drop recency; keep the two dampers that actually deliver the steady,
shared ceiling: an outlier-robust high percentile over a pool deeper
than is drawn (steady — the percentile barely stirs as one bucket
scrolls off), plus a floor (a lone packet stays a green nub). On the
live DB this restores peak 8, ~4–13% of bars full, heights spread 1–4.

activity_peak loses its bucket_seconds/half_life_min params; menu.py and
channels.py drop the now-unused bucket-seconds plumbing. The tall
dashboard chart was already on an honest max and is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:52:41 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 241c2c062a Activity sparkline: a steady, shared scaling peak instead of the bare window max
Relative-to-peak scaling reads a sparkline's shape well but is jumpy: the plain
window maximum lurches whenever the busiest bucket enters or ages out, and a lone
packet in a quiet window fills the column because it *is* the max. Add
`activity_peak`, a pure function that folds three dampers into one ceiling, each on
a different axis:

- recency — each bucket discounted by `decay ** age` (half-life 30 min), so an
  aging burst releases its grip smoothly and the ceiling glides rather than snaps;
- robustness — a high percentile (P90) of the weighted counts, not their max, so a
  freak-busy bucket clips to full height instead of crushing every other bar;
- floor — never below a small floor, so a stray packet in a lull stays a green nub
  on the baseline, not a full column.

The header pulse scales over the monitor's full six-hour history (deeper than the
row draws), and the channel manager pools every visible channel's histogram into
one shared ceiling (memoised per snapshot), so the rows stay comparable. To give
the channel peak history to decay over, widen its query to 6h/72 buckets while the
sparkline still draws only the newest 24 (2h). Split the dashboard's 2h
observation window off into its own OBSERVATION_WINDOW so it keeps its "reception
over 2 h" meaning. The tall dashboard chart is left on its honest max — it quotes
real magnitudes on its axis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:31:02 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 f07f8c1b93 Activity sparkline: scale to a peak, shared across a column
Bar heights now scale relative to a peak the way the dashboard's tall
activity chart does, instead of stepping at fixed count thresholds: the
busiest bucket fills the column and the rest draw in proportion.

A new `peak` argument is the scaling ceiling and the sharing mechanism.
Left None, each sparkline self-scales to its own window (the header
pulse, alone on its row). Passed a value, every sparkline handed the
same one shares a scale, so their bars are comparable — the channel
manager computes the busiest bucket across all channels via a new
_LiveStats.peak() and hands it to each row. No hidden cross-instance
state: the scale is a pure function of each call's inputs, which stays
correct for the windowed channel list where only visible rows render.

Drops the _HEADER_ACTIVITY_LEVELS / _ACTIVITY_LEVELS threshold ladders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 03:32:44 -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 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 fa340bec03 Chart notches break the baseline cleanly to the axis
The day-boundary notch left its bottom dot lit: the zero baseline is
drawn per braille cell (both dot columns), so it filled in under the
notch's blank column, and the notch connected to the axis instead of
extending down to it. Worse, when the neighbouring bar was tall the
notch's bottom dot took the bar's colour, not even the faint grey.

Introduce a GAP sentinel in braillechart: a timeline column that
renders fully blank — no bar and no baseline — the one deliberate
break in the "grey is zero" flatline. _assemble now decides the
baseline per dot column rather than per cell, so a GAP column goes
blank clean to the axis while its cell-mate keeps its own zero line.
The Time Machine's day charts mark their notches with GAP instead of
None.

A genuine zero-value day still draws its faint grey baseline as
before — only the notch column breaks it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 21:10:00 -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 219f121625 Braille chart widget: one timeline renderer, now on the right, grey = zero
A new ui.braillechart module every value-over-time row will draw through,
carrying three app-wide rules: time flows left-to-right (the rightmost
column is now), a faint grey baseline always marks value zero (mid-chart
when a series goes negative, so positives rise and negatives hang), and
both braille dot columns are used for two readings per character cell.
Existing charts are rewired onto it in follow-up commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 14:50:23 -04:00