Skip to content

Totals over the window, a readable time axis, and a build stamp - #38

Merged
evandhoffman merged 13 commits into
mainfrom
build-stamp
Aug 28, 2026
Merged

Totals over the window, a readable time axis, and a build stamp#38
evandhoffman merged 13 commits into
mainfrom
build-stamp

Conversation

@evandhoffman

@evandhoffman evandhoffman commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes #35.

Three related pieces of one session, in one pull request because they were only
ever mergeable in order — see the commits for the separable history.

1. Totals over the window

Every legend entry gains a second number: how much moved over the window the
chart draws, beside how fast it is moving now. Network and disk in bytes,
packets and disk operations as counts.

Disk                     5 s
                      Totals
● Read   12.8 MB/s     35 MB
● Write  13.0 MB/s     14 MB

The sum is already in the buffer. A rate sample is the mean over the gap
before it, so Σ(rate × preceding gap) telescopes back to exactly the counter
delta. No counter history, no source changes, nothing read that the ring buffer
does not already hold — so it stays well inside the rule that v1 writes nothing
to disk. The round-trip test is the premise: differentiate a synthetic counter
with RateTracker, integrate it back, expect the original delta.

Three things it refuses to fake:

  • It reports the span it covered. Ten seconds after launch the buffer holds
    ten seconds, and "2 min" over a twelfth of that looks exactly like the right
    answer — which is why nobody would check it.
  • A gap nobody sampled is not traffic. An interval wider than maximumGap
    is clipped, so a laptop back from sleep cannot credit one sample with an hour
    that never crossed the wire. AppModel.totalGap follows the Sampling tab
    rather than assuming 0.5 s.
  • Fewer than two samples has no total — nil, not zero, the same answer
    RateTracker gives on a first read.

Network totals in bytes while the rate stays Mbit/s: a link is quoted in
bits, a volume in bytes. MetricUnit.accumulation holds the ÷8 as its only
definition, derived from the unit like direction and composition.

A card with totals draws a Grid — headed, right-justified — rather than the
wrapping FlowLayout. A magnitude is read by where its last digit sits, and
wrapped entries put them at four different left edges.

2. A time axis that does not collide

From a screenshot: 08:52:3008:53:0008:53:30. Three things were wrong at once.

  • The room was measured off the chart, not the plot. A flat 50-point
    allowance stood in for the y-axis — right for one card, generous for every
    card whose numbers are long, which are the cards whose plots are narrowest.
    Now read from the real plot rect via chartBackground. Not chartOverlay:
    an overlay would swallow the mouse-down a tile's drag needs.
  • Every stride was costed at the same label width, though a stride of a
    minute or more shows no seconds. Measured: 20 points against 32.
  • The labels were wider than they needed to be — own size now, and the hour
    loses its leading zero.

Rule 2 now beats rule 1. "Never fewer than two labels" used to win on the
grounds that slightly tight beats bare. Right about slightly tight, wrong about
the output: four labels 32 points wide with centres 40 apart. Two labels are
still what the walk reaches for; what is gone is the promise.

Turn the time labels sideways, new in the Charts tab, off by default: on its
side a label costs its line height, so the narrowest card fits four times where
upright it fits two.

3. A build stamp in the title bar

Monitor
v1.4.0-11-g0ac6bafa · Aug 28 09:56

There to identify which build produced which screenshot. White on black in the
system font — deliberately not the panel's palette or the cards' monospaced
face. It is the one thing in the window that is not a reading: everything else
is a measurement styled to be scanned, and this is a label on the photograph.

Two facts, two sources. The commit is stamped at build time by a new
StampCommit prebuild plugin (git describe --tags --always --dirty) — a
running program has no other way to know, and a checked-in constant is one
somebody has to remember to update. The build time is read at runtime from the
executable's modification date: stamping it too would rewrite a source file on
every build and recompile MonitorCore every time, for a fact the filesystem
already has. The plugin rewrites its output only when the hash changes, for the
same reason.

