Commit Graph
27 Commits
Author SHA1 Message Date
Jean-Pierre MartineauandClaude Opus 5 972c0b6f03 Screens: Esc is the way out, so no row spends two lines saying so
The app-wide Back row existed because it duplicated Esc — two of its call
sites said as much in a comment. On both platforms Esc is a labelled key
and every footer hint names it, so the row bought nothing for the blank
separator and line it took out of every screen; on the PicoCalc's 26 rows
that is a row in thirteen.

back_rows() is gone, and with it the four hand-drawn copies: ReorderScreen's
clean-state row, the live feed's pinned foot stop (the cursor now clamps on
the oldest packet), node detail's tail action, and the action-list rows on
the trace and TX-sweep screens.

Kept, because there leaving is a choice rather than an exit — the same rule
that keeps a dialog's Cancel button: exit_rows()' staged pair, where Apply
has no key of its own and ✗ Back names what the other way out costs;
ReorderScreen's dirty-state pair; the two Quit rows, which initiate the
app's terminal action behind a confirm; and the new-record dialog's Close,
which is its default, so dropping it would let a reflex Enter navigate.

With no Back row there is no Back value: the six spellings callers used to
test three at a time (_BACK, "__back__", _CANCEL, two tuple shapes, bare
None) collapse to `is CANCEL` / `is None`. The gallery now asserts the
absence of an exit row on every screen, on both platforms.

Resolves findings E and §10 of docs/survey-navigation.md. Finding D — Esc
peeling a filter on three screens and leaving on two — still stands, and is
now the only thing left telling a user what Esc does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 23:15:53 -04:00
Jean-Pierre MartineauandClaude Opus 5 80a2a9bd0b Contacts: a contact is dropped one at a time, from its own page
The list could only sweep contacts by age — a purge or nothing. Removing one
named contact now lives where the reader is already looking at it: the last
action on its Node detail page, behind a red Cancel/Remove confirm, and the
list it returns to rebuilds without the row.

The deletion lands in both halves of what the list shows — the device's table
and the cross-session store — because forgetting only the radio's half merges
the contact straight back on the next read. A refusal is put in a dialog and
nothing is forgotten: a contact the radio still holds must not vanish from the
list. Only the contact goes; its reception history and chat messages stay, and
a later send offers to write it back.

An action row's tint now survives the icon lane being dropped — it lands on the
label, as marked_label already does for menu rows — so the delete still reads
as a delete on the PicoCalc.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:39:22 -04:00
Jean-Pierre MartineauandClaude Opus 5 c2640d9660 Path lines: chrome pays its own way, and every route says its length
Three things the path rows owed their readers.

The opens-further `…` was being reserved out of the lane, so a route that
fitted exactly cracked on its own last chip — the row claiming the walk ran
on when it had arrived, the crack being the closing cap with half of it taken
away. It now rides outside the budget (`with_action_mark`, and `cut_to(...,
action=True)`): the path is fitted to the whole lane and the mark takes the
leftovers, or goes unshown. That is the widget's answer everywhere a path row
carries it, not the node page's local trade.

Every stats line hanging under a path now leads with the hop count
(`hops_atom`) — the figure the picture above encodes but never states, and
the first thing two routes are compared on. `direct` where there are none,
which is the app's own word for it; the Explore list's direct-shot provenance
tag was that same word for the same reason, so the atom absorbs it.

And Explore paths cuts its unselected candidates instead of middle-eliding
them, so walking the cursor no longer rewrites the row under it. The `⋯`
rescue saves a route's two endpoints; here they are the same two on every row
by construction, so it spent cells on what the reader knows and took them off
the candidates' front. The probe-results rows pin their ranking lanes out of
the ←→ scroll while they are at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 01:30:02 -04:00
Jean-Pierre MartineauandClaude Opus 5 6a12104cf7 Path lines: cut them all the same way, and end them where they end
Two fixes to what a path line shows at its edges and at its ends.

