Commit Graph
170 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 4.8 b94e0469db Settings: allow the pytest and beep commands used while iterating
Adds a handful of read-only test-run invocations (targeted pytest selections, a UTF-8 full-suite run) and the done-beep to the permission allowlist, so routine iteration doesn't stop to prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:52:36 -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 Opus 4.8 29b1d66c7c Message paths: stack relay labels above/below in the name's hue
Each relay's hash byte now sits straight above or below its marker, fanning outward from the centre line, instead of beside it — drawn in the node's own name-palette hue so it reads as that mesh name and never crowds the line running through the marker. Endpoints keep a beside-the-marker label since they ride the canvas edges. The graph box is sized and centred on the fan's real up/down reach (rarely symmetric) rather than a mirrored block, so it spends rows on the paths. MapCanvas gains an avoid_dots placement and a labels-only vertical anti-stacking margin, so a label may sit immediately above/below a single-glyph marker while dense text still can't silt into a block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 21:06:51 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 bdd4650133 Full width: reclaim the terminal's under-reported last column
On some terminals prompt_toolkit's size probe reports the window one column
narrower than it really is, so the frame draws to columns-1 and the true
last column sits unused (selectable to the right of the border). Wrap the
real terminal output in _WidthExtendedOutput, which reports columns+1 from
get_size(); since the renderer and the frame compositor both key off that,
the final column now gets drawn.

Gated behind _RECLAIM_LAST_COLUMN (env MESHTERM_FULL_WIDTH=0 to disable) so
it can be turned off on a terminal whose probe is already correct — there
the extra column would fall off-screen and wrap — and eventually promoted
to a user setting. Test-supplied outputs are never wrapped, so headless
rendering keeps its exact size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:47:08 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 dfd7d2332d Battery gauge: a braille fuel gauge pinned to the header's right edge
Add a one-cell braille battery indicator to the status bar, shown only when
the companion reports a pack. The cell fills bottom-to-top in four tiers —
green healthy, orange at a quarter, red near empty — with the true percent
beside it; it blinks red↔dim when critically low and sweeps a filling
animation while charging. A session-long BatteryService polls the pack,
maps millivolts to a state-of-charge estimate off a single-cell LiPo curve,
and infers charging from a rising terminal voltage (firmware exposes no
charging flag). The header pins the gauge flush right and the activity
pulse cedes exactly the cells it needs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 18:12:43 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 cba3be3f69 Atlas: a sparser fan, every node named to the right of its icon
Halve the neighbours the canvas fans out (a wider _FAN_SLOT_DOTS) so the
shape reads instead of crowding, and name each non-selected node just to
the right of its marker via a new _label_right — the walk's reading way.
The selected node keeps its smart two-sided placement. Widen the east
gutter (_PAD_X_DOTS) and cap fan labels (_FAN_LABEL_W) so even the fan's
outermost name lands without running off the edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 17:49:17 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 b508f2fe27 Busy overlay: skeleton card with a Knight-Rider braille scanner
Replace the between-screens ring spinner with a titled skeleton card: a
heading, the one-cell working chip beside a "reading from <device>…"
caption, and a two-row braille LED bar in place of the old ring.

The bar is a Knight-Rider scanner — a red head sweeps left↔right and
bounces off both ends over a dark-gray track, dragging a persistence-of-
vision comet trail (per-tick decay) with a white-hot core. It rides the
same tick that spins the chip and fades in from black with the card, so
the wait reads as work rather than a frozen bar.

