Repository navigation
feat: make the inspector the dashboard - #36
Merged
Merged
Conversation
The v0.3 inspector works but nobody will find it. The dashboard links to it from a grey footer, next to a link that dumps raw JSON. This makes / the console. Traffic is the page, routes become a rail that doubles as the domain filter. Covers the responsive fold, the two states the merge can fumble, and the concurrent probing the live rail now needs.
The console refreshes routes every ten seconds instead of once per page load. Dialing serially with a 300ms timeout made five down routes a 1.5 second render, and the page and its API call each paid separately. Answers are cached for 5 seconds, half the poll interval, so a route that comes up shows within one tick and never two.
The v0.3 inspector was reachable from a 0.8rem grey footer link, next to a link that dumps raw JSON. It is the best thing in the release and almost nobody would find it. / is now the console. The routes table becomes a left rail that doubles as the domain filter, and the rail refreshes on the poll tick instead of only at page load.
refreshRoutes cleared the list and appended fresh rows every ten seconds. #rail is its own scroll container, so a user scrolled down a long rail was snapped back to the top on a tick they did not cause. It now reconciles by domain the way render() reconciles by record id: unchanged rows are never touched, only the dot and the address are updated, new routes insert in place and removed ones are dropped. A selected route removed from the config left nothing marked in the rail while the list stayed filtered to it, which read as the inspector going quiet. The rail now falls back to All traffic and requeries. Also fold the dead duplicate grid-template-columns rule into the original, retitle the page now that it is the front door, and pin the rail's filter contract in the test.
The inspector page moved to /. The README, the CHANGELOG and the 0.3.0 release notes all name the old URL, so it redirects rather than 404s. 302, not 301. A permanent redirect is cached forever and there would be no way to take /inspect back later.
The inspector page said 'The inspector is off' and offered nothing to do about it. With routes on the page there is something better to fall back to: the rail goes full width and the note names the config key. One code path. setOff already handled a reload flipping capture while the page stayed open, so the initial load goes through it too.
Three columns do not fit a half screen window, which is the normal way to watch an inspector. Below 1100px the rail folds to a chip strip above the list. Below 700px the detail becomes a sheet over the list, instead of stacking under a list capped at 45vh where reading a request scrolled the list away. Added a base display:none for #sheetclose outside the media query. It is a new direct child of the main grid, and without a default it would auto-place into an implicit fourth cell at every width, not just below 700px.
body.inspector-off #detail and body.sheet-open #detail tied in specificity, so source order alone decided which won. A sheet opened below 700px could survive a capture-off transition, floating stale content over what should be a full-width routes list. Same tie affected the close button. Added a capture-off rule after the sheet-open rules so it wins the tie. Also commented the two other places where a rule's winner depends on staying after the one it overrides, so a future reorder does not silently flip the result.
…addr The list's path column absorbs whatever the other four fixed-width columns leave. A 40/60 split against the detail pane left less than those four columns needed below about 1000px, crushing the path to nothing at the exact width this task exists to serve. Flipped the split so the list gets 55%. The rail's upstream address had no shrink handling, so a long one overflowed past the rail and got painted over by the list panel next to it instead of truncating. Give it overflow, ellipsis and a min-width of 0 so it truncates cleanly and the domain keeps priority.
Five unlabeled columns is a puzzle. The headers stick to the top of the list. Recent rows now read as an age, which is the question the clock time was standing in for, with the full timestamp on hover. Older than an hour goes back to a clock time.
Seeing the request and reproducing it were separate jobs. Now the detail pane hands you the command. Redacted headers are dropped from the command rather than pasted in. A command that looks right and fails on a [redacted] credential is worse than one that is visibly missing it.
r.method reached the curl command unquoted. net/http accepts any RFC 7230 token as a method, including backticks and dollar signs, so a captured request with a method like GET`id` executed on paste. A hostile page a developer merely visited could plant that row with a fetch call. Route the method through shellQuote like every other field. Add a regression test that pins each field in asCurl to its shellQuote call.
Arrows move the selection, / focuses the filter, Esc leaves a field and closes the detail sheet. j and k work too. Keys are ignored while a field has focus, so arrows still move the caret in the filter box.
A value rendering as the literal string [redacted] reads like the header was sent that way. It is now marked as redacted, with the config key that stores values as sent on hover.
The page had its own copy of "[redacted]" and the test only checked the string appeared somewhere in the body. It appears twice, so changing the JS constant kept the test green while redacted values started rendering as literal text and leaking into copied curl commands. The sentinel now comes from inspect.Redacted through the template data, so there is one definition. The test asserts the whole declaration. Also fold the two route handlers onto one routeViews helper. They built the same slice and called UpstreamAddr three times per route. Document that statuses wants the complete address set, since it drops anything it is not asked about, and that the probe cache is meant to miss on the ten second tick so nobody raises the TTL to make it hit.
The old dashboard rendered each route as a link, so clicking it opened the site. The rail replaced that with click to filter and nothing opened a route any more. With capture off it was worse: the row looked inert but the click still filtered a hidden list. Each row is now two controls. A button filters, a small arrow link opens the site in a new tab. Both are focusable, so the rail is no longer the one region that needs a mouse. The link is skipped by the filter click handler, and its href is set as a property, never as markup. With capture off the filter button is disabled and the link stays live, so the routes page keeps a use.
show() awaited fetch and json unguarded while every sibling caught. A daemon restart threw an unhandled rejection, and the row was already highlighted, so the pane kept showing the last request. It now lands in a note. Holding an arrow key ran a fetch and a full reconcile at the OS repeat rate. Stepping now moves the highlight on the two rows that changed and holds the detail fetch for 120ms, so a held key is one request. Landing on the row you are already on does nothing at all. The ten second re-stamp wrote every row's age cell whether or not the label had changed, including rows past the hour whose label is fixed, and it kept doing it over a hidden table with capture off. It now skips when capture is off, writes only what changed, and stops at the first row past the hour. Capture off left the search box and both selects live, so typing still fired a refresh that 503s. All five header controls go with the state. The detail sheet outlived a clear and an off then on cycle, leaving a sheet over the list saying "Pick a request". Both drop the selection, so both now close the sheet. Arrowing up parked the selected row under the sticky header. Row scroll margin clears it. Drop the dead display: none on #sheetclose inside the sheet media query. The base rule outside it is the one that stops grid auto-placement, and it already applies at that width.
The hour break assumed every row below an hour-old row is older still. It is not. Rows are ordered by record id, and inspect assigns the id when a request completes, while the label comes from when it started. A long poll, an SSE stream or a hung backend that ran for an hour lands at the top of the list with the oldest start time, and the break returned there, freezing every label under it for the life of the page. The upgraded-connection case is already special-cased in the middleware for the same reason, but that covers 101 and h2 extended CONNECT only. Drop the break. The capture-off guard and the per-row dirty check carry the win that mattered: no DOM writes and no table-wide style invalidation for a row whose label did not change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The v0.3 inspector was reachable from a 0.8rem grey footer link, next to a link that dumps raw JSON. It is the best thing in that release and almost nobody would find it.
/is now the console. The routes table becomes a rail on the left that doubles as the domain filter, and it refreshes on a timer instead of only at page load./inspectredirects, since the README, the CHANGELOG and the 0.3.0 release notes all name it.Nothing about capture changed. Same storage, same defaults, same config keys.
What is in here
/, merged from the two old templates. Every load-bearing behaviour of the v0.3 inspector page survives: the reconciling render, the scroll anchor, cursor paging, store-side filtering, the generation counter.Worth a reviewer's attention
asCurlhad a command injection hole in an earlier commit on this branch. Every field was quoted except the HTTP method, and Go'snet/httpaccepts any RFC 7230 token as a method, withtcharincluding backtick,$and'. A hostile page could plant a poisoned row with afetch, and it would execute when someone copied that row as curl. Fixed inefd2630;TestConsoleQuotesEveryCurlFieldnow pins every field.The relative-time re-stamp walks all rows on purpose. Two stopping rules were tried and both are wrong, for different reasons, and the comment above
restamp()records why so neither comes back.Not in here
The console is now mostly JavaScript that no test executes. That is #35, along with three smaller items from the same review.
Design:
docs/superpowers/specs/2026-08-11-inspector-console-design.md