Three things this turned up on the way:

  • macOS 26 wraps toolbar items in a shared glass capsule, which made the stamp
    dark-on-light and clipped it. It opts out with
    sharedBackgroundVisibility(.hidden), gated to 26 and additive.
  • The window's own title is removed, or the name draws twice. toolbar(removing:)
    is macOS 14 but the .title kind is 15, so 14 gets an empty navigationTitle.
  • Removing the title cost the toolbar its trailing alignment — the title had been
    taking the slack in the middle, not the items' placement. They are
    .primaryAction now, which says what the layout actually depends on.

Verification

215 tests in 26 suites, swift build -c release, and swiftformat Sources Tests Plugins --lint all clean. Lint now covers Plugins; ci.yml updated to match.

ChartAxisTests is rewritten against the new contract, keeping every existing
claim and adding the ones this needed — chiefly labels never overlap, swept
across every window, six plot widths, both orientations and forty phases each.

All three pieces were looked at in the running app and revised from what they
actually looked like, which is where most of the above came from.

https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj

A rate sample is the mean over the gap before it, so multiplying each
sample by the interval it measures and summing telescopes back to
exactly the counter delta. No counter history is kept, no source
changes, and nothing is read that the ring buffer does not hold.

WindowTotal reports the span it actually covered alongside the value.
Ten seconds after launch the buffer holds ten seconds, and putting
"2 min" under a number covering a twelfth of that is the quiet kind of
wrong. A gap wider than maximumGap is clipped rather than credited in
full, so a slept laptop cannot invent traffic.

MetricUnit.accumulation says what a rate adds up to, derived from the
unit the way ChartMirror and ChartStack read direction and composition.
Network totals in bytes and the divide by eight lives there: a link is
quoted in bits, a volume in bytes.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
A second number per legend entry: how much moved, beside how fast it is
moving. The span sits once beside the title rather than after every
entry, since the History picker is global and repeating it down a legend
is four copies of one fact.

The span reads the window only when the card really has it. Ten seconds
after launch it says ten seconds, because "2 min" over a twelfth of that
looks exactly like the right answer and nobody would check it. An entry
covering materially less than the card's stated span is dimmed — one
source failed while its neighbour kept reading.

AppModel.totalGap is four ticks of the master clock, so it follows the
Sampling tab. A ceiling fixed for the 0.5 s default would clip every
interval of a sampler slowed to 2 s and quietly report a quarter of the
traffic.

Off by default, like mirroring and stacking. Narrower reason: those
change what the picture means and this only adds a number, but it adds
one to a header already decided by ViewThatFits.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
The argument for matching mirroring and stacking lost on first contact:
shipped off, the reaction to the finished feature was "I don't see it".

Those two change what the picture means, so a reader who never asked for
them deserves the chart they had. A total only adds a number beside one
already there, and a number nobody can find is worth less than a header
that reflows.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
08:52:3008:53:0008:53:30. Three things were wrong at once.

The room was measured off the chart rather than the plot, with a flat
50-point allowance standing in for the y-axis — right for one card, and
generous for every card whose numbers are long, which are the cards whose
plots are narrowest. chartBackground hands over the real plot rect.
Not chartOverlay: an overlay would swallow the mouse-down a tile's drag
needs.

Every stride was costed at the same label width, though a stride of a
minute or more shows no seconds and needs a third less room. Measured, at
the new smaller label size, that is 20 points against 32. Budgeting the
coarse strides at the wide figure left the axis unable to reach for the
one a cramped card wanted.

And the labels were wider than they needed to be. They get their own size
now, smaller than the y-axis labels beside them, and the hour loses its
leading zero.

Rule 2 now beats rule 1. Slightly tight labels do beat a bare axis, but
these were not slightly tight, they were on top of each other. Two labels
are still what the walk reaches for; what is gone is the promise.

Turning them sideways is how a narrow card has both: on its side a label
costs its line height, 10 points against 32, so it fits four times where
upright it fits two.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
A monitor is left running for days, and the copy on screen is very often
not the copy just built. "Am I looking at the change I just made?" should
not cost two clicks.