Thread an optional title/caption through busy_overlay so each gap names
the screen being fetched and the companion it's reading from. Give the
CLI progress dialog the same liveness: a spinning chip on every row and a
slim braille meter, with no dead track on indeterminate tasks. Drop the
now-unused RingSpinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 03:43:15 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 71b7d44263 Splash footnote: attributed copyright with auto-extending year range
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:34:25 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 350cf67360 Day/hour ticks: fit to each label's real width, not the longest
The axis reserved a fixed worst-case budget per tick (chars // 8, sized
for "Jul 12"), so short labels like bare day numbers were dropped even
where they had ample room — the 7 d window lost a day it could plainly
fit. Replace the estimate with _fit_ticks: step the tick count down from
every bar until an even subset actually clears the collision rule for the
labels it would draw, rebuilding the labels each try so bare "7" is
measured as one cell, not six. Both ends stay annotated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:12:12 -04:00
Jean-Pierre MartineauandClaude Fable 5 bc1ba7de4c Day charts: even-spaced date ticks back, an hour per column at 24 h
Revert the axis ticks to the evenly-thinned dated labels — one per day
where they fit, else a subset keeping both ends, the month printed on the
first tick and each rollover. The packed right-first scheme read worse; the
even-split bars and the honest y-marks stay. _thinned_picks now holds the
shared fit-or-subset choice.

The 24 h window charts an hour per column instead of a day (its name is
"24 h", not "1 day"): a new Repository.hourly_series buckets by UTC clock
hour like daily_activity does by day, _fill_hours fills the quiet hours,
and _hour_ticks labels the axis with local HH:00 times closing on "now".
Both feed the same bar machinery — only the resolution and labels differ.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 23:13:49 -04:00
Jean-Pierre MartineauandClaude Fable 5 8d2096767f Day charts: notch only between days, first bar flush with the axis
The braille glyphs already carry an intra-cell left margin, so the old
leading GAP on the oldest day read as double padding against the axis
border while the right edge showed single. Boundaries now pay the n-1
notch dots and the bars split the rest evenly, flush at both borders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 22:50:23 -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 Fable 5 ff76f41736 Allowlist pytest and git add/commit so routine runs stop prompting
Generalizes the one-off pytest entries to prefix rules across the
python spellings actually used (python, venv exe, Bash and PowerShell)
and covers the commit workflow CLAUDE.md already sanctions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:07:43 -04:00
Jean-Pierre MartineauandClaude Fable 5 060f4b1eb6 Message paths: hash-byte labels, white-over-gray edges, a taller graph
The graph now speaks in path-hash bytes: every relay gets its marker
plus the first byte of its hash in the marker's colour, while the rows
beneath carry the names annotated trace-style (YUL-Poly (3d)), so the
two halves cross-reference. Edges drop the SNR gradient — the selected
path draws white over the unused paths' gray, which is the one question
the graph is asked. Columns are the distinct depths spread evenly across
the width, height follows the fan's real reach (lanes are no longer
burned on relay-less direct paths), and labels sweep for a spot clear of
the drawn lines (MapCanvas grows avoid_dots) before overprinting one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:54:54 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 adc0e9f04d Clearing a channel drops its unread from the global count
Unread is tracked in-memory in ChatService, keyed by conversation. A
cleared channel's key stayed in that map with its count intact, so the
header badge kept counting messages nobody could read anymore. Route
the clear through set_active so the key's unread is popped before the
slot is wiped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 18:05:19 -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 7f1750df0e purge-test-data: mock pollution housekeeping as a skill
meshterm --mock writes to the real ~/.meshterm database, so simulator
sessions leave fake nodes (Alice, hub-1, hopN hashes, a1b2c3d4… keys)
in the history behind the atlas, dashboard, and trace suggestions. The
new skill (runs on the cheap model, like commit/verify) backs the DB
up, deletes exactly the MockDevice-fingerprinted rows — deliberately
conservative: no short-hash matching, background run rows untouched —
and verifies a clean second pass. The verify skill now names it as the
mandatory last step after any mock run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:35:02 -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 Fable 5 94aba0cbcd Message paths: a path graph over selectable, sideways-scrolling rows
The ^P view grows from a static scroll window into its own dialog. Up
top, every distinct path the message took drawn in the atlas's edge
style — origin left, us right, the final relay → us link coloured by
that path's reception SNR, earlier links slate — with markers for every
node but labels only along the currently selected path, so a many-path
graph stays readable; the selected path draws bright, the rest dim.
Beneath, one row per arrival with a selection cursor: ↑↓ move it (the
graph follows), and a row longer than the dialog scrolls horizontally
with ←→ — the whole line shifts under an edge …, and snaps back the
moment the selection moves on. crop_cells in tui.render is the new
cell-exact window primitive behind that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:47:56 -04:00
Jean-Pierre MartineauandClaude Fable 5 2dca52fa56 Atlas: focus-left canvas that earns its area, list that scrolls in place
The graph now takes the majority of the screen (tapering a little on
tall terminals), draws only as many neighbours as its area can carry —
strongest links first, the weaker rest collapsed into one … marker that
lights up as whichever hidden row the list highlights — and lays the
walk out left-to-right: focus toward the left with its label west of
the marker, trail-back ducked under it at the far edge, the fan and its
labels east. The link list beneath scrolls inside the fixed screen:
canvas, legend, and heading hold still, faint ↑/↓ n more markers
bracket the window, and PgUp/PgDn stride by a windowful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:41:58 -04:00
Jean-Pierre MartineauandClaude Fable 5 8330c5a5db path_text grows the trace flavour: one widget for every path
Hash annotations (Alice (3d63)), None hops for our own device at a
route's endpoints, and dim_from to fade a boomerang's mirrored return
leg — so the trace screens' walked/planned routes, the per-hop link
tables, and the CLI trace output all render through THE path widget.
Trace routes thereby join the app-wide colour rules (per-name hues,
pure-white you) instead of their old flat brand/accent, and the live
route line wraps under a hanging indent instead of folding to column 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 15:41:55 -04:00
Jean-Pierre MartineauandClaude Haiku 4.5 534128ab84 Configure verify skill for a cheaper model
The commit and verify skills were burning the full parent-conversation
context on every routine invocation. Both now set model: haiku in
frontmatter so that context is billed at the cheap rate instead of
whatever model the main session is running.

