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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>