The edges: the crack only reached the two surfaces that drew their own marks.
Everywhere else a path still met render_to_ansi(no_wrap=True) or a plain
truncate, both of which ellipsize — so the Message paths rows you were not
on trailed off in dots while the one you were on cracked. Those cuts now go
through cut_to, which reads the line and decides: a chip breaks off on the
crack, prose keeps its ellipsis. That covers the unselected arrival rows,
every select-list row (the trophy walks and probe candidates), the TX sweep
action rows, and the composer's suggestion rows. The generic surfaces are
safe to convert precisely because the fallback is the old behaviour.

The ends: a path's ends are the nodes it went between, so a line that opens
on its first *relay* is naming a node that only passed the message on. The
Message paths rows now run origin → relays → us — the same two endpoints the
graph one row up already drew between, so the row and the picture finally
cross-read. A hopless arrival stops collapsing to the word "direct":
Alice ▶ ★ is what direct delivery looks like, on the same rails as every
relayed row.

And our own end is the ★ everywhere, not our name — it was already so on the
trace lane, the trophy card and the composer, and now also on the node
detail Routes rows, the TX sweep route, the mesh walk trail and the live
feed's subject lane. It is the one node the reader never has to be told, and
in an 18-cell feed lane or a width-starved trail the cells it gives back are
hops that stay in view.

Left alone deliberately: the packet viewer's `via` row (the dialog has a
`from` row naming the origin, so `via` really is just the relay chain), the
verbatim wire spec, and widgets._link_text — a table of FROM → TO *links*,
still on the legacy path_text builder.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:36:43 -04:00
Jean-Pierre MartineauandClaude Opus 5 9e262d157d Path lines: a cut chip cracks, it doesn't trail off
An ellipsis at a path line's edge says a word was shortened. What actually
ran out is the lane — the route continues, and in chip mode there is a
segment right there to say so with. A cut chip now breaks off on a half
block drawn in that chip's own fill and no background, so the cell is half
segment and half bare page: the crack reads as a chip that carries on.
CRACK_TAIL (▌) closes a line the path runs past, CRACK_HEAD (▐) opens one
whose start is off to the left.

Only chips can crack, which is also the whole mode test: cut_mark reads the
fill from the nearest cell still drawn — scanning back for a tail cut,
forward for a head cut, so a cut landing on a seam still takes the colour
the reader can see — and falls back to the muted ellipsis when it finds
none. An arrow line has no fill to shear, so it keeps cutting exactly as
before and no caller has to know which mode drew the line.

Applied at the four places a path line is cut rather than folded: the
ellipsized() last resort, the over-wide lone hop in wrapped(), and the two
h-scroll surfaces whose edge marks were literal "…" — the Message paths
selected row and the node detail Routes row (whose unselected rows now go
through cut_to as well). Untouched: the ⋯ that marks whole hops elided out
of a path's middle, which is a different claim entirely.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 18:09:19 -04:00
Jean-Pierre MartineauandClaude Opus 5 641286828d Routes: the graph tags, the list names - Message paths' split
The Routes tab had it the other way round: names in the graph, hashes
in the row list. Names in the fan read well on one route and fall
apart on several - each label is set above or below its own marker,
so a long one spills across the lanes either side of it, and the very
graph that earns its lanes is the one that can least afford them.

So take the split the Message paths dialog already settled on. In the
fan, every relay is its first hash byte alone - two cells, whatever
the node is called; only the two endpoints stay named. In the rows,
every hop the resolver can place reads as its contact name, in that
node's own hue, and only a hop with no name to show stands in its
hash at the device's path-hash width. Nothing spells the same node
twice: the two bands cross-reference by colour.

Names cost cells a hash chain doesn't, which is what the row's own
horizontal scroll is for - so it now draws the faint edge marks the
key lane and the Message paths rows draw, and clamps where the tail
is actually readable rather than one step short of it, under a right
mark promising a remainder the keys could not reach.

self_key falls out of the route line and its view: the far end is our
name now, not our hash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 04:04:37 -04:00
Jean-Pierre MartineauandClaude Opus 5 2319653ea0 Routes: two rows the fan was spending on air, on the console
On the PicoCalc the route graph opened under a blank row and closed
under a caption, neither of which draws anything the picture doesn't
already say. The blank is the renderer's own end padding: it centres
the lane band inside two cells of air while the topmost label wants
only one, so that first canvas row is reliably empty. The stage now
asks for one row over its ceiling and peels that row off, which pays
for the grant, and drops the caption outright — the white line and the
you star make the same point.