context: fork was tried on both skills to avoid shipping the parent
context at all, but reverted: a bare "/commit" or "commit this" with no
explicit argument caused the forked subagent to skip the skill's steps
and return a generic greeting instead of committing. Fork only works
reliably when the caller always supplies a rich task description as the
argument, which isn't true for these skills' common invocation.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-12 11:35:45 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 56b7580e48 Chat shows a message's delivery paths on demand (^P)
The radio's RX log has recorded every frame's relay path all along;
now a chat message can be asked how it got here. ^P floats a paths
window for the picked (or latest) message: one row per logged arrival
— time, reception SNR, and the relay chain through the shared compact
path widget — so a channel broadcast that reached us three ways reads
as three rows over two distinct paths, resends labelled.

Channel messages are matched by evidence, not guesswork: overheard
GRP_TXT frames decrypt under the channel's own key (fetched from the
device when the conversation didn't carry it) and match the message by
content, tolerating the "Name: " sender-prefix convention on the wire
— our own broadcasts included, since repeaters' rebroadcasts of us are
overheard like anything else. Direct frames are ECDH-encrypted, so the
view offers the window's TXT_MSG frames matched by time alone and says
so plainly. The sender-prefix parser moves to core.channels where the
matcher, the transcript, and the dashboard feed now all share it, and
the repository gains the windowed packet_frames_between query.