The two halves are sourced differently on purpose. The commit is stamped
at build time by a prebuild plugin running git describe, because a
running program has no other way to know it and a checked-in constant is
one somebody has to remember to update. The build time is read at runtime
from the executable's modification date: stamping it too would rewrite a
source file on every build and recompile MonitorCore every time, which is
the fast swift run loop gone for a fact the filesystem already has.

describe rather than a bare hash, for the -dirty. A build with
uncommitted changes is not the commit it names.

Drawn in Theme.readout rather than the title bar's secondary style. A
stamp nobody can read at a glance is one nobody checks, which is the same
failure as not having one.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
@evandhoffman evandhoffman added enhancement New feature or request release:minor Merging this bumps the minor version labels Aug 28, 2026
Wrapped entries put the totals at four different left edges down the same
card. A card with totals now draws a small Grid instead of a FlowLayout:
swatch, name, rate, total.

Right-justified, because these are magnitudes and a magnitude is read by
where its last digit sits — 104 MB over 48 MB aligned on the left puts
the hundreds above the tens and hides the difference the column exists to
show.

Headed, because a second bare number beside a rate does not say what it
is. The span beside the title says how long; the heading says of what.

Under the title, never beside it. ViewThatFits is the right question for
a wrapping row and the wrong one for a column of figures: pushed right of
the title on a wide card, the heading floats in the middle of the header
with nothing under it that reads as a table.

Cards with nothing to total keep the flow — Memory's seven slices and
five temperature sensors want wrapping, not a column.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
The stamp borrowed the toolbar's styling and came out as dark text on a
light glass pill — the lowest contrast anywhere in the window, on the one
element whose whole purpose is to still be legible in a PNG somebody
opens next month. It was clipped where the capsule ended, too.

macOS 26 wraps every toolbar item in that shared capsule. The stamp is
not a control and should not dress as one, so it opts out and paints its
own background. The call is gated to 26 and additive: earlier releases
add no capsule and need no opt-out.

Not the panel's palette and not the cards' monospaced face either. This
is the one thing in the window that is not a reading — everything else is
a measurement styled to be scanned, and this is a label on the
photograph.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
One block — a heading with its build underneath — rather than a chip
parked next to a system title repeating it. So the window's own title
goes: toolbar(removing: .title), or an empty navigationTitle on macOS 14,
where the .title kind does not exist. The Window scene keeps its real
name, so the Window menu and the Dock still say what this is.

That name now lives beside the version in MonitorVersion. Two spellings
of one name is the sort of thing nobody notices until a screenshot.

A rounded rectangle rather than a capsule now that it is two lines: a
capsule's ends bow away from a left-aligned second line.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
Removing the window title cost the toolbar something that was not
obvious until it was gone. The title was taking the slack in the middle,
and it was that, not the items' own placement, pushing the History picker
and the size and rate controls to the right. Without it, .automatic
packed them up against the stamp.

.primaryAction says what the layout actually depends on, instead of
leaning on something that is no longer there.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
@evandhoffman evandhoffman changed the title Say which build this is, in the title bar Totals over the window, a readable time axis, and a build stamp Aug 28, 2026
@evandhoffman
evandhoffman changed the base branch from time-axis-labels to main August 28, 2026 13:57
Splitting this session into three left them stacked, and a stacked pull
request is not independently reviewable or mergeable — so the split
bought none of the review value it looked like it bought. What it cost
was a squash-merge and a release each, for one change.

The separable history belongs in the commits, where it is free.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
There is one window. A change to a card, the axis under it and the title
bar above it are seen together or not at all, and the only useful
question — does this read better — can only be asked of the whole screen.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
@evandhoffman
evandhoffman merged commit 751994d into main Aug 28, 2026
2 checks passed
@evandhoffman
evandhoffman deleted the build-stamp branch August 28, 2026 14:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request release:minor Merging this bumps the minor version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show how much moved, not only how fast: cumulative totals over the window

1 participant