Both rows go back into the ceiling, so a ceiling-bound fan draws two
lanes taller on the console than the captioned desktop stage affords
it from the same viewport. The desktop keeps its air and its caption:
this rides frame_border, the same frugality signal the tab strip's air
already binds to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 03:53:43 -04:00
Jean-Pierre MartineauandClaude Fable 5 70fb0ceea3 F-key lane: the F3 toggles were better - restored
The per-item chips (node page F1 Info / F2 Routes, Time machine
F1-F3 spans) revert to the single next-thing toggle on F3, per JP's
call. The PicoCalc window ring still stops at 30 d; its F3 chip just
cycles the three spans that remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 00:10:37 -04:00
Jean-Pierre MartineauandClaude Fable 5 192179d79f Node page: a tab is one press - F1 Info, F2 Routes
The F3 next-tab toggle gave way to one chip per view in strip order;
the tab on screen dims (its key would change nothing this paint), and
Tab/Shift+Tab still cycle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:11:45 -04:00
Jean-Pierre MartineauandClaude Fable 5 6a79ec5b5e Colour marks an identified node: unknown hashes go grey
A hash whose node nobody can name reads whole in node.unknown — path
lines, key lanes, the walk, the packet card, the arrivals — never in a
hue its bytes would derive, exactly as the path graph already drew it.
highlighted_hash gains a known flag; the two path widgets decide it from
their resolver, and the node page threads it per hop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 19:11:12 -04:00
Jean-Pierre MartineauandClaude Opus 5 8a80d47815 F-key lane: a jump rides the Shift half of the page it follows
Home and End are on the PicoCalc's keyboard. Paging is not. So the
jump-to-either-end pair was never the unreachable thing that earned it
F1/F2 -- it only ever wanted a chip for consistency with the pager it
belongs to. Now it rides that pager: Shift+F5 is Top because F5 is
Page up, Shift+F4 is Bottom because F4 is Page down. One axis, one pair
of keys, the second-grade verb sitting behind the first.

That frees F1-F3 on every screen, and the lane is the only place the
device can advertise anything -- there is no hint line to read a chord
off. So the freed slots went to the affordances that were invisible
there:

  select lists  Sect down/up, the section jumps. Ctrl+PgUp on a
                keyboard with no PgUp is the one binding in the app
                that was physically unreachable. Plus Delete, lit on
                exactly the rows the key acts on.
  map           Region (Home, renamed -- it frames the ground the mesh
                covers, it does not "reset" anything), You (new: Ctrl+L,
                recentre on our own node at the zoom you chose), and
                Frame for the find matches.
  node page     the tab it would switch you to.
  time machine  the window span it would take you to -- w was
                undiscoverable on the device.

The map is the one exception to the new rule: Region answers "where
should I be looking", not "how far down the body am I", so it keeps F1
beside the other two destinations rather than hiding behind a pager it
has nothing to do with.

Screens that never scroll (every dialog, the busy splash, the reorder
list) now say so with an empty lane instead of offering a pager that
does nothing. Screens whose Home/End mean something else say what:
Latest/Oldest in a chat, Newest/Oldest in the packet viewer, You on the
walk (where End is bound to nothing, so its slot stays blank).

The map's hint line finally fits 72: its basemap status was never a key
hint, so it moved to the title's status chain where the standards put
those -- which is what took the map off the gallery's pre-existing
overflow list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 14:56:42 -04:00
Jean-Pierre MartineauandClaude Opus 5 e00a0529fa Node page: the key is a viewport you slide, not a line that wraps
A public key is 64 hex digits; the Info tab's value lane is 63 cells at
the 72-column standard and 44 on the PicoCalc. So the key never fit, and
wrapping it spent a whole second line on the handful of digits that fell
off — a line the location preview then had to give up.

It holds one line now and scrolls with ←→, the same step the route
pathline one tab over uses. The marks that say "more this way" sit
beside the window rather than over its first cell, so every window the
lane can settle on starts on a byte boundary: the step is four whole
bytes, and a key read from a mid-byte cut spells it wrong. The shift
clamps against the width actually being drawn, to the first step that
brings the tail into the lane, so scrolling can never run off into empty
lane and a resize only pulls an out-of-range shift back in.