Both chat kinds now share one message-pick model: ↑ picks, the pick
walks with ↑↓/PgUp/PgDn/^Home and day-jumps with ^PgUp/^PgDn, ^End
returns to compose. Enter on a pick replies in a channel (unchanged)
and opens the paths in a direct chat, each with its banner cue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:37:57 -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 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 34c6ada1ae Write the UX standards down: CLAUDE.md
The consistency pass's decisions, recorded where every future session
reads them: the lexicon (node/contact/heard/key/hash/path/route/via,
Back vs Quit), the list furniture rules and their enforcement points in
ui/menus.py, title casing and dash semantics, the footer-hint grammar
and Esc verbs, the dialog conventions, the one-glyph-per-concept icon
table, and the colour rules the screens already share.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:06:10 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 826ab5b4ee Buttons are chips, status marks are ✓/✗, confirms are dialogs
Three button/status stragglers join the house style. The tracing
dialog's "❯ [ Abort ]" — the one control drawn with a pointer and
brackets — becomes the centered reverse-video chip every other popup
button already is (ButtonDialog, the reconnect dialog's Abort). Chat's
✅/❌ delivery emoji become the app-wide ✓/✗ status marks in their
ok/err styles: the emoji belong to the packet-class icon lane, and the
single-width marks keep transcript rows evenly spaced. And the channel
manager's Clear — the last bare Yes/No confirm in the app — becomes a
standard destructive dialog (Cancel left, Clear right and default,
danger theming) like every other consequence gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:04:35 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 84a2c45f12 Footer hints: one grammar, Esc always last, one key notation
The key hints now follow one sentence shape across every screen:
navigation keys first, action keys next, Esc last — the quit dialog and
TX apply dialog had Esc leading. The Esc verb is now systematic: "Esc
back" leaves a screen, "Esc close" dismisses a read-only floating view
(result windows and the share/key popups switch over), "Esc cancel"
abandons a prompt, "Esc keep" leaves a value picker unchanged. Bare
hints fill in their missing verbs ("↑↓ move", the map's and composer's
naked "Esc"), the confirm prompt says "Enter select" like its button
siblings, and key notation unifies on the compact forms the app already
half-used: ^R/^End for Ctrl chords (chat's "Ctrl-R" included) and ⌫ for
backspace (the map/atlas "Bksp").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:59:22 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 d1c8ed73a0 Titles speak one language: Sentence case, no emoji, — for subjects
Every screen and dialog title now follows the same three rules. Titles
are sentence-cased ("Trace — YUL-Poly", "Dashboard — mesh overview",
"Nodes"), never bare lowercase ("trace · x", "nodes", "command line ·
x", "path width", "admin login" and a dozen siblings). An em dash
introduces a title's subject or qualifier while middle dots stay for
chained status atoms ("Map · z12 · 34 nodes · 2 km across"), so the two
separators mean different things instead of being used at random. And
no title carries an emoji — the icon belongs to the menu row; a
content-sized floating dialog whose title emoji the terminal draws a
cell narrower than Rich measures leaves the border short (the bug the
Send advert popup already documented), so the Watchtower, Courier,
Time machine, identity-key, reboot, sync, restore, location, factory
reset, and advert dialogs all drop theirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 03:55:12 -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 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 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 4e8325af08 Mesh day charts: exact axis width, notched day boundaries
_day_columns floor-divided chars*2 by the day count and dropped the
remainder, so the stretched bars often fell short of the width the
axis border and caption assumed — the chart reading as shifted left
of its own axis. Now splits in character units (falling back to plain
dot columns when there are more days than characters) and spreads the
remainder across the oldest days, so the total always lands exactly on
2 x chars.

A day spanning more than one character also opens with a blank dot
column now — the left half of its leading braille cell — so
same-height neighbouring days still read as separate bars instead of
fusing into one solid block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:16:59 -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 87b3410ca8 Time Machine mesh page: day charts up to chart standards
The per-day charts drew one dot column per day, leaving a short
history as a sliver in a wide terminal, and captioned themselves with
raw ISO dates. Days now stretch into day-wide bars that fill the
chart's width (falling back to per-day density once history is deep),
and the captions speak the house style - "Jul 05" marks with "today"
closing the right edge, like the node page's "now".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:49:02 -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 3fdf9ae7c7 Courier recipient picker: heat-coloured names, shared type glyphs
The contact rows join the picker convention the Nodes list and Time
Machine already speak: per-type glyph in the app's marker palette and
the name coloured by recency heat instead of flat brand-on-everything.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:49:02 -04:00
Jean-Pierre MartineauandClaude Fable 5 1205ef211b Packet viewer paging walks the feed highlight in step
The viewer reports each navigation through on_navigate and the
dashboard follows by re-highlighting that packet in the feed, so
closing the viewer lands where the reading left off. Home/End move
the highlight to the newest/oldest packet while it is active (plain
scroll-to-edge otherwise).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:44:39 -04:00
Jean-Pierre MartineauandClaude Fable 5 21c5017706 Chat byte budget rides the input line and can no longer vanish
The counter was its own last body line, and the pinned day-divider
header reserves the top viewport row without relaxing the bottom scroll
clamp - so exactly when the transcript filled the screen, the counter
fell off the bottom. The budget now trails the compose text inline
(wrapping along with it, costing no row), and the frame slides the
window down a row when a sticky header is pinned at the very bottom,
so the last body line always wins over the duplicate heading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:44:39 -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 072a4573fe Activity charts: a previous session's history dims to grey
timeline_rows and activity_sparkline accept per-column style overrides,
and the monitor exposes which histogram buckets this session actually
heard. The header pulse draws seeded history grey and live traffic
green, so a restart is visible in the chart instead of masquerading as
fresh reception; the dashboard chart follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:35:41 -04:00
Jean-Pierre MartineauandClaude Fable 5 3903d996db Activity and traffic persist across sessions, with housekeeping
The monitor seeds its six-hour activity buckets and its by-kind tallies
from stored observations at session start, so the header pulse, the
dashboard chart, and the traffic panel open mid-story after a restart
instead of counting from zero. A once-per-session sweep prunes
observations past the new history_days retention setting (default 365,
0 = keep forever), keeping the always-recording database bounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:22:29 -04:00
Jean-Pierre MartineauandClaude Fable 5 30498efe0b Time Machine: intermediate time marks, contact names fill the blanks
Chart captions now draw through axis_caption, so a wide volume/SNR/day
chart gains quarter/mid time marks between its edge labels. Nodes whose
stored observations never carried a name ask the device's contact list
before rendering as "unknown" (picker and arrivals alike), and the hash
lanes go through the shared hash widget. make_node_resolver learns an
optional stored-names fallback and the repository can serve the latest
advertised name per node, for the dashboard feed to use next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:16:38 -04:00
Jean-Pierre MartineauandClaude Fable 5 b50d201c24 Hash and name conventions: display budget, shared hue palette
highlighted_hash grows a width budget - shorter than the key shows an
ellipsis, shorter keys pad so lanes align - making it THE hash widget.
The chat sender palette moves to theme.name_style as the app-wide node
name colour rule; our own node is always the pure-white "you" style
(the nodes table's self row now follows it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:12:01 -04:00
Jean-Pierre MartineauandClaude Fable 5 f9411bfc9b Braille meter and axis captions: two new shared chart widgets
meter() is the one horizontal proportion bar (two fill steps per cell,
full-height tally or slim-on-a-track gauge); the SNR bar now draws
through it. axis_caption() fills a timeline's caption with as many
intermediate marks as the width fits, instead of only its two ends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:11:42 -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 36e8173b5a Map: arrows-only movement, find-as-you-type node filter
Panning is the arrow keys (Shift for fine), zoom is PgUp/PgDn alone, and
reset moves to Home — freeing every letter and +/- so typing can drive a
live name filter instead: matches keep bright white labels and always win
the label contest, everything else dims to bare context glyphs, Enter
frames the matches, Backspace edits, and Esc peels the filter before the
map. The footer swaps to the query while a find is active, the title
counts matches, and the location picker follows the same movement keys
with find disabled (typing there has no job). Hints fit 72 columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 15:17:33 -04:00