The lane is pinned chrome, not a cursor row — walking down to the action
rows leaves the reader's window where they put it; only leaving the tab
drops it. Both the keys and the footer atom naming them are gated on the
key actually overflowing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 12:02:00 -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 5 7c0c2c5a07 Path lines: bare us endpoints, female point opening a wrapped line
A hop with no label now draws as its seam alone — the arrow, no chip, no
word. The trace screens' route lane uses it (bare_self) for both ends of
every walk: a trace leaves us and comes back to us, so naming ourselves
twice only pushed the hops that *are* news out of the lane. Arrow mode
drops the padding the vacated label sat in ("→ 3d → f2 →"); chip mode
lets the seam point into the page.

Wrapped chip lines now open on the female point (U+E0B2, core set) from
the second line down, so a continuation reads as one — only the very
first chip of a path keeps its square left edge. The packer pays for that
cell, so lines still fit their column.

Also carries the round already sitting in the tree: the wrap evens hops
across the fewest lines it can take and folds at the route's turn, the
trace/node-detail/select surfaces route their rows through the widget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 17:04:49 -04:00
Jean-Pierre MartineauandClaude Fable 5 18b1e034fd Node detail: light route labels through coalesced hop ids
The Routes tab mutes every label off the selected route, testing
membership against the route's raw draw hops. But the path graph folds a
short hop into the wide id it can only be among the drawn routes (a
1-byte trace hop beside the same relay's full contact id), so a route
recorded at the short width greyed the very marker its white line rides
through — TSFCT SUTTON-680M's bf→8e→3d route muted YUL-Cartierville.
Decide membership in the graph's own id space instead, by running the
layers through the same coalesce the widget applies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 17:26:39 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 5ff8eae74b Node detail: fix tab-strip corners, dim inactive tabs, one rule colour
- Swap the active tab's bottom corners: the flat rule turning up into
  the tab's wall is west+north (╯), and turning back down out of it is
  north+east (╰) — had these backwards.
- The left margin belongs to the tab boxes, not the bottom rule: the
  rule is one continuous line, so it now runs the full width including
  through the margin instead of being indented with the tabs.
- Inactive tab outlines move from "muted" to the dimmer "faint" shade.
- The whole bottom rule is now a single colour (accent, the active
  tab's) rather than mixing accent/faint segments — it's one line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:02:52 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 d1c2df61c9 Node detail: flatten unselected tabs into a continuous underline
Only the active tab now breaks the bottom rule (up, across open, back
down); inactive tabs sit flush against it with no corners or seams of
their own, so they read as behind/in-the-dark rather than each having
their own raised box. Top border and its z-order layering (active tab
always wins both corners) are unchanged. Adds a 2-column left margin
when the width has slack for it.

Tab switching moves off the shared Left/Right handler onto Tab/Shift+Tab
only, freeing the arrow keys for other use on this screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 13:53:02 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 76125f7402 Node detail: box every tab, layer corners by z-order
All tabs now render as full boxes at all times (muted by default,
accent when active) instead of only the active one — so every tab's
width is fixed regardless of selection, and labels never shift
column when the active tab changes. Tabs share a single vertical at
each seam; the shared corner "opens" only at the strip's leading
edge and at the active tab's own left edge, "closing" (ceding to the
tab on its left) everywhere else, so the active tab reads as lifted
in front of its neighbours.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 13:33:35 -04:00
Jean-Pierre MartineauandClaude Sonnet 5 a0f91f3b9c Node detail: box the active tab in the tab strip
Since the strip already spent a blank line above and its own line,
box the active tab in accent-coloured rounded corners with its
bottom edge opening into a full-width rule, instead of a plain
── Label ── heading, now that a third line is on the table anyway.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 13:13:14 -04:00
Jean-Pierre MartineauandClaude Fable 5 2d6e20ba2e Node detail: fold the vitals into an Info tab
The identity block was eating the stage's vertical budget on every tab.
The pinned chrome shrinks to the one-line identity header + tab strip;
the vitals rows (key, heard, packets, signal, where), the location
preview, and the Open full map / Time machine actions all move into a
new Info tab (replacing the Map tab — the preview now grows into
whatever rows the tab spares). The Routes tab spends the freed lines on
its graph and route list, and the strip costs a single line of air (the
graph's own top padding provides it).

Opening the full map from the page now also seeds the map's find filter
with the node's name, so it opens centred on the node *and* lit among
the rest — exactly as if the user had typed the find.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 09:59:15 -04:00
Jean-Pierre MartineauandClaude Fable 5 8193ac3823 Node detail: pin the stage, window the route list
The page no longer scrolls as one strip — which could carry the route
graph off the screen while the selection below was still driving it.
The identity block, tab strip, and stage are now pinned; the graph's
row ceiling is struck per render against the viewport (compressing its
lanes before anything would overflow), a faint rule closes the stage,
and the route list windows itself into the leftover lines with the
shared ↑/↓ n-more edge markers, whole hanging-wrapped rows at a time.

Enter on a route row now arms a trace on that route directly (the rows
wear the opens-further-prompts …); the dedicated Trace action survives
only as the auto-route stand-in when there is no evidence to list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 03:32:35 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 2561313a16 Node detail: contract a bidirectional repeater knot to one super-node
Three or more repeaters that relay each other in every order are a
strongly-connected component the left-to-right flow graph can't order. The
bidir-pair merge collapsed all of them onto one column, where their markers and
labels piled up illegibly (labels rammed into the trunk line, the ordering
lost). A two-node pair draws fine as a vertical; three-plus does not.

Contract each such knot to a single super-node — the standard SCC-condensation
move, the one honest way to seat a cycle in a DAG layout. pathgraph gains
`bidir_clusters`, which finds the 3+ mutually-linked groups; node detail
rewrites every route's draw sequence so a knot's members become one synthetic
cluster stop, drawn as a single marker labelled by count and shared type
("3 repeaters", or "n nodes" when mixed). Only the graph geometry contracts —
each route row and its trace spec keep every member named in order, so the list
under the graph still carries the exact ordering the marker folds away, and a
trace still arms on the real path. The tidy two-node pair is left exactly as it
was.

Also completes _collapse's emphasis handling: routes made identical by the
contraction now fold to one line that lights when either of its rows is
selected, keeping the strongest priority for geometry and the emphasised copy's
colour for the highlight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:54:36 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 a770f559cd Node detail: make the Back row leave, like Esc
Committing the Back action resolved a bare "back" token, but the opener's
action loop only breaks on CANCEL (or None) and has no "back" branch — so it
fell through and simply re-showed the page. Back appeared inert while Esc
worked. Resolve CANCEL for the Back row so it leaves the screen on the same
path Esc does, back to the contact list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:34:15 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 8d8193b6b1 Node detail: a tabbed stage with a switchable route list
The page stacked the location preview and the route graph down one long
scroll, and the graph was inert — no way to pick which route it lit. Give
it a tabbed stage instead: a one-line strip (new widgets.tab_strip) pages
Map and Routes one full-height view at a time, switched with ←→ (Tab /
Shift+Tab backup; BackTab now dispatches as "shift_tab"). Tabs are
adaptive — only views with content show, a lone tab collapses to a lit
heading, and our own fix-less node falls back to a bare action list.

The Routes tab folds the old route/suggest info rows into a selectable
route list (best marked ★ best, the firmware's device route, then the
good observed alternatives), each spelled contact→relays→us through the
path widget with a hanging wrap. ↑↓ over the rows drives the graph: the
picked route lights white while the rest go grey and every node off it
fades its label to grey (a per-render label_rgb_of closure — the pathgraph
widget stays generic). A dedicated 🎯 Trace this route row opens the trace
flow armed on whichever route is selected (nothing transmits here).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 19:01:10 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 9cf5a9cc78 Node detail: draw routes inbound (node → us), keep only good ones
The route graph on the node page is built from observation data — every
route it knows was received, not sent — so it now reads left to right in
that inbound direction: the contact on the left, our star on the right.
Renames the section "Routes heard" and the caption "node → you, as heard",
dropping the "path to node" framing that over-claimed a direction observed
paths may not hold in reverse.

Only good alternatives are drawn now, not every chain ever heard: an
observed route earns a grey lane only when its evidence is fresh (its
stalest hop heard within ~4 evidence half-lives) and not a far-weaker
outlier (score within a quarter of the strongest observed). The
best-evidence route still rides white regardless, and a bare direct
neighbour keeps its zero-hop line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:22:28 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 3ac43b7c35 Node graph: a layered redrawing, one marker per node, names throughout
The contact page's route graph fanned every path as a free lane from the two
endpoints and averaged shared relays together, so a well-connected node drew as
an acute tangle of near-overlapping lines. Worse, the same physical node could
appear twice: a 1-byte trace hop (27) standing beside its own wide contact id
(27d4396a2967) whenever the short hash was too ambiguous to widen — YUL-Poly
drew YUL-UpperSalaberry as two separate markers.

Redraw the route graph as a layered left-to-right graph (pathgraph.py):

* longest-path ranks put every node in the column of its hop distance, so each
  edge steps strictly forward and a relay shared by several routes is drawn once;
* an edge that skips a column routes through invisible waypoints, bending around
  the nodes between its ends instead of slicing through them;
* nodes are ordered to cut crossings (barycentre sweeps) then spread down the
  full height with their ys pulled toward their neighbours' (PAVA packing), which
  straightens each route into a lane and opens the branch angles up;
* first-appearance ordering keeps the layout identical on every repaint.

Kill the double-drawn node at two levels. build_topology now coalesces an
under-specified id into the one longer node it can only be (27 -> 27d4396a2967),
folding their links — fixing the atlas and the scenario ranker too, not just this
graph — while leaving a genuinely ambiguous stub (c5 -> two nodes) alone. The
widget then closes the same gap locally across its own layers, catching a hash
that is ambiguous against the whole contact list but unmistakable among these
paths (be -> be1d1c1dbc4b).

On the node detail page specifically: name every node, not just the two ends
(a relay reads by its contact name, unknown ones keep the honest hash byte), and
draw the target with its own map glyph (repeater, room, sensor) to match the
header. us stays on the left, the target on the right.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:47:23 -04:00
Jean-Pierre MartineauandClaude Opus 4.8 62a2d76e8c Node detail: a full-screen page for each contact, reached by Enter
Pressing Enter on a contact now opens its whole story on one page instead of
a bare row: identity (name in its hue, key with the routing hash lit, type,
first/last heard, packet tally, reception SNR/RSSI), an inline location
preview centred on the node (a small static basemap via the new MiniMap), the
observed routes to it, and an action list (trace armed with the suggested best
path, open full map, time machine). Contacts owns the show/open/reshow loop;
topology.suggested answers "the best evidence-backed way to reach this node",
wired ahead of the firmware route in the trace arming precedence. Timemachine
and Trace grew the entry points the page opens.

Four fixes fell out of driving it on real hardware:

Slow open. Each located-node page built a fresh BasemapSource and resolved its
TileJSON -- a blocking HTTPS round-trip (~100ms, up to the 12s timeout on a bad
link) -- on every open. Hold one source for the session (ctx.basemap_source),
shared by the map, the picker, and the preview, and warm it behind the menu at
login, so the resolve is paid once, off the navigation path.

Black map. A companion advertising nonsense coordinates (Homestead reports
lat -97, lon -1042) flung the viewport off the world, leaving the map a screen
of empty/water fill that reads as solid black -- both from "open full map" and
from filtering to it and framing. The old guard only caught the 0/0 no-GPS
null island; the new geo.usable_fix() rejects any out-of-range fix too, applied
to the map markers and the location preview alike. Verified on-device: of 265
located contacts, only Homestead drops.

Path to node. The route/suggest rows render through THE path widget with a
1-byte hash in parentheses (a "name (3d)" tag), so they cross-read hop for hop
against the route graph's byte labels.

Route graph. Draw us on the left and the target on the right (us to target,
"how we reach it", left to right) via a local endpoint swap that leaves the
shared route_graph_style untouched for its other callers, and give the fan a
generous lane step and row budget so a well-connected node's candidate paths
spread into separate, legible tracks instead of a cramped tangle -- the height
stays adaptive, so a one- or two-route node still draws compact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 23:41:52 